Add prefab reparenting python auto tests (#3653)

* Add prefab reparenting python auto tests

Signed-off-by: chiyteng <chiyteng@amazon.com>

* modify prefab python auto test framework

Signed-off-by: chiyteng <chiyteng@amazon.com>

* remove extra spaces

Signed-off-by: chiyteng <chiyteng@amazon.com>

* delete unused files

Signed-off-by: chiyteng <chiyteng@amazon.com>

* delete unused files

Signed-off-by: chiyteng <chiyteng@amazon.com>

* fix nits

Signed-off-by: chiyteng <chiyteng@amazon.com>

* Refactor prefab python tests

Signed-off-by: chiyteng <chiyteng@amazon.com>

* Fix nits

Signed-off-by: chiyteng <chiyteng@amazon.com>

* Modify comments

Signed-off-by: chiyteng <chiyteng@amazon.com>

* Fix nits and add comments for Prefab.py

Signed-off-by: chiyteng <chiyteng@amazon.com>
This commit is contained in:
chiyenteng
2021-09-03 12:06:22 -07:00
committed by GitHub
parent 64a561a948
commit f7d4d80e5b
16 changed files with 627 additions and 137 deletions
@@ -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.
@@ -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
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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")
@@ -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"
}
}
}
}
}
@@ -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)
@@ -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<AZ::EntityId, AZStd::string> 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())
@@ -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;
@@ -24,8 +24,9 @@ namespace AzToolsFramework
namespace Prefab
{
typedef AZ::Outcome<void, AZStd::string> PrefabOperationResult;
typedef AZ::Outcome<AZ::EntityId, AZStd::string> CreatePrefabResult;
typedef AZ::Outcome<AZ::EntityId, AZStd::string> InstantiatePrefabResult;
typedef AZ::Outcome<void, AZStd::string> PrefabOperationResult;
typedef AZ::Outcome<bool, AZStd::string> PrefabRequestResult;
typedef AZ::Outcome<AZ::EntityId, AZStd::string> 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;
/**
@@ -10,6 +10,7 @@
#include <AzCore/Component/EntityId.h>
#include <AzCore/EBus/EBus.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Math/Vector3.h>
#include <AzCore/Outcome/Outcome.h>
#include <AzCore/std/containers/vector.h>
@@ -22,8 +23,9 @@ namespace AzToolsFramework
namespace Prefab
{
using PrefabOperationResult = AZ::Outcome<void, AZStd::string>;
using CreatePrefabResult = AZ::Outcome<AZ::EntityId, AZStd::string>;
using InstantiatePrefabResult = AZ::Outcome<AZ::EntityId, AZStd::string>;
using PrefabOperationResult = AZ::Outcome<void, AZStd::string>;
/**
* 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<PrefabPublicRequests>;
@@ -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);
}
@@ -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;