PrefabTestUtils.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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, wait_for_propagation
  9. from editor_python_test_tools.utils import Report
  10. from editor_python_test_tools.utils import TestHelper as helper
  11. from editor_python_test_tools.pyside_utils import wait_for_condition
  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):
  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. :return: Root of the newly created nested entities.
  31. """
  32. assert level_count > 0, "Can't create nested entities with less than one level"
  33. current_entity = EditorEntity.create_editor_entity_at(
  34. pos, name=get_linear_nested_items_name(nested_entities_name_prefix, 0))
  35. root_entity = current_entity
  36. for current_level in range(1, level_count):
  37. current_entity = EditorEntity.create_editor_entity(
  38. parent_id=current_entity.id, name=get_linear_nested_items_name(nested_entities_name_prefix, current_level))
  39. return root_entity
  40. def validate_linear_nested_entities(nested_entities_root, expected_level_count, expected_pos):
  41. """
  42. This is a helper function which helps validate linear nested entities
  43. created by helper function create_linear_nested_entities.
  44. :param nested_entities_root: Root of nested entities created by create_linear_nested_entities.
  45. :param expected_level_count: Number of levels which target nested entities should have.
  46. :param expected_pos: The position where target nested entities should be.
  47. """
  48. assert expected_level_count > 0, "Can't validate nested entities with less than one layer"
  49. assert nested_entities_root.get_parent_id().IsValid(), \
  50. "Root of nested entities should have a valid parent entity"
  51. current_entity = nested_entities_root
  52. level_count = 1
  53. while True:
  54. assert current_entity.id.IsValid(), f"Entity '{current_entity.get_name()}' is not valid"
  55. current_entity_pos = current_entity.get_world_translation()
  56. assert current_entity_pos.IsClose(expected_pos), \
  57. f"Entity '{current_entity.get_name()}' position '{current_entity_pos.ToString()}' " \
  58. f"is not located at expected position '{expected_pos.ToString()}'"
  59. child_entities = current_entity.get_children()
  60. if len(child_entities) == 0:
  61. break
  62. assert len(child_entities) == 1, \
  63. f"These entities are not linearly nested. Entity '{current_entity.get_name}' has {len(child_entities)} child entities"
  64. level_count += 1
  65. child_entity = child_entities[0]
  66. assert child_entity.get_parent_id() == current_entity.id, \
  67. f"Entity '{child_entity.get_name()}' should be under entity '{current_entity.get_name()}'"
  68. current_entity = child_entity
  69. assert level_count == expected_level_count, \
  70. f"Number of levels of nested entities should be {expected_level_count}, not {level_count}"
  71. def create_linear_nested_prefabs(entities, nested_prefabs_file_name_prefix, nested_prefabs_instance_name_prefix, level_count):
  72. """
  73. This is a helper function which helps create nested prefabs
  74. where each nested prefab has only one child prefab at most. For example:
  75. Prefab_0
  76. |- Prefab_1
  77. | |- Prefab_2
  78. | | |- TestEntity
  79. ...
  80. :param entities: Entities which the nested prefabs will be built from.
  81. :param nested_prefabs_file_name_prefix: Name prefix which will be used to generate file names of newly created nested prefabs.
  82. :param level_count: Number of levels the newly constructed nested prefabs will have.
  83. :param nested_prefabs_instance_name_prefix: Name prefix which will be used to generate names of newly created nested prefab instances.
  84. :return: A list of created nested prefabs and a list of created nested prefab instances. Ordered from top to bottom.
  85. """
  86. assert level_count > 0, "Can't create nested prefabs with less than one level"
  87. created_prefabs = []
  88. created_prefab_instances = []
  89. for current_level in range(0, level_count):
  90. current_prefab, current_prefab_instance = Prefab.create_prefab(
  91. entities, get_linear_nested_items_name(nested_prefabs_file_name_prefix, current_level),
  92. prefab_instance_name=get_linear_nested_items_name(nested_prefabs_instance_name_prefix, current_level))
  93. created_prefabs.append(current_prefab)
  94. created_prefab_instances.append(current_prefab_instance)
  95. entities = current_prefab_instance.get_direct_child_entities()
  96. return created_prefabs, created_prefab_instances
  97. def validate_linear_nested_prefab_instances_hierarchy(nested_prefab_instances):
  98. """
  99. This is a helper function which helps validate linear nested prefabs
  100. created by helper function create_linear_nested_prefabs.
  101. :param nested_prefab_instances: A list of nested prefab instances created by create_linear_nested_prefabs. Ordered from top to bottom.
  102. """
  103. if len(nested_prefab_instances) == 0:
  104. return
  105. nested_prefab_instances_root = nested_prefab_instances[0]
  106. assert nested_prefab_instances_root.container_entity.get_parent_id().IsValid(), \
  107. "Root of nested prefabs should have a valid parent entity"
  108. parent_prefab_instance = nested_prefab_instances_root
  109. for current_level in range(0, len(nested_prefab_instances) - 1):
  110. current_prefab_instance = nested_prefab_instances[current_level]
  111. current_prefab_instance_container_entity = current_prefab_instance.container_entity
  112. assert current_prefab_instance_container_entity.id.IsValid(), \
  113. f"Prefab '{current_prefab_instance_container_entity.get_name()}' is not valid"
  114. direct_child_entities = current_prefab_instance.get_direct_child_entities()
  115. assert len(direct_child_entities) == 1, \
  116. f"These prefab instances are not linearly nested. " \
  117. f"Entity '{current_entity.get_name}' has {len(direct_child_entities)} direct child entities"
  118. direct_child_entity = direct_child_entities[0]
  119. inner_prefab_instance = nested_prefab_instances[current_level + 1]
  120. inner_prefab_instance_container_entity = inner_prefab_instance.container_entity
  121. assert direct_child_entity.id == inner_prefab_instance_container_entity.id, \
  122. f"Direct child entity of prefab '{current_prefab_instance_container_entity.get_name()}' " \
  123. f"should be prefab '{inner_prefab_instance_container_entity.get_name()}', " \
  124. f"not '{direct_child_entity.get_name()}'."
  125. assert inner_prefab_instance_container_entity.get_parent_id() == current_prefab_instance_container_entity.id, \
  126. f"Prefab '{inner_prefab_instance_container_entity.get_name()}' should be the inner prefab of " \
  127. f"prefab '{current_prefab_instance_container_entity.get_name()}'"
  128. most_inner_prefab_instance = nested_prefab_instances[-1]
  129. most_inner_prefab_instance_container_entity = most_inner_prefab_instance.container_entity
  130. assert most_inner_prefab_instance_container_entity.id.IsValid(), \
  131. f"Prefab '{most_inner_prefab_instance_container_entity.get_name()}' is not valid"
  132. direct_child_entities = most_inner_prefab_instance.get_direct_child_entities()
  133. for entity in direct_child_entities:
  134. assert entity.get_parent_id() == most_inner_prefab_instance_container_entity.id, \
  135. f"Entity '{entity.get_name()}' should be under prefab '{most_inner_prefab_instance_container_entity.get_name()}'"
  136. def check_entity_children_count(entity_id, expected_children_count):
  137. entity_children_count_matched_result = (
  138. "Entity with a unique name found",
  139. "Entity with a unique name *not* found")
  140. entity = EditorEntity(entity_id)
  141. children_entity_ids = entity.get_children_ids()
  142. entity_children_count_matched = len(children_entity_ids) == expected_children_count
  143. Report.result(entity_children_count_matched_result, entity_children_count_matched)
  144. if not entity_children_count_matched:
  145. Report.info(f"Entity '{entity_id.ToString()}' actual children count: {len(children_entity_ids)}. Expected children count: {expected_children_count}")
  146. return entity_children_count_matched
  147. def open_base_tests_level():
  148. helper.init_idle()
  149. helper.open_level("Prefab", "Base")
  150. def validate_undo_redo_on_prefab_creation(prefab_instance, original_parent_id):
  151. """
  152. This is a helper function which helps validate undo/redo functionality after creating a prefab.
  153. :param prefab_instance: A prefab instance generated by Prefab.create_prefab()
  154. :param original_parent_id: The EntityId of the original parent to the entity that a prefab was created from
  155. """
  156. # Get information on prefab instance's child entities
  157. child_entities = prefab_instance.get_direct_child_entities()
  158. child_entity_names = []
  159. for child_entity in child_entities:
  160. child_entity_names.append(child_entity.get_name())
  161. # Undo the create prefab operation
  162. general.undo()
  163. wait_for_propagation()
  164. # Validate that undo has reverted the addition of the EditorPrefabComponent
  165. is_prefab = editor.EditorComponentAPIBus(bus.Broadcast, "HasComponentOfType", prefab_instance.container_entity.id,
  166. globals.property.EditorPrefabComponentTypeId)
  167. assert not is_prefab, "Undo operation failed. Entity is still recognized as a prefab."
  168. # Validate that undo has restored the original parent entity of the entity used to create the prefab
  169. search_filter = entity.SearchFilter()
  170. search_filter.names = child_entity_names
  171. entities_found = entity.SearchBus(bus.Broadcast, 'SearchEntities', search_filter)
  172. for child_entity in entities_found:
  173. assert EditorEntity(child_entity).get_parent_id() == original_parent_id, \
  174. "Original parent was not restored on Undo."
  175. # Redo the create prefab operation
  176. general.redo()
  177. wait_for_propagation()
  178. # Validate that redo has re-added the EditorPrefabComponent to the prefab instance
  179. is_prefab = editor.EditorComponentAPIBus(bus.Broadcast, "HasComponentOfType", prefab_instance.container_entity.id,
  180. globals.property.EditorPrefabComponentTypeId)
  181. assert is_prefab, "Redo operation failed. Entity is not recognized as a prefab."
  182. # Validate the redo has restored the child entities under the container entity
  183. entities_found = entity.SearchBus(bus.Broadcast, 'SearchEntities', search_filter)
  184. for child_entity in entities_found:
  185. assert EditorEntity(child_entity).get_parent_id() == prefab_instance.container_entity.id, \
  186. "Prefab parent was not restored on Redo."
  187. def validate_spawned_entity_rotation(entity, expected_rotation):
  188. """
  189. This is a helper function which helps validate the rotation of entities spawned via the spawnable API
  190. :param entity: The spawned entity on which to validate transform values
  191. :param expected_rotation: The expected world rotation of the spawned entity
  192. """
  193. spawned_entity_rotation = entity.get_world_rotation()
  194. x_rotation_success = math.isclose(spawned_entity_rotation.x, expected_rotation.x,
  195. rel_tol=1e-5)
  196. y_rotation_success = math.isclose(spawned_entity_rotation.y, expected_rotation.y,
  197. rel_tol=1e-5)
  198. z_rotation_success = math.isclose(spawned_entity_rotation.z, expected_rotation.z,
  199. rel_tol=1e-5)
  200. Report.info(f"Spawned Entity Rotation: Found {spawned_entity_rotation}, expected {expected_rotation}")
  201. return x_rotation_success and y_rotation_success and z_rotation_success
  202. def validate_spawned_entity_scale(entity, expected_scale):
  203. """
  204. This is a helper function which helps validate the scale of entities spawned via the spawnable API
  205. :param entity: The spawned entity on which to validate transform values
  206. :param expected_scale: The expected world scale of the spawned entity
  207. """
  208. spawned_entity_scale = entity.get_world_uniform_scale()
  209. scale_success = spawned_entity_scale == expected_scale
  210. Report.info(f"Spawned Entity Scale: Found {spawned_entity_scale}, expected {expected_scale}")
  211. return scale_success
  212. def validate_spawned_entity_translation(entity, expected_position):
  213. """
  214. This is a helper function which helps validate the world position of entities spawned via the spawnable API
  215. :param entity: The spawned entity on which to validate transform values
  216. :param expected_position: The expected world translation of the spawned entity
  217. """
  218. spawned_entity_position = entity.get_world_translation()
  219. position_success = spawned_entity_position == expected_position
  220. Report.info(f"Spawned Entity Translation: Found {spawned_entity_position}, expected {expected_position}")
  221. return position_success
  222. def validate_spawned_entity_transform(entity, expected_position, expected_rotation, expected_scale):
  223. """
  224. This is a helper function which helps validate the transform of entities spawned via the spawnable API
  225. :param entity: The spawned entity on which to validate transform values
  226. :param expected_position: The expected world position of the spawned entity
  227. :param expected_rotation: The expected world rotation of the spawned entity
  228. :param expected_scale: The expected local scale of the spawned entity
  229. """
  230. position_success = helper.wait_for_condition(lambda: validate_spawned_entity_translation(entity, expected_position),
  231. 5.0)
  232. rotation_success = helper.wait_for_condition(lambda: validate_spawned_entity_rotation(entity, expected_rotation),
  233. 5.0)
  234. scale_success = helper.wait_for_condition(lambda: validate_spawned_entity_scale(entity, expected_scale),
  235. 5.0)
  236. assert position_success, \
  237. f"Entity was not spawned in the position expected: Found {entity.get_world_translation()}, " \
  238. f"expected {expected_position}"
  239. assert rotation_success, \
  240. f"Entity was not spawned with the rotation expected: Found {entity.get_world_rotation()}, " \
  241. f"expected {expected_rotation}"
  242. assert scale_success, \
  243. f"Entity was not spawned with the scale expected: Found {entity.get_world_uniform_scale()}, " \
  244. f"expected {expected_scale}"
  245. return position_success and rotation_success and scale_success