diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py index 0b4cf47961..fec9d9880b 100644 --- a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py @@ -214,6 +214,12 @@ class EditorEntity: """ return editor.EditorEntityInfoRequestBus(bus.Event, "GetParent", self.id) + def get_children_ids(self) -> List[azlmbr.entity.EntityId]: + """ + :return: Entity ids of children. Type: [entity.EntityId()] + """ + return editor.EditorEntityInfoRequestBus(bus.Event, "GetChildren", self.id) + def add_component(self, component_name: str) -> EditorComponent: """ Used to add new component to Entity. diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab.py b/AutomatedTesting/Gem/PythonTests/prefab/Prefab.py new file mode 100644 index 0000000000..14a1ab62f0 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/prefab/Prefab.py @@ -0,0 +1,221 @@ +""" +Copyright (c) Contributors to the Open 3D Engine Project. +For complete copyright and license terms please see the LICENSE at the root of this distribution. + +SPDX-License-Identifier: Apache-2.0 OR MIT +""" + +from __future__ import annotations +from collections import Counter +from collections import deque +from os import path + +from PySide2 import QtWidgets + +from azlmbr.entity import EntityId +from azlmbr.math import Vector3 +from editor_python_test_tools.editor_entity_utils import EditorEntity +from editor_python_test_tools.utils import Report + +import azlmbr.bus as bus +import azlmbr.prefab as prefab +import editor_python_test_tools.pyside_utils as pyside_utils +import prefab.Prefab_Test_Utils as prefab_test_utils + +# This is a helper class which contains some of the useful information about a prefab instance. +class PrefabInstance: + + def __init__(self, name: str=None, prefab_file_name: str=None, container_entity: EditorEntity=EntityId()): + self.name = name + self.prefab_file_name: str = prefab_file_name + self.container_entity: EditorEntity = container_entity + + """ + See if this instance is valid to be used with other prefab operations. + :return: Whether the target instance is valid or not. + """ + def is_valid() -> bool: + return self.container_entity.id.IsValid() and self.name is not None and self.prefab_file_name in Prefab.existing_prefabs + + + """ + Reparent this instance to target parent entity. + The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs. + :param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next. + """ + async def ui_reparent_prefab_instance(self, parent_entity_id: EntityId): + container_entity_name = self.container_entity.get_name() + current_children_entity_ids_having_prefab_name = prefab_test_utils.get_children_ids_by_name(parent_entity_id, container_entity_name) + Report.info(f'current_children_entity_ids_having_prefab_name: {current_children_entity_ids_having_prefab_name}') + + pyside_utils.run_soon(lambda: self.container_entity.set_parent_entity(parent_entity_id)) + pyside_utils.run_soon(lambda: prefab_test_utils.wait_for_propagation()) + + try: + active_modal_widget = await pyside_utils.wait_for_modal_widget() + error_message_box = active_modal_widget.findChild(QtWidgets.QMessageBox) + ok_button = error_message_box.button(QtWidgets.QMessageBox.Ok) + ok_button.click() + assert False, "Cyclical dependency detected while reparenting prefab" + except pyside_utils.EventLoopTimeoutException: + pass + + updated_children_entity_ids_having_prefab_name = prefab_test_utils.get_children_ids_by_name(parent_entity_id, container_entity_name) + Report.info(f'updated_children_entity_ids_having_prefab_name: {updated_children_entity_ids_having_prefab_name}') + new_child_with_reparented_prefab_name_added = len(updated_children_entity_ids_having_prefab_name) == len(current_children_entity_ids_having_prefab_name) + 1 + assert new_child_with_reparented_prefab_name_added, "No entity with reparented prefab name become a child of target parent entity" + + updated_container_entity_id = set(updated_children_entity_ids_having_prefab_name).difference(current_children_entity_ids_having_prefab_name).pop() + updated_container_entity = EditorEntity(updated_container_entity_id) + updated_container_entity_parent_id = updated_container_entity.get_parent_id() + has_correct_parent = updated_container_entity_parent_id.ToString() == parent_entity_id.ToString() + assert has_correct_parent, "Prefab reparented is *not* under the expected parent entity" + + self.container_entity = EditorEntity(updated_container_entity_id) + +# This is a helper class which contains some of the useful information about a prefab template. +class Prefab: + + existing_prefabs = {} + + def __init__(self, file_name: str): + self.file_name:str = file_name + self.file_path: str = prefab_test_utils.get_prefab_file_path(file_name) + self.instances: dict = {} + + """ + Check if a prefab is ready to be used to generate its instances. + :param file_name: A unique file name of the target prefab. + :return: Whether the target prefab is loaded or not. + """ + @classmethod + def is_prefab_loaded(cls, file_name: str) -> bool: + return file_name in Prefab.existing_prefabs + + """ + Check if a prefab exists in the directory for files of prefab tests. + :param file_name: A unique file name of the target prefab. + :return: Whether the target prefab exists or not. + """ + @classmethod + def prefab_exists(cls, file_name: str) -> bool: + file_path = prefab_test_utils.get_prefab_file_path(file_name) + return path.exists(file_path) + + """ + Return a prefab which can be used immediately. + :param file_name: A unique file name of the target prefab. + :return: The prefab with given file name. + """ + @classmethod + def get_prefab(cls, file_name: str) -> Prefab: + if Prefab.is_prefab_loaded(file_name): + return Prefab.existing_prefabs[file_name] + else: + assert Prefab.prefab_exists(file_name), f"Attempted to get a prefab {file_name} that doesn't exist" + new_prefab = Prefab(file_name) + Prefab.existing_prefabs[file_name] = Prefab(file_name) + return new_prefab + + """ + Create a prefab in memory and return it. The very first instance of this prefab will also be created. + :param entities: The entities that should form the new prefab (along with their descendants). + :param file_name: A unique file name of new prefab. + :param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name. + :return: An outcome object with an entityId of the new prefab's container entity; on failure, it comes with an error message detailing the cause of the error. + """ + @classmethod + def create_prefab(cls, entities: list[EditorEntity], file_name: str, prefab_instance_name: str=None) -> Prefab: + assert not Prefab.is_prefab_loaded(file_name), f"Can't create Prefab '{file_name}' since the prefab already exists" + + new_prefab = Prefab(file_name) + entity_ids = [entity.id for entity in entities] + create_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'CreatePrefabInMemory', entity_ids, new_prefab.file_path) + assert create_prefab_result.IsSuccess(), f"Prefab operation 'CreatePrefab' failed. Error: {create_prefab_result.GetError()}" + + container_entity = EditorEntity(create_prefab_result.GetValue()) + + if prefab_instance_name: + container_entity.set_name(prefab_instance_name) + else: + prefab_instance_name = file_name + + prefab_test_utils.wait_for_propagation() + container_entity_id = prefab_test_utils.find_entity_by_unique_name(prefab_instance_name) + new_prefab.instances[prefab_instance_name] = PrefabInstance(prefab_instance_name, file_name, EditorEntity(container_entity_id)) + Prefab.existing_prefabs[file_name] = new_prefab + return new_prefab + + """ + Remove target prefab instances. + :param prefab_instances: Instances to be removed. + """ + @classmethod + def remove_prefabs(cls, prefab_instances: list[PrefabInstance]): + instances_to_remove_name_counts = Counter() + instances_removed_expected_name_counts = Counter() + + entities_to_remove = [prefab_instance.container_entity for prefab_instance in prefab_instances] + while entities_to_remove: + entity = entities_to_remove.pop(-1) + entity_name = entity.get_name() + instances_to_remove_name_counts[entity_name] += 1 + + children_entity_ids = entity.get_children_ids() + for child_entity_id in children_entity_ids: + entities_to_remove.append(EditorEntity(child_entity_id)) + + for entity_name, entity_count in instances_to_remove_name_counts.items(): + entities = prefab_test_utils.find_entities_by_name(entity_name) + instances_removed_expected_name_counts[entity_name] = len(entities) - entity_count + + container_entity_ids = [prefab_instance.container_entity.id for prefab_instance in prefab_instances] + delete_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DeleteEntitiesAndAllDescendantsInInstance', container_entity_ids) + assert delete_prefab_result.IsSuccess(), f"Prefab operation 'DeleteEntitiesAndAllDescendantsInInstance' failed. Error: {delete_prefab_result.GetError()}" + + prefab_test_utils.wait_for_propagation() + + prefab_entities_deleted = True + for entity_name, expected_entity_count in instances_removed_expected_name_counts.items(): + actual_entity_count = len(prefab_test_utils.find_entities_by_name(entity_name)) + if actual_entity_count is not expected_entity_count: + prefab_entities_deleted = False + break + + assert prefab_entities_deleted, "Not all entities and descendants in target prefabs are deleted." + + for instance in prefab_instances: + instance_deleted_prefab = Prefab.get_prefab(instance.prefab_file_name) + instance_deleted_prefab.instances.pop(instance.name) + instance = PrefabInstance() + + """ + Instantiate an instance of this prefab. + :param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name. + :param parent_entity: The entity the prefab should be a child of in the transform hierarchy. + :param prefab_position: The position in world space the prefab should be instantiated in. + :return: An outcome object with an entityId of the new prefab's container entity; on failure, it comes with an error message detailing the cause of the error. + """ + def instantiate(self, name: str=None, parent_entity: EditorEntity=None, prefab_position: Vector3=Vector3()) -> PrefabInstance: + parent_entity_id = parent_entity.id if parent_entity is not None else EntityId() + + instantiate_prefab_result = prefab.PrefabPublicRequestBus( + bus.Broadcast, 'InstantiatePrefab', self.file_path, parent_entity_id, prefab_position) + + assert instantiate_prefab_result.IsSuccess(), f"Prefab operation 'InstantiatePrefab' failed. Error: {instantiate_prefab_result.GetError()}" + + container_entity_id = instantiate_prefab_result.GetValue() + container_entity = EditorEntity(container_entity_id) + + if name: + container_entity.set_name(name) + else: + name = self.file_name + + prefab_test_utils.wait_for_propagation() + container_entity_id = prefab_test_utils.find_entity_by_unique_name(name) + self.instances[name] = PrefabInstance(name, self.file_name, EditorEntity(container_entity_id)) + + prefab_test_utils.check_entity_at_position(container_entity_id, prefab_position) + + return container_entity_id diff --git a/AutomatedTesting/Gem/PythonTests/prefab/PrefabLevel_BasicWorkflow.py b/AutomatedTesting/Gem/PythonTests/prefab/PrefabLevel_BasicWorkflow.py deleted file mode 100644 index 7a600f7976..0000000000 --- a/AutomatedTesting/Gem/PythonTests/prefab/PrefabLevel_BasicWorkflow.py +++ /dev/null @@ -1,108 +0,0 @@ -""" -Copyright (c) Contributors to the Open 3D Engine Project. -For complete copyright and license terms please see the LICENSE at the root of this distribution. - -SPDX-License-Identifier: Apache-2.0 OR MIT -""" - -# fmt:off -class Tests(): - create_new_entity = ("'CreateNewEntity' passed", "'CreateNewEntity' failed") - create_prefab = ("'CreatePrefab' passed", "'CreatePrefab' failed") - instantiate_prefab = ("'InstantiatePrefab' passed", "'InstantiatePrefab' failed") - has_one_child = ("instantiated prefab contains only one child as expected", "instantiated prefab does *not* contain only one child as expected") - instantiated_prefab_position = ("instantiated prefab's position is at the expected position", "instantiated prefab's position is *not* at the expected position") - delete_prefab = ("'DeleteEntitiesAndAllDescendantsInInstance' passed", "'DeleteEntitiesAndAllDescendantsInInstance' failed") - instantiated_prefab_removed = ("instantiated prefab's container entity has been removed", "instantiated prefab's container entity has *not* been removed") - instantiated_child_removed = ("instantiated prefab's child entity has been removed", "instantiated prefab's child entity has *not* been removed") -# fmt:on - -def PrefabLevel_BasicWorkflow(): - """ - This test will help verify if the following functions related to Prefab work as expected: - - CreatePrefab - - InstantiatePrefab - - DeleteEntitiesAndAllDescendantsInInstance - """ - - import os - import sys - - from editor_python_test_tools.utils import Report - from editor_python_test_tools.utils import TestHelper as helper - import editor_python_test_tools.hydra_editor_utils as hydra - - import azlmbr.bus as bus - import azlmbr.entity as entity - from azlmbr.entity import EntityId - import azlmbr.editor as editor - import azlmbr.prefab as prefab - from azlmbr.math import Vector3 - import azlmbr.legacy.general as general - - NEW_PREFAB_NAME = "new_prefab" - NEW_PREFAB_FILE_NAME = NEW_PREFAB_NAME + ".prefab" - NEW_PREFAB_FILE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), NEW_PREFAB_FILE_NAME) - INSTANTIATED_PREFAB_POSITION = Vector3(10.00, 20.0, 30.0) - INSTANTIATED_PREFAB_NAME = "instantiated_prefab" - INSTANTIATED_CHILD_ENTITY_NAME = "child_1" - TEST_LEVEL_FOLDER = "Prefab" - TEST_LEVEL_NAME = "Base" - - def find_entity_by_name(entity_name): - searchFilter = entity.SearchFilter() - searchFilter.names = [entity_name] - entityIds = entity.SearchBus(bus.Broadcast, 'SearchEntities', searchFilter) - if entityIds and entityIds[0].IsValid(): - return entityIds[0] - return None - - def print_error_if_failed(prefab_operation_result): - if not prefab_operation_result.IsSuccess(): - Report.info(f'Error message: {prefab_operation_result.GetError()}') - - -# Open the test level - helper.init_idle() - helper.open_level(TEST_LEVEL_FOLDER, TEST_LEVEL_NAME) - -# Create a new Entity at the root level - new_entity_id = editor.ToolsApplicationRequestBus(bus.Broadcast, 'CreateNewEntity', EntityId()) - Report.result(Tests.create_new_entity, new_entity_id.IsValid()) - -# Checks for prefab creation passed or not - create_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'CreatePrefabInMemory', [new_entity_id], NEW_PREFAB_FILE_PATH) - Report.result(Tests.create_prefab, create_prefab_result.IsSuccess()) - print_error_if_failed(create_prefab_result) - -# Checks for prefab instantiation passed or not - instantiate_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'InstantiatePrefab', NEW_PREFAB_FILE_PATH, EntityId(), INSTANTIATED_PREFAB_POSITION) - Report.result(Tests.instantiate_prefab, instantiate_prefab_result.IsSuccess() and instantiate_prefab_result.GetValue().IsValid()) - print_error_if_failed(instantiate_prefab_result) - - container_entity_id = instantiate_prefab_result.GetValue() - editor.EditorEntityAPIBus(bus.Event, 'SetName', container_entity_id, INSTANTIATED_PREFAB_NAME) - - children_entity_ids = editor.EditorEntityInfoRequestBus(bus.Event, 'GetChildren', container_entity_id) - Report.result(Tests.has_one_child, len(children_entity_ids) is 1) - - child_entity_id = children_entity_ids[0] - editor.EditorEntityAPIBus(bus.Event, 'SetName', child_entity_id, INSTANTIATED_CHILD_ENTITY_NAME) - -# Checks if the new prefab is at the correct position and if it fails, it will provide the expected postion and the actual postion of the entity in the Editor log - actual_prefab_position = azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldTranslation", container_entity_id) - is_at_position = actual_prefab_position.IsClose(INSTANTIATED_PREFAB_POSITION) - Report.result(Tests.instantiated_prefab_position, is_at_position) - if not is_at_position: - Report.info(f'Expected position: {INSTANTIATED_PREFAB_POSITION.ToString()}, actual position: {actual_prefab_position.ToString()}') - -# Checks for prefab deletion passed or not - delete_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DeleteEntitiesAndAllDescendantsInInstance', [container_entity_id]) - Report.result(Tests.delete_prefab, delete_prefab_result.IsSuccess()) - print_error_if_failed(delete_prefab_result) - Report.result(Tests.instantiated_prefab_removed, find_entity_by_name(INSTANTIATED_PREFAB_NAME) is None) - Report.result(Tests.instantiated_child_removed, find_entity_by_name(INSTANTIATED_CHILD_ENTITY_NAME) is None) - -if __name__ == "__main__": - from editor_python_test_tools.utils import Report - Report.start_test(PrefabLevel_BasicWorkflow) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndDeletePrefab.py b/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndDeletePrefab.py new file mode 100644 index 0000000000..c6e0daa4dd --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndDeletePrefab.py @@ -0,0 +1,34 @@ +""" +Copyright (c) Contributors to the Open 3D Engine Project. +For complete copyright and license terms please see the LICENSE at the root of this distribution. + +SPDX-License-Identifier: Apache-2.0 OR MIT +""" + +def Prefab_BasicWorkflow_CreateAndDeletePrefab(): + + CAR_PREFAB_FILE_NAME = 'car_prefab' + + from editor_python_test_tools.editor_entity_utils import EditorEntity + from prefab.Prefab import Prefab + + import prefab.Prefab_Test_Utils as prefab_test_utils + + prefab_test_utils.open_base_tests_level() + + # Creates a new Entity at the root level + # Asserts if creation didn't succeed + car_entity = EditorEntity.create_editor_entity() + car_prefab_entities = [car_entity] + + # Checks for prefab creation passed or not + car_prefab = Prefab.create_prefab( + car_prefab_entities, CAR_PREFAB_FILE_NAME) + + # Checks for prefab deletion passed or not + car = car_prefab.instances[CAR_PREFAB_FILE_NAME] + Prefab.remove_prefabs([car]) + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(Prefab_BasicWorkflow_CreateAndDeletePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndReparentPrefab.py b/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndReparentPrefab.py new file mode 100644 index 0000000000..04f7b97628 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndReparentPrefab.py @@ -0,0 +1,51 @@ +""" +Copyright (c) Contributors to the Open 3D Engine Project. +For complete copyright and license terms please see the LICENSE at the root of this distribution. + +SPDX-License-Identifier: Apache-2.0 OR MIT +""" + +def Prefab_BasicWorkflow_CreateAndReparentPrefab(): + + CAR_PREFAB_FILE_NAME = 'car_prefab' + WHEEL_PREFAB_FILE_NAME = 'wheel_prefab' + + import editor_python_test_tools.pyside_utils as pyside_utils + + @pyside_utils.wrap_async + async def run_test(): + + from editor_python_test_tools.editor_entity_utils import EditorEntity + from prefab.Prefab import Prefab + + import prefab.Prefab_Test_Utils as prefab_test_utils + + prefab_test_utils.open_base_tests_level() + + # Creates a new Entity at the root level + # Asserts if creation didn't succeed + car_entity = EditorEntity.create_editor_entity() + car_prefab_entities = [car_entity] + + # Checks for prefab creation passed or not + car_prefab = Prefab.create_prefab( + car_prefab_entities, CAR_PREFAB_FILE_NAME) + + # Creates another new Entity at the root level + wheel_entity = EditorEntity.create_editor_entity() + wheel_prefab_entities = [wheel_entity] + + # Checks for wheel prefab creation passed or not + wheel_prefab = Prefab.create_prefab( + wheel_prefab_entities, WHEEL_PREFAB_FILE_NAME) + + # Checks for prefab reparenting passed or not + car = car_prefab.instances[CAR_PREFAB_FILE_NAME] + wheel = wheel_prefab.instances[WHEEL_PREFAB_FILE_NAME] + await wheel.ui_reparent_prefab_instance(car.container_entity.id) + + run_test() + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(Prefab_BasicWorkflow_CreateAndReparentPrefab) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreatePrefab.py b/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreatePrefab.py new file mode 100644 index 0000000000..d86b03d5ad --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreatePrefab.py @@ -0,0 +1,30 @@ +""" +Copyright (c) Contributors to the Open 3D Engine Project. +For complete copyright and license terms please see the LICENSE at the root of this distribution. + +SPDX-License-Identifier: Apache-2.0 OR MIT +""" + +def Prefab_BasicWorkflow_CreatePrefab(): + + CAR_PREFAB_FILE_NAME = 'car_prefab' + + from editor_python_test_tools.editor_entity_utils import EditorEntity + from editor_python_test_tools.utils import Report + from prefab.Prefab import Prefab + + import prefab.Prefab_Test_Utils as prefab_test_utils + + prefab_test_utils.open_base_tests_level() + + # Creates a new Entity at the root level + # Asserts if creation didn't succeed + car_entity = EditorEntity.create_editor_entity() + car_prefab_entities = [car_entity] + + # Checks for prefab creation passed or not + Prefab.create_prefab(car_prefab_entities, CAR_PREFAB_FILE_NAME) + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(Prefab_BasicWorkflow_CreatePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_InstantiatePrefab.py b/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_InstantiatePrefab.py new file mode 100644 index 0000000000..b9015ab556 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_InstantiatePrefab.py @@ -0,0 +1,34 @@ +""" +Copyright (c) Contributors to the Open 3D Engine Project. +For complete copyright and license terms please see the LICENSE at the root of this distribution. + +SPDX-License-Identifier: Apache-2.0 OR MIT +""" + +def Prefab_BasicWorkflow_InstantiatePrefab(): + + from azlmbr.math import Vector3 + + EXISTING_TEST_PREFAB_FILE_NAME = "Test" + INSTANTIATED_TEST_PREFAB_POSITION = Vector3(10.00, 20.0, 30.0) + EXPECTED_TEST_PREFAB_CHILDREN_COUNT = 1 + + from prefab.Prefab import Prefab + + import prefab.Prefab_Test_Utils as prefab_test_utils + + prefab_test_utils.open_base_tests_level() + + # Checks for prefab instantiation passed or not + test_prefab = Prefab.get_prefab(EXISTING_TEST_PREFAB_FILE_NAME) + + instantiated_test_container_entity_id = test_prefab.instantiate( + prefab_position=INSTANTIATED_TEST_PREFAB_POSITION) + + prefab_test_utils.check_entity_children_count( + instantiated_test_container_entity_id, + EXPECTED_TEST_PREFAB_CHILDREN_COUNT) + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(Prefab_BasicWorkflow_InstantiatePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_Test_Utils.py b/AutomatedTesting/Gem/PythonTests/prefab/Prefab_Test_Utils.py new file mode 100644 index 0000000000..8cd59ed077 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/prefab/Prefab_Test_Utils.py @@ -0,0 +1,94 @@ +""" +Copyright (c) Contributors to the Open 3D Engine Project. +For complete copyright and license terms please see the LICENSE at the root of this distribution. + +SPDX-License-Identifier: Apache-2.0 OR MIT +""" + +import os + +from azlmbr.entity import EntityId +from azlmbr.math import Vector3 +from editor_python_test_tools.editor_entity_utils import EditorEntity +from editor_python_test_tools.utils import Report +from editor_python_test_tools.utils import TestHelper as helper + +import azlmbr.bus as bus +import azlmbr.components as components +import azlmbr.entity as entity +import azlmbr.legacy.general as general + +def get_prefab_file_name(prefab_name): + return prefab_name + ".prefab" + +def get_prefab_file_path(prefab_name): + return os.path.join(os.path.dirname(os.path.abspath(__file__)), get_prefab_file_name(prefab_name)) + +def find_entities_by_name(entity_name): + searchFilter = entity.SearchFilter() + searchFilter.names = [entity_name] + return entity.SearchBus(bus.Broadcast, 'SearchEntities', searchFilter) + +def find_entity_by_unique_name(entity_name): + unique_name_entity_found_result = ( + "Entity with a unique name found", + "Entity with a unique name *not* found") + + entities = find_entities_by_name(entity_name) + unique_name_entity_found = len(entities) == 1 + Report.result(unique_name_entity_found_result, unique_name_entity_found) + + if unique_name_entity_found: + return entities[0] + else: + Report.info(f"{len(entities)} entities with name '{entity_name}' found") + return EntityId() + +def check_entity_at_position(entity_id, expected_entity_position): + entity_at_expected_position_result = ( + "entity is at expected position", + "entity is *not* at expected position") + + actual_entity_position = components.TransformBus(bus.Event, "GetWorldTranslation", entity_id) + is_at_position = actual_entity_position.IsClose(expected_entity_position) + Report.result(entity_at_expected_position_result, is_at_position) + + if not is_at_position: + Report.info(f"Entity '{entity_id.ToString()}'\'s expected position: {expected_entity_position.ToString()}, actual position: {actual_entity_position.ToString()}") + + return is_at_position + +def check_entity_children_count(entity_id, expected_children_count): + entity_children_count_matched_result = ( + "Entity with a unique name found", + "Entity with a unique name *not* found") + + entity = EditorEntity(entity_id) + children_entity_ids = entity.get_children_ids() + entity_children_count_matched = len(children_entity_ids) == expected_children_count + Report.result(entity_children_count_matched_result, entity_children_count_matched) + + if not entity_children_count_matched: + Report.info(f"Entity '{entity_id.ToString()}' actual children count: {len(children_entity_ids)}. Expected children count: {expected_children_count}") + + return entity_children_count_matched + +def get_children_ids_by_name(entity_id, entity_name): + entity = EditorEntity(entity_id) + children_entity_ids = entity.get_children_ids() + + result = [] + for child_entity_id in children_entity_ids: + child_entity = EditorEntity(child_entity_id) + child_entity_name = child_entity.get_name() + if child_entity_name == entity_name: + result.append(child_entity_id) + + return result + +def wait_for_propagation(): + general.idle_wait_frames(1) + +def open_base_tests_level(): + helper.init_idle() + helper.open_level("Prefab", "Base") diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Test.prefab b/AutomatedTesting/Gem/PythonTests/prefab/Test.prefab new file mode 100644 index 0000000000..c8d37d7762 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/prefab/Test.prefab @@ -0,0 +1,107 @@ +{ + "ContainerEntity": { + "Id": "ContainerEntity", + "Name": "Test", + "Components": { + "Component_[12826143848076424135]": { + "$type": "EditorDisabledCompositionComponent", + "Id": 12826143848076424135 + }, + "Component_[15101974329990766813]": { + "$type": "EditorVisibilityComponent", + "Id": 15101974329990766813 + }, + "Component_[16680274563691182095]": { + "$type": "EditorOnlyEntityComponent", + "Id": 16680274563691182095 + }, + "Component_[17501195180351523199]": { + "$type": "SelectionComponent", + "Id": 17501195180351523199 + }, + "Component_[17646858836910065148]": { + "$type": "EditorPrefabComponent", + "Id": 17646858836910065148 + }, + "Component_[18334569630592611766]": { + "$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent", + "Id": 18334569630592611766, + "Parent Entity": "" + }, + "Component_[1851406541343621754]": { + "$type": "EditorInspectorComponent", + "Id": 1851406541343621754 + }, + "Component_[25876196591746739]": { + "$type": "EditorEntitySortComponent", + "Id": 25876196591746739 + }, + "Component_[2800966138796329695]": { + "$type": "EditorPendingCompositionComponent", + "Id": 2800966138796329695 + }, + "Component_[8557036193836405255]": { + "$type": "EditorLockComponent", + "Id": 8557036193836405255 + }, + "Component_[9320658693245331333]": { + "$type": "EditorEntityIconComponent", + "Id": 9320658693245331333 + } + } + }, + "Entities": { + "Entity_[965482067476]": { + "Id": "Entity_[965482067476]", + "Name": "Test_Entity", + "Components": { + "Component_[10201286510014079044]": { + "$type": "EditorLockComponent", + "Id": 10201286510014079044 + }, + "Component_[10449365794489552009]": { + "$type": "EditorInspectorComponent", + "Id": 10449365794489552009, + "ComponentOrderEntryArray": [ + { + "ComponentId": 8658441042113324226 + } + ] + }, + "Component_[14918448446211728610]": { + "$type": "EditorOnlyEntityComponent", + "Id": 14918448446211728610 + }, + "Component_[16421196842573146832]": { + "$type": "EditorPendingCompositionComponent", + "Id": 16421196842573146832 + }, + "Component_[18246651644622464292]": { + "$type": "EditorVisibilityComponent", + "Id": 18246651644622464292 + }, + "Component_[2027518491025985743]": { + "$type": "EditorDisabledCompositionComponent", + "Id": 2027518491025985743 + }, + "Component_[4284802475196956760]": { + "$type": "EditorEntityIconComponent", + "Id": 4284802475196956760 + }, + "Component_[5856000098143259126]": { + "$type": "EditorEntitySortComponent", + "Id": 5856000098143259126 + }, + "Component_[7830595068045232876]": { + "$type": "SelectionComponent", + "Id": 7830595068045232876 + }, + "Component_[8658441042113324226]": { + "$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent", + "Id": 8658441042113324226, + "Parent Entity": "ContainerEntity" + } + } + } + } +} \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonTests/prefab/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/prefab/TestSuite_Main.py index 4e3d6ba77f..09df962b0b 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/TestSuite_Main.py +++ b/AutomatedTesting/Gem/PythonTests/prefab/TestSuite_Main.py @@ -13,10 +13,8 @@ import os import sys from ly_test_tools import LAUNCHERS +sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../automatedtesting_shared') -sys.path.append (os.path.dirname (os.path.abspath (__file__)) + '/../automatedtesting_shared') - -import ly_test_tools.environment.file_system as file_system from base import TestAutomationBase @pytest.mark.SUITE_main @@ -24,14 +22,28 @@ from base import TestAutomationBase @pytest.mark.parametrize("project", ["AutomatedTesting"]) class TestAutomation(TestAutomationBase): - def _run_prefab_test(self, request, workspace, editor, test_module): - self._run_test(request, workspace, editor, test_module, ["--regset=/Amazon/Preferences/EnablePrefabSystem=true"]) + def _run_prefab_test(self, request, workspace, editor, test_module, batch_mode=True, autotest_mode=True): + self._run_test(request, workspace, editor, test_module, + extra_cmdline_args=["--regset=/Amazon/Preferences/EnablePrefabSystem=true"], + batch_mode=batch_mode, + autotest_mode=autotest_mode) def test_PrefabLevel_OpensLevelWithEntities(self, request, workspace, editor, launcher_platform): from . import PrefabLevel_OpensLevelWithEntities as test_module self._run_prefab_test(request, workspace, editor, test_module) - def test_PrefabLevel_BasicWorkflow(self, request, workspace, editor, launcher_platform): - from . import PrefabLevel_BasicWorkflow as test_module + def test_Prefab_BasicWorkflow_CreatePrefab(self, request, workspace, editor, launcher_platform): + from . import Prefab_BasicWorkflow_CreatePrefab as test_module + self._run_prefab_test(request, workspace, editor, test_module) + + def test_Prefab_BasicWorkflow_InstantiatePrefab(self, request, workspace, editor, launcher_platform): + from . import Prefab_BasicWorkflow_InstantiatePrefab as test_module self._run_prefab_test(request, workspace, editor, test_module) + def test_Prefab_BasicWorkflow_CreateAndDeletePrefab(self, request, workspace, editor, launcher_platform): + from . import Prefab_BasicWorkflow_CreateAndDeletePrefab as test_module + self._run_prefab_test(request, workspace, editor, test_module) + + def test_Prefab_BasicWorkflow_CreateAndReparentPrefab(self, request, workspace, editor, launcher_platform): + from . import Prefab_BasicWorkflow_CreateAndReparentPrefab as test_module + self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index c412033362..63f042be85 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -61,7 +61,7 @@ namespace AzToolsFramework m_prefabUndoCache.Destroy(); } - PrefabOperationResult PrefabPublicHandler::CreatePrefabInMemory(const EntityIdList& entityIds, AZ::IO::PathView filePath) + CreatePrefabResult PrefabPublicHandler::CreatePrefabInMemory(const EntityIdList& entityIds, AZ::IO::PathView filePath) { EntityList inputEntityList, topLevelEntities; AZ::EntityId commonRootEntityId; @@ -70,9 +70,11 @@ namespace AzToolsFramework entityIds, inputEntityList, topLevelEntities, commonRootEntityId, commonRootEntityOwningInstance); if (!findCommonRootOutcome.IsSuccess()) { - return findCommonRootOutcome; + return AZ::Failure(findCommonRootOutcome.TakeError()); } + AZ::EntityId containerEntityId; + InstanceOptionalReference instanceToCreate; { // Initialize Undo Batch object @@ -92,7 +94,7 @@ namespace AzToolsFramework inputEntityList, commonRootEntityOwningInstance->get(), entities, instances); if (!retrieveEntitiesAndInstancesOutcome.IsSuccess()) { - return retrieveEntitiesAndInstancesOutcome; + return AZ::Failure(retrieveEntitiesAndInstancesOutcome.TakeError()); } AZStd::unordered_map oldEntityAliases; @@ -153,7 +155,7 @@ namespace AzToolsFramework "(A null instance is returned).")); } - AZ::EntityId containerEntityId = instanceToCreate->get().GetContainerEntityId(); + containerEntityId = instanceToCreate->get().GetContainerEntityId(); // Apply the correct transform to the container for the new instance, and store the patch for use when creating the link. PrefabDom patch = ApplyContainerTransformAndGeneratePatch(containerEntityId, commonRootEntityId, topLevelEntities); @@ -261,10 +263,10 @@ namespace AzToolsFramework } } - return AZ::Success(); + return AZ::Success(containerEntityId); } - PrefabOperationResult PrefabPublicHandler::CreatePrefabInDisk(const EntityIdList& entityIds, AZ::IO::PathView filePath) + CreatePrefabResult PrefabPublicHandler::CreatePrefabInDisk(const EntityIdList& entityIds, AZ::IO::PathView filePath) { auto result = CreatePrefabInMemory(entityIds, filePath); if (result.IsSuccess()) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h index 6faaa6932f..42b6bc1666 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h @@ -42,11 +42,12 @@ namespace AzToolsFramework void UnregisterPrefabPublicHandlerInterface(); // PrefabPublicInterface... - PrefabOperationResult CreatePrefabInDisk( + CreatePrefabResult CreatePrefabInDisk( const EntityIdList& entityIds, AZ::IO::PathView filePath) override; - PrefabOperationResult CreatePrefabInMemory( + CreatePrefabResult CreatePrefabInMemory( const EntityIdList& entityIds, AZ::IO::PathView filePath) override; - InstantiatePrefabResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) override; + InstantiatePrefabResult InstantiatePrefab( + AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) override; PrefabOperationResult SavePrefab(AZ::IO::Path filePath) override; PrefabEntityResult CreateEntity(AZ::EntityId parentId, const AZ::Vector3& position) override; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h index 83763ef55f..528d4f6d1b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h @@ -24,8 +24,9 @@ namespace AzToolsFramework namespace Prefab { - typedef AZ::Outcome PrefabOperationResult; + typedef AZ::Outcome CreatePrefabResult; typedef AZ::Outcome InstantiatePrefabResult; + typedef AZ::Outcome PrefabOperationResult; typedef AZ::Outcome PrefabRequestResult; typedef AZ::Outcome PrefabEntityResult; @@ -44,9 +45,10 @@ namespace AzToolsFramework * Automatically detects descendants of entities, and discerns between entities and child instances. * @param entityIds The entities that should form the new prefab (along with their descendants). * @param filePath The absolute path for the new prefab file. - * @return An outcome object; on failure, it comes with an error message detailing the cause of the error. + * @return An outcome object with an entityId of the new prefab's container entity; + * on failure, it comes with an error message detailing the cause of the error. */ - virtual PrefabOperationResult CreatePrefabInDisk( + virtual CreatePrefabResult CreatePrefabInDisk( const EntityIdList& entityIds, AZ::IO::PathView filePath) = 0; /** @@ -54,9 +56,10 @@ namespace AzToolsFramework * Automatically detects descendants of entities, and discerns between entities and child instances. * @param entityIds The entities that should form the new prefab (along with their descendants). * @param filePath The absolute path for the new prefab file. - * @return An outcome object; on failure, it comes with an error message detailing the cause of the error. + * @return An outcome object with an entityId of the new prefab's container entity; + * on failure, it comes with an error message detailing the cause of the error. */ - virtual PrefabOperationResult CreatePrefabInMemory( + virtual CreatePrefabResult CreatePrefabInMemory( const EntityIdList& entityIds, AZ::IO::PathView filePath) = 0; /** diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h index 1605ad97be..7b23fffb7f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -22,8 +23,9 @@ namespace AzToolsFramework namespace Prefab { - using PrefabOperationResult = AZ::Outcome; + using CreatePrefabResult = AZ::Outcome; using InstantiatePrefabResult = AZ::Outcome; + using PrefabOperationResult = AZ::Outcome; /** * The primary purpose of this bus is to facilitate writing automated tests for prefabs. @@ -47,14 +49,16 @@ namespace AzToolsFramework /** * Create a prefab out of the entities provided, at the path provided, and keep it in memory. * Automatically detects descendants of entities, and discerns between entities and child instances. - * Return whether the creation succeeded or not. + * Return an outcome object with an container entity id of the prefab created if creation succeeded; + * on failure, it comes with an error message detailing the cause of the error. */ - virtual PrefabOperationResult CreatePrefabInMemory( + virtual CreatePrefabResult CreatePrefabInMemory( const EntityIdList& entityIds, AZStd::string_view filePath) = 0; /** * Instantiate a prefab from a prefab file. - * Return the container entity id of the prefab instantiated if instantiation succeeded. + * Return an outcome object with an container entity id of the prefab instantiated if instantiation succeeded; + * on failure, it comes with an error message detailing the cause of the error. */ virtual InstantiatePrefabResult InstantiatePrefab( AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) = 0; @@ -62,10 +66,9 @@ namespace AzToolsFramework /** * Deletes all entities and their descendants from the owning instance. Bails if the entities don't * all belong to the same instance. - * Return whether the deletion succeeded or not. + * Return an outcome object; on failure, it comes with an error message detailing the cause of the error. */ virtual PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) = 0; - }; using PrefabPublicRequestBus = AZ::EBus; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp index d964be25af..ad171dc6f4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp @@ -45,7 +45,7 @@ namespace AzToolsFramework m_prefabPublicInterface = nullptr; } - PrefabOperationResult PrefabPublicRequestHandler::CreatePrefabInMemory(const EntityIdList& entityIds, AZStd::string_view filePath) + CreatePrefabResult PrefabPublicRequestHandler::CreatePrefabInMemory(const EntityIdList& entityIds, AZStd::string_view filePath) { return m_prefabPublicInterface->CreatePrefabInMemory(entityIds, filePath); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h index 87608e1263..ae0ed2a5d1 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h @@ -31,7 +31,7 @@ namespace AzToolsFramework void Connect(); void Disconnect(); - PrefabOperationResult CreatePrefabInMemory(const EntityIdList& entityIds, AZStd::string_view filePath) override; + CreatePrefabResult CreatePrefabInMemory(const EntityIdList& entityIds, AZStd::string_view filePath) override; InstantiatePrefabResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) override; PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override;