PrefabTestUtils.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. """
  6. import math
  7. from editor_python_test_tools.editor_entity_utils import EditorEntity
  8. from editor_python_test_tools.prefab_utils import Prefab
  9. from editor_python_test_tools.wait_utils import PrefabWaiter
  10. from editor_python_test_tools.utils import Report
  11. from editor_python_test_tools.utils import TestHelper as helper
  12. import azlmbr.bus as bus
  13. import azlmbr.editor as editor
  14. import azlmbr.entity as entity
  15. import azlmbr.legacy.general as general
  16. import azlmbr.globals as globals
  17. def get_linear_nested_items_name(nested_items_name_prefix, current_level):
  18. return f"{nested_items_name_prefix}{current_level}"
  19. def create_linear_nested_entities(nested_entities_name_prefix, level_count, pos, parent_id=None):
  20. """
  21. This is a helper function which helps create nested entities
  22. where each nested entity has only one child entity at most. For example:
  23. Entity_0
  24. |- Entity_1
  25. | |- Entity_2
  26. ...
  27. :param nested_entities_name_prefix: Name prefix which will be used to generate names of newly created nested entities.
  28. :param level_count: Number of levels which the newly constructed nested entities will have.
  29. :param pos: The position where the nested entities will be.
  30. :param parent_id: EntityId of the intended parent to the root entity
  31. :return: Root of the newly created nested entities.
  32. """
  33. assert level_count > 0, "Can't create nested entities with less than one level"
  34. current_entity = EditorEntity.create_editor_entity_at(
  35. pos, name=get_linear_nested_items_name(nested_entities_name_prefix, 0), parent_id=parent_id)
  36. root_entity = current_entity
  37. for current_level in range(1, level_count):
  38. current_entity = EditorEntity.create_editor_entity(
  39. parent_id=current_entity.id, name=get_linear_nested_items_name(nested_entities_name_prefix, current_level))
  40. return root_entity
  41. def validate_linear_nested_entities(nested_entities_root, expected_level_count, expected_pos):
  42. """
  43. This is a helper function which helps validate linear nested entities
  44. created by helper function create_linear_nested_entities.
  45. :param nested_entities_root: Root of nested entities created by create_linear_nested_entities.
  46. :param expected_level_count: Number of levels which target nested entities should have.
  47. :param expected_pos: The position where target nested entities should be.
  48. """
  49. assert expected_level_count > 0, "Can't validate nested entities with less than one layer"
  50. assert nested_entities_root.get_parent_id().IsValid(), \
  51. "Root of nested entities should have a valid parent entity"
  52. current_entity = nested_entities_root
  53. level_count = 1
  54. while True:
  55. assert current_entity.id.IsValid(), f"Entity '{current_entity.get_name()}' is not valid"
  56. current_entity_pos = current_entity.get_world_translation()
  57. assert current_entity_pos.IsClose(expected_pos), \
  58. f"Entity '{current_entity.get_name()}' position '{current_entity_pos.ToString()}' " \
  59. f"is not located at expected position '{expected_pos.ToString()}'"
  60. child_entities = current_entity.get_children()
  61. if len(child_entities) == 0:
  62. break
  63. assert len(child_entities) == 1, \
  64. f"These entities are not linearly nested. Entity '{current_entity.get_name}' has {len(child_entities)} child entities"
  65. level_count += 1
  66. child_entity = child_entities[0]
  67. assert child_entity.get_parent_id() == current_entity.id, \
  68. f"Entity '{child_entity.get_name()}' should be under entity '{current_entity.get_name()}'"
  69. current_entity = child_entity
  70. assert level_count == expected_level_count, \
  71. f"Number of levels of nested entities should be {expected_level_count}, not {level_count}"
  72. def create_linear_nested_prefabs(entities, nested_prefabs_file_name_prefix, nested_prefabs_instance_name_prefix, level_count):
  73. """
  74. This is a helper function which helps create nested prefabs
  75. where each nested prefab has only one child prefab at most. For example:
  76. Prefab_0
  77. |- Prefab_1
  78. | |- Prefab_2
  79. | | |- TestEntity
  80. ...
  81. :param entities: Entities which the nested prefabs will be built from.
  82. :param nested_prefabs_file_name_prefix: Name prefix which will be used to generate file names of newly created nested prefabs.
  83. :param level_count: Number of levels the newly constructed nested prefabs will have.
  84. :param nested_prefabs_instance_name_prefix: Name prefix which will be used to generate names of newly created nested prefab instances.
  85. :return: A list of created nested prefabs and a list of created nested prefab instances. Ordered from top to bottom.
  86. """
  87. assert level_count > 0, "Can't create nested prefabs with less than one level"
  88. created_prefabs = []
  89. created_prefab_instances = []
  90. for current_level in range(0, level_count):
  91. current_prefab, current_prefab_instance = Prefab.create_prefab(
  92. entities, get_linear_nested_items_name(nested_prefabs_file_name_prefix, current_level),
  93. prefab_instance_name=get_linear_nested_items_name(nested_prefabs_instance_name_prefix, current_level))
  94. created_prefabs.append(current_prefab)
  95. created_prefab_instances.append(current_prefab_instance)
  96. entities = current_prefab_instance.get_direct_child_entities()
  97. # Focus on the newly created prefab instance before next creation to perform a prefab edit rather than override edit.
  98. current_prefab_instance.container_entity.focus_on_owning_prefab()
  99. # Switch focus back on the originally focused instance.
  100. parent_entity = EditorEntity(created_prefab_instances[0].container_entity.get_parent_id())
  101. parent_entity.focus_on_owning_prefab()
  102. return created_prefabs, created_prefab_instances
  103. def validate_linear_nested_prefab_instances_hierarchy(nested_prefab_instances):
  104. """
  105. This is a helper function which helps validate linear nested prefabs
  106. created by helper function create_linear_nested_prefabs.
  107. :param nested_prefab_instances: A list of nested prefab instances created by create_linear_nested_prefabs. Ordered from top to bottom.
  108. """
  109. if len(nested_prefab_instances) == 0:
  110. return
  111. nested_prefab_instances_root = nested_prefab_instances[0]
  112. assert nested_prefab_instances_root.container_entity.get_parent_id().IsValid(), \
  113. "Root of nested prefabs should have a valid parent entity"
  114. parent_prefab_instance = nested_prefab_instances_root
  115. for current_level in range(0, len(nested_prefab_instances) - 1):
  116. current_prefab_instance = nested_prefab_instances[current_level]
  117. current_prefab_instance_container_entity = current_prefab_instance.container_entity
  118. assert current_prefab_instance_container_entity.id.IsValid(), \
  119. f"Prefab '{current_prefab_instance_container_entity.get_name()}' is not valid"
  120. direct_child_entities = current_prefab_instance.get_direct_child_entities()
  121. assert len(direct_child_entities) == 1, \
  122. f"These prefab instances are not linearly nested. " \
  123. f"Entity '{current_entity.get_name}' has {len(direct_child_entities)} direct child entities"
  124. direct_child_entity = direct_child_entities[0]
  125. inner_prefab_instance = nested_prefab_instances[current_level + 1]
  126. inner_prefab_instance_container_entity = inner_prefab_instance.container_entity
  127. assert direct_child_entity.id == inner_prefab_instance_container_entity.id, \
  128. f"Direct child entity of prefab '{current_prefab_instance_container_entity.get_name()}' " \
  129. f"should be prefab '{inner_prefab_instance_container_entity.get_name()}', " \
  130. f"not '{direct_child_entity.get_name()}'."
  131. assert inner_prefab_instance_container_entity.get_parent_id() == current_prefab_instance_container_entity.id, \
  132. f"Prefab '{inner_prefab_instance_container_entity.get_name()}' should be the inner prefab of " \
  133. f"prefab '{current_prefab_instance_container_entity.get_name()}'"
  134. most_inner_prefab_instance = nested_prefab_instances[-1]
  135. most_inner_prefab_instance_container_entity = most_inner_prefab_instance.container_entity
  136. assert most_inner_prefab_instance_container_entity.id.IsValid(), \
  137. f"Prefab '{most_inner_prefab_instance_container_entity.get_name()}' is not valid"
  138. direct_child_entities = most_inner_prefab_instance.get_direct_child_entities()
  139. for entity in direct_child_entities:
  140. assert entity.get_parent_id() == most_inner_prefab_instance_container_entity.id, \
  141. f"Entity '{entity.get_name()}' should be under prefab '{most_inner_prefab_instance_container_entity.get_name()}'"
  142. def check_entity_children_count(entity_id, expected_children_count):
  143. entity_children_count_matched_result = (
  144. "Entity with a unique name found",
  145. "Entity with a unique name *not* found")
  146. entity = EditorEntity(entity_id)
  147. children_entity_ids = entity.get_children_ids()
  148. entity_children_count_matched = len(children_entity_ids) == expected_children_count
  149. Report.result(entity_children_count_matched_result, entity_children_count_matched)
  150. if not entity_children_count_matched:
  151. Report.info(f"Entity '{entity_id.ToString()}' actual children count: {len(children_entity_ids)}. Expected children count: {expected_children_count}")
  152. return entity_children_count_matched
  153. def validate_count_for_named_editor_entity(entity_name, expected_count):
  154. """
  155. This is a helper function which helps validate the number of entities for a given name in editor.
  156. :param entity_name: Entity name for the entities to be validated.
  157. :param expected_count: Expected number of entities.
  158. """
  159. entities = EditorEntity.find_editor_entities([entity_name])
  160. assert len(entities) == expected_count, f"{len(entities)} entity(s) found. " \
  161. f"Expected {expected_count} {entity_name} entity(s)."
  162. def validate_child_count_for_named_editor_entity(entity_name, expected_child_count):
  163. """
  164. This is a helper function which helps validate the number of children of entities for a given name in editor.
  165. :param entity_name: Entity name for the entities to be validated.
  166. :param expected_child_count: Expected number of children.
  167. """
  168. entities = EditorEntity.find_editor_entities([entity_name])
  169. for entity in entities:
  170. child_entities = entity.get_children()
  171. assert len(child_entities) == expected_child_count, f"{len(child_entities)} children found. " \
  172. f"Expected {expected_child_count} children for all {entity_name} entity(s)."
  173. def open_base_tests_level():
  174. helper.init_idle()
  175. helper.open_level("Prefab", "Base")
  176. def validate_undo_redo_on_prefab_creation(prefab_instance, original_parent_id):
  177. """
  178. This is a helper function which helps validate undo/redo functionality after creating a prefab.
  179. :param prefab_instance: A prefab instance generated by Prefab.create_prefab()
  180. :param original_parent_id: The EntityId of the original parent to the entity that a prefab was created from
  181. """
  182. # Get information on prefab instance's child entities
  183. child_entities = prefab_instance.get_direct_child_entities()
  184. child_entity_names = []
  185. for child_entity in child_entities:
  186. child_entity_names.append(child_entity.get_name())
  187. # Undo the create prefab operation
  188. general.undo()
  189. PrefabWaiter.wait_for_propagation()
  190. # Validate that undo has reverted the addition of the EditorPrefabComponent
  191. is_prefab = editor.EditorComponentAPIBus(bus.Broadcast, "HasComponentOfType", prefab_instance.container_entity.id,
  192. globals.property.EditorPrefabComponentTypeId)
  193. assert not is_prefab, "Undo operation failed. Entity is still recognized as a prefab."
  194. # Validate that undo has restored the original parent entity of the entity used to create the prefab
  195. search_filter = entity.SearchFilter()
  196. search_filter.names = child_entity_names
  197. entities_found = entity.SearchBus(bus.Broadcast, 'SearchEntities', search_filter)
  198. for child_entity in entities_found:
  199. assert EditorEntity(child_entity).get_parent_id() == original_parent_id, \
  200. "Original parent was not restored on Undo."
  201. # Redo the create prefab operation
  202. general.redo()
  203. PrefabWaiter.wait_for_propagation()
  204. # Validate that redo has re-added the EditorPrefabComponent to the prefab instance
  205. is_prefab = editor.EditorComponentAPIBus(bus.Broadcast, "HasComponentOfType", prefab_instance.container_entity.id,
  206. globals.property.EditorPrefabComponentTypeId)
  207. assert is_prefab, "Redo operation failed. Entity is not recognized as a prefab."
  208. # Validate the redo has restored the child entities under the container entity
  209. entities_found = entity.SearchBus(bus.Broadcast, 'SearchEntities', search_filter)
  210. for child_entity in entities_found:
  211. assert EditorEntity(child_entity).get_parent_id() == prefab_instance.container_entity.id, \
  212. "Prefab parent was not restored on Redo."
  213. def validate_spawned_entity_rotation(entity, expected_rotation):
  214. """
  215. This is a helper function which helps validate the rotation of entities spawned via the spawnable API
  216. :param entity: The spawned entity on which to validate transform values
  217. :param expected_rotation: The expected world rotation of the spawned entity
  218. """
  219. spawned_entity_rotation = entity.get_world_rotation()
  220. x_rotation_success = math.isclose(spawned_entity_rotation.x, expected_rotation.x,
  221. rel_tol=1e-5)
  222. y_rotation_success = math.isclose(spawned_entity_rotation.y, expected_rotation.y,
  223. rel_tol=1e-5)
  224. z_rotation_success = math.isclose(spawned_entity_rotation.z, expected_rotation.z,
  225. rel_tol=1e-5)
  226. Report.info(f"Spawned Entity Rotation: Found {spawned_entity_rotation}, expected {expected_rotation}")
  227. return x_rotation_success and y_rotation_success and z_rotation_success
  228. def validate_spawned_entity_scale(entity, expected_scale):
  229. """
  230. This is a helper function which helps validate the scale of entities spawned via the spawnable API
  231. :param entity: The spawned entity on which to validate transform values
  232. :param expected_scale: The expected world scale of the spawned entity
  233. """
  234. spawned_entity_scale = entity.get_world_uniform_scale()
  235. scale_success = spawned_entity_scale == expected_scale
  236. Report.info(f"Spawned Entity Scale: Found {spawned_entity_scale}, expected {expected_scale}")
  237. return scale_success
  238. def validate_spawned_entity_translation(entity, expected_position):
  239. """
  240. This is a helper function which helps validate the world position of entities spawned via the spawnable API
  241. :param entity: The spawned entity on which to validate transform values
  242. :param expected_position: The expected world translation of the spawned entity
  243. """
  244. spawned_entity_position = entity.get_world_translation()
  245. position_success = spawned_entity_position == expected_position
  246. Report.info(f"Spawned Entity Translation: Found {spawned_entity_position}, expected {expected_position}")
  247. return position_success
  248. def validate_spawned_entity_transform(entity, expected_position, expected_rotation, expected_scale):
  249. """
  250. This is a helper function which helps validate the transform of entities spawned via the spawnable API
  251. :param entity: The spawned entity on which to validate transform values
  252. :param expected_position: The expected world position of the spawned entity
  253. :param expected_rotation: The expected world rotation of the spawned entity
  254. :param expected_scale: The expected local scale of the spawned entity
  255. """
  256. position_success = helper.wait_for_condition(lambda: validate_spawned_entity_translation(entity, expected_position),
  257. 5.0)
  258. rotation_success = helper.wait_for_condition(lambda: validate_spawned_entity_rotation(entity, expected_rotation),
  259. 5.0)
  260. scale_success = helper.wait_for_condition(lambda: validate_spawned_entity_scale(entity, expected_scale),
  261. 5.0)
  262. assert position_success, \
  263. f"Entity was not spawned in the position expected: Found {entity.get_world_translation()}, " \
  264. f"expected {expected_position}"
  265. assert rotation_success, \
  266. f"Entity was not spawned with the rotation expected: Found {entity.get_world_rotation()}, " \
  267. f"expected {expected_rotation}"
  268. assert scale_success, \
  269. f"Entity was not spawned with the scale expected: Found {entity.get_world_uniform_scale()}, " \
  270. f"expected {expected_scale}"
  271. return position_success and rotation_success and scale_success
  272. def validate_expected_override_status(entity: EditorEntity, expected_override_status: bool) -> None:
  273. """
  274. Validates the expected override status of the given entity. NOTE: This should only be used on an entity within a
  275. prefab as this will currently always return as True on a container entity.
  276. :param entity: The EditorEntity to validate the status of overrides on
  277. :param expected_override_status: True if overrides are expected, False otherwise
  278. :return: None
  279. """
  280. if expected_override_status:
  281. assert entity.has_overrides(), \
  282. f"Found no overrides on expected entity: {entity.id}"
  283. else:
  284. assert not entity.has_overrides(), \
  285. f"Found overrides present on unexpected entity: {entity.id}"
  286. def validate_expected_components(entity: EditorEntity, expected_components: list = None,
  287. unexpected_components: list = None) -> None:
  288. """
  289. Validates that the entity has the given expected components, and none of the unexpected components.
  290. Useful for ensuring prefab overrides have affected only specific entities.
  291. :return: None
  292. """
  293. if expected_components:
  294. for component in expected_components:
  295. assert entity.has_component(component), \
  296. f"Failed to find expected {component} component on {entity.get_name()} with id {entity.id}"
  297. if unexpected_components:
  298. for component in unexpected_components:
  299. assert not entity.has_component(component), \
  300. f"Unexpectedly found {component} component on {entity.get_name()} with id {entity.id}"