Merge branch 'development' into Atom/santorac/ShaderVariantListSizeLimit

This commit is contained in:
santorac
2022-01-12 11:24:15 -08:00
3026 changed files with 190790 additions and 145839 deletions
+1 -8
View File
@@ -2,18 +2,14 @@
.vs/
.vscode/
__pycache__
AssetProcessorTemp/**
[Bb]uild/
[Oo]ut/**
CMakeUserPresets.json
[Cc]ache/
/[Ii]nstall/
Editor/EditorEventLog.xml
Editor/EditorLayout.xml
**/*egg-info/**
**/*egg-link
**/[Rr]estricted
UserSettings.xml
[Uu]ser/
FrameCapture/**
.DS_Store
@@ -22,9 +18,6 @@ client*.cfg
server*.cfg
.mayaSwatches/
_savebackup/
#Output folder for test results when running Automated Tests
TestResults/**
*.swatches
/imgui.ini
/scripts/project_manager/logs/
/AutomatedTesting/Gem/PythonTests/scripting/TestResults
@@ -0,0 +1,131 @@
{
"ContainerEntity": {
"Id": "ContainerEntity",
"Name": "PinkFlower",
"Components": {
"Component_[10444337162843472597]": {
"$type": "EditorLockComponent",
"Id": 10444337162843472597
},
"Component_[14431042590323756177]": {
"$type": "EditorEntitySortComponent",
"Id": 14431042590323756177,
"ChildEntityOrderEntryArray": [
{
"EntityId": "Entity_[491002114939]"
}
]
},
"Component_[14577735453176806353]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 14577735453176806353
},
"Component_[15674021346798629563]": {
"$type": "EditorInspectorComponent",
"Id": 15674021346798629563
},
"Component_[16784074985702513600]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 16784074985702513600,
"Parent Entity": ""
},
"Component_[3351614541100572773]": {
"$type": "EditorOnlyEntityComponent",
"Id": 3351614541100572773
},
"Component_[3600658560328167663]": {
"$type": "EditorPrefabComponent",
"Id": 3600658560328167663
},
"Component_[6155673728651558934]": {
"$type": "EditorEntityIconComponent",
"Id": 6155673728651558934
},
"Component_[8458684662170321289]": {
"$type": "EditorVisibilityComponent",
"Id": 8458684662170321289
},
"Component_[8705192117416252351]": {
"$type": "SelectionComponent",
"Id": 8705192117416252351
},
"Component_[8801280051488695852]": {
"$type": "EditorPendingCompositionComponent",
"Id": 8801280051488695852
}
}
},
"Entities": {
"Entity_[491002114939]": {
"Id": "Entity_[491002114939]",
"Name": "PinkFlower",
"Components": {
"Component_[11083205340162142682]": {
"$type": "EditorLockComponent",
"Id": 11083205340162142682
},
"Component_[11327363779873517]": {
"$type": "EditorOnlyEntityComponent",
"Id": 11327363779873517
},
"Component_[12717389211269537921]": {
"$type": "EditorEntitySortComponent",
"Id": 12717389211269537921
},
"Component_[12826285685970138542]": {
"$type": "AZ::Render::EditorMeshComponent",
"Id": 12826285685970138542,
"Controller": {
"Configuration": {
"ModelAsset": {
"assetId": {
"guid": "{549F4C4D-A7D9-5F2A-A6BD-F24C8BC43BBF}",
"subId": 280086017
},
"assetHint": "assets/objects/foliage/grass_flower_pink.azmodel"
}
}
}
},
"Component_[14101419970865342875]": {
"$type": "EditorPendingCompositionComponent",
"Id": 14101419970865342875
},
"Component_[14770464075286403207]": {
"$type": "EditorVisibilityComponent",
"Id": 14770464075286403207
},
"Component_[15205142653091190082]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 15205142653091190082,
"Parent Entity": "ContainerEntity"
},
"Component_[15510898811147803772]": {
"$type": "EditorInspectorComponent",
"Id": 15510898811147803772,
"ComponentOrderEntryArray": [
{
"ComponentId": 15205142653091190082
},
{
"ComponentId": 12826285685970138542,
"SortIndex": 1
}
]
},
"Component_[15988401742428977134]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 15988401742428977134
},
"Component_[4300550837037679336]": {
"$type": "EditorEntityIconComponent",
"Id": 4300550837037679336
},
"Component_[996914988793716659]": {
"$type": "SelectionComponent",
"Id": 996914988793716659
}
}
}
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4c1d04d687bdce69965965c290c907b3f9a730a7ea73874b734e1c7ff41426d9
size 3832768
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:27f87510ae07771dbad3e430e31502b1d1d13dee868f143b7f1de2a7febc8eb9
size 7046400
@@ -0,0 +1,8 @@
{
"values": [
{
"$type": "ScriptProcessorRule",
"scriptFilename": "Assets/TestAnim/scene_export_actor.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
#
#
import traceback, sys, uuid, os, json
#
# Example for exporting ActorGroup scene rules
#
def log_exception_traceback():
exc_type, exc_value, exc_tb = sys.exc_info()
data = traceback.format_exception(exc_type, exc_value, exc_tb)
print(str(data))
def get_node_names(sceneGraph, nodeTypeName, testEndPoint = False, validList = None):
import azlmbr.scene.graph
import scene_api.scene_data
node = sceneGraph.get_root()
nodeList = []
children = []
paths = []
while node.IsValid():
# store children to process after siblings
if sceneGraph.has_node_child(node):
children.append(sceneGraph.get_node_child(node))
nodeName = scene_api.scene_data.SceneGraphName(sceneGraph.get_node_name(node))
paths.append(nodeName.get_path())
include = True
if (validList is not None):
include = False # if a valid list filter provided, assume to not include node name
name_parts = nodeName.get_path().split('.')
for valid in validList:
if (valid in name_parts[-1]):
include = True
break
# store any node that has provides specifc data content
nodeContent = sceneGraph.get_node_content(node)
if include and nodeContent.CastWithTypeName(nodeTypeName):
if testEndPoint is not None:
include = sceneGraph.is_node_end_point(node) is testEndPoint
if include:
if (len(nodeName.get_path())):
nodeList.append(scene_api.scene_data.SceneGraphName(sceneGraph.get_node_name(node)))
# advance to next node
if sceneGraph.has_node_sibling(node):
node = sceneGraph.get_node_sibling(node)
elif children:
node = children.pop()
else:
node = azlmbr.scene.graph.NodeIndex()
return nodeList, paths
def generate_mesh_group(scene, sceneManifest, meshDataList, paths):
# Compute the name of the scene file
clean_filename = scene.sourceFilename.replace('.', '_')
mesh_group_name = os.path.basename(clean_filename)
# make the mesh group
mesh_group = sceneManifest.add_mesh_group(mesh_group_name)
mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, clean_filename)) + '}'
# add all nodes to this mesh group
for activeMeshIndex in range(len(meshDataList)):
mesh_name = meshDataList[activeMeshIndex]
mesh_path = mesh_name.get_path()
sceneManifest.mesh_group_select_node(mesh_group, mesh_path)
def create_shape_configuration(nodeName):
import scene_api.physics_data
if(nodeName in ['_foot_','_wrist_']):
shapeConfiguration = scene_api.physics_data.BoxShapeConfiguration()
shapeConfiguration.scale = [1.1, 1.1, 1.1]
shapeConfiguration.dimensions = [2.1, 3.1, 4.1]
return shapeConfiguration
else:
shapeConfiguration = scene_api.physics_data.CapsuleShapeConfiguration()
shapeConfiguration.scale = [1.0, 1.0, 1.0]
shapeConfiguration.height = 1.0
shapeConfiguration.radius = 1.0
return shapeConfiguration
def create_collider_configuration(nodeName):
import scene_api.physics_data
colliderConfiguration = scene_api.physics_data.ColliderConfiguration()
colliderConfiguration.Position = [0.1, 0.1, 0.2]
colliderConfiguration.Rotation = [45.0, 35.0, 25.0]
return colliderConfiguration
def generate_physics_nodes(actorPhysicsSetupRule, nodeNameList):
import scene_api.physics_data
hitDetectionConfig = scene_api.physics_data.CharacterColliderConfiguration()
simulatedObjectColliderConfig = scene_api.physics_data.CharacterColliderConfiguration()
clothConfig = scene_api.physics_data.CharacterColliderConfiguration()
ragdollConfig = scene_api.physics_data.RagdollConfiguration()
for nodeName in nodeNameList:
shapeConfiguration = create_shape_configuration(nodeName)
colliderConfiguration = create_collider_configuration(nodeName)
hitDetectionConfig.add_character_collider_node_configuration_node(nodeName, colliderConfiguration, shapeConfiguration)
simulatedObjectColliderConfig.add_character_collider_node_configuration_node(nodeName, colliderConfiguration, shapeConfiguration)
clothConfig.add_character_collider_node_configuration_node(nodeName, colliderConfiguration, shapeConfiguration)
#
ragdollNode = scene_api.physics_data.RagdollNodeConfiguration()
ragdollNode.JointConfig.Name = nodeName
ragdollConfig.add_ragdoll_node_configuration(ragdollNode)
ragdollConfig.colliders.add_character_collider_node_configuration_node(nodeName, colliderConfiguration, shapeConfiguration)
actorPhysicsSetupRule.set_simulated_object_collider_config(simulatedObjectColliderConfig)
actorPhysicsSetupRule.set_hit_detection_config(hitDetectionConfig)
actorPhysicsSetupRule.set_cloth_config(clothConfig)
actorPhysicsSetupRule.set_ragdoll_config(ragdollConfig)
def generate_actor_group(scene, sceneManifest, meshDataList, paths):
import scene_api.scene_data
import scene_api.physics_data
import scene_api.actor_group
# fetch bone data
validNames = ['_neck_','_pelvis_','_leg_','_knee_','_spine_','_arm_','_clavicle_','_head_','_elbow_','_wrist_']
graph = scene_api.scene_data.SceneGraph(scene.graph)
nodeList, allNodePaths = get_node_names(graph, 'BoneData', validList = validNames)
nodeNameList = []
for activeMeshIndex, nodeName in enumerate(nodeList):
nodeNameList.append(nodeName.get_name())
# add comment
commentRule = scene_api.actor_group.CommentRule()
commentRule.text = str(nodeNameList)
# ActorPhysicsSetupRule
actorPhysicsSetupRule = scene_api.actor_group.ActorPhysicsSetupRule()
generate_physics_nodes(actorPhysicsSetupRule, nodeNameList)
# add scale of the Actor rule
actorScaleRule = scene_api.actor_group.ActorScaleRule()
actorScaleRule.scaleFactor = 2.0
# add coordinate system rule
coordinateSystemRule = scene_api.actor_group.CoordinateSystemRule()
coordinateSystemRule.useAdvancedData = False
# add morph target rule
morphTargetRule = scene_api.actor_group.MorphTargetRule()
morphTargetRule.targets.select_targets([nodeNameList[0]], nodeNameList)
# add skeleton optimization rule
skeletonOptimizationRule = scene_api.actor_group.SkeletonOptimizationRule()
skeletonOptimizationRule.autoSkeletonLOD = True
skeletonOptimizationRule.criticalBonesList.select_targets([nodeNameList[0:2]], nodeNameList)
# add LOD rule
lodRule = scene_api.actor_group.LodRule()
lodRule0 = lodRule.add_lod_level(0)
lodRule0.select_targets([nodeNameList[1:4]], nodeNameList)
actorGroup = scene_api.actor_group.ActorGroup()
actorGroup.name = os.path.basename(scene.sourceFilename)
actorGroup.add_rule(actorScaleRule)
actorGroup.add_rule(coordinateSystemRule)
actorGroup.add_rule(skeletonOptimizationRule)
actorGroup.add_rule(morphTargetRule)
actorGroup.add_rule(lodRule)
actorGroup.add_rule(actorPhysicsSetupRule)
actorGroup.add_rule(commentRule)
sceneManifest.manifest['values'].append(actorGroup.to_dict())
def update_manifest(scene):
import json, uuid, os
import azlmbr.scene.graph
import scene_api.scene_data
graph = scene_api.scene_data.SceneGraph(scene.graph)
mesh_name_list, all_node_paths = get_node_names(graph, 'MeshData')
scene_manifest = scene_api.scene_data.SceneManifest()
generate_actor_group(scene, scene_manifest, mesh_name_list, all_node_paths)
generate_mesh_group(scene, scene_manifest, mesh_name_list, all_node_paths)
# Convert the manifest to a JSON string and return it
return scene_manifest.export()
sceneJobHandler = None
def on_update_manifest(args):
try:
scene = args[0]
return update_manifest(scene)
except RuntimeError as err:
print (f'ERROR - {err}')
log_exception_traceback()
except:
log_exception_traceback()
global sceneJobHandler
sceneJobHandler.disconnect()
sceneJobHandler = None
# try to create SceneAPI handler for processing
try:
import azlmbr.scene
sceneJobHandler = azlmbr.scene.ScriptBuildingNotificationBusHandler()
sceneJobHandler.connect()
sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
except:
sceneJobHandler = None
@@ -0,0 +1,66 @@
#
# 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 traceback, sys, uuid, os, json
import scene_export_utils
import scene_api.motion_group
#
# Example for exporting MotionGroup scene rules
#
def update_manifest(scene):
import azlmbr.scene.graph
import scene_api.scene_data
# create a SceneManifest
sceneManifest = scene_api.scene_data.SceneManifest()
# create a MotionGroup
motionGroup = scene_api.motion_group.MotionGroup()
motionGroup.name = os.path.basename(scene.sourceFilename.replace('.', '_'))
motionAdditiveRule = scene_api.motion_group.MotionAdditiveRule()
motionAdditiveRule.sampleFrame = 2
motionGroup.add_rule(motionAdditiveRule)
motionScaleRule = motionGroup.create_rule(scene_api.motion_group.MotionScaleRule())
motionScaleRule.scaleFactor = 1.1
motionGroup.add_rule(motionScaleRule)
# add motion group to scene manifest
sceneManifest.add_motion_group(motionGroup)
# Convert the manifest to a JSON string and return it
return sceneManifest.export()
sceneJobHandler = None
def on_update_manifest(args):
try:
scene = args[0]
return update_manifest(scene)
except RuntimeError as err:
print (f'ERROR - {err}')
scene_export_utils.log_exception_traceback()
except:
scene_export_utils.log_exception_traceback()
global sceneJobHandler
sceneJobHandler.disconnect()
sceneJobHandler = None
# try to create SceneAPI handler for processing
try:
import azlmbr.scene
sceneJobHandler = azlmbr.scene.ScriptBuildingNotificationBusHandler()
sceneJobHandler.connect()
sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
except:
sceneJobHandler = None
@@ -0,0 +1,8 @@
{
"values": [
{
"$type": "ScriptProcessorRule",
"scriptFilename": "Assets/TestAnim/scene_export_motion.py"
}
]
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:011454252e40c927343cce16296412f02f45d1f345c75c036651bdcca473bda5
size 2672
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7bafcc4aefab827e1414e64bcdde235500b51392e52c9ccd588b2d7a24b865a0
size 20214
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ac841c02e81c9ae23476522b7e517889d746d4ff32b3251ac4360168f39b30a3
size 450708
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3653ca7fb42c7e5991815c17fc9ebeab14a1aa861bfb1d37e233ada66a835f00
size 7188
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a30b85428202c548e77cdcc0a595f9f26e82d29622be26891d569f4779a75830
size 1802388
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3d76b7c93f8873ca7c4013dcc4eff887c25552c9c5847290481306656b22069
size 3668
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:219f57137c5bd44093762ea9d0fc24308679956b980f26bf194edd3a1798fcda
size 3668
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96ed61c66d1d1f71e940ab75899ee253007e704004e1157c900c6308151a8bf1
size 1802388
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:70a7dc4e2455624067e842b059954f6c4bb9b0debc3cb1ffa783b14bffc61786
size 7188
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7f5a945c61f92f3da77dfd9fdd2c95aa257f4df6bcb82483c608ab660bb72e5f
size 450708
@@ -0,0 +1,63 @@
#
# 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 traceback, sys, uuid, os, json
#
# Utility methods for processing scenes
#
def log_exception_traceback():
exc_type, exc_value, exc_tb = sys.exc_info()
data = traceback.format_exception(exc_type, exc_value, exc_tb)
print(str(data))
def get_node_names(sceneGraph, nodeTypeName, testEndPoint = False, validList = None):
import azlmbr.scene.graph
import scene_api.scene_data
node = sceneGraph.get_root()
nodeList = []
children = []
paths = []
while node.IsValid():
# store children to process after siblings
if sceneGraph.has_node_child(node):
children.append(sceneGraph.get_node_child(node))
nodeName = scene_api.scene_data.SceneGraphName(sceneGraph.get_node_name(node))
paths.append(nodeName.get_path())
include = True
if (validList is not None):
include = False # if a valid list filter provided, assume to not include node name
name_parts = nodeName.get_path().split('.')
for valid in validList:
if (valid in name_parts[-1]):
include = True
break
# store any node that has provides specifc data content
nodeContent = sceneGraph.get_node_content(node)
if include and nodeContent.CastWithTypeName(nodeTypeName):
if testEndPoint is not None:
include = sceneGraph.is_node_end_point(node) is testEndPoint
if include:
if (len(nodeName.get_path())):
nodeList.append(scene_api.scene_data.SceneGraphName(sceneGraph.get_node_name(node)))
# advance to next node
if sceneGraph.has_node_sibling(node):
node = sceneGraph.get_node_sibling(node)
elif children:
node = children.pop()
else:
node = azlmbr.scene.graph.NodeIndex()
return nodeList, paths
@@ -1,10 +1,10 @@
"""
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
"""
#
# 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 traceback, logging, json
from typing import Tuple, List
@@ -14,30 +14,44 @@ from scene_api.scene_data import SceneGraphName
def log_exception_traceback():
"""
Outputs an exception stacktrace.
"""
"""Outputs an exception stacktrace."""
data = traceback.format_exc()
logger = logging.getLogger('python')
logger.error(data)
def sanitize_name_for_disk(name: str):
"""
Removes illegal filename characters from a string.
def sanitize_name_for_disk(name: str) -> str:
"""Removes illegal filename characters from a string.
Parameters
----------
name :
String to clean.
Returns
-------
str
Name with illegal characters removed.
:param name: String to clean.
:return: Name with illegal characters removed.
"""
return "".join(char for char in name if char not in "|<>:\"/?*\\")
def get_mesh_node_names(scene_graph: sceneData.SceneGraph) -> Tuple[List[SceneGraphName], List[str]]:
"""
Returns a tuple of all the mesh nodes as well as all the node paths
"""Returns a tuple of all the mesh nodes as well as all the node paths
Parameters
----------
scene_graph :
Scene graph to search
Returns
-------
Tuple[List[SceneGraphName], List[str]]
Tuple of [Mesh Nodes, All Node Paths]
:param scene_graph: Scene graph to search
:return: Tuple of [Mesh Nodes, All Node Paths]
"""
import azlmbr.scene as sceneApi
import azlmbr.scene.graph
@@ -8,6 +8,7 @@
import azlmbr.bus
import azlmbr.math
from scene_api.scene_data import PrimitiveShape, DecompositionMode, ColorChannel, TangentSpaceSource, TangentSpaceMethod
from scene_helpers import *
@@ -41,6 +42,36 @@ def add_material_component(entity_id):
raise RuntimeError("UpdateComponentForEntity for editor_material_component failed")
def add_physx_meshes(scene_manifest: sceneData.SceneManifest, source_file_name: str, mesh_name_list: List, all_node_paths: List[str]):
first_mesh = mesh_name_list[0].get_path()
# Add a Box Primitive PhysX mesh with a comment
physx_box = scene_manifest.add_physx_primitive_mesh_group(source_file_name + "_box", PrimitiveShape.BOX, 0.0, None)
scene_manifest.physx_mesh_group_add_comment(physx_box, "This is a box primitive")
# Select the first mesh, unselect every other node
scene_manifest.physx_mesh_group_add_selected_node(physx_box, first_mesh)
for node in all_node_paths:
if node != first_mesh:
scene_manifest.physx_mesh_group_add_unselected_node(physx_box, node)
# Add a Convex Mesh PhysX mesh with a comment
convex_mesh = scene_manifest.add_physx_convex_mesh_group(source_file_name + "_convex", 0.08, .0004,
True, True, True, True, True, 24, True, "Glass")
scene_manifest.physx_mesh_group_add_comment(convex_mesh, "This is a convex mesh")
# Select/Unselect nodes using lists
all_except_first_mesh = [x for x in all_node_paths if x != first_mesh]
scene_manifest.physx_mesh_group_add_selected_unselected_nodes(convex_mesh, [first_mesh], all_except_first_mesh)
# Configure mesh decomposition for this mesh
scene_manifest.physx_mesh_group_decompose_meshes(convex_mesh, 512, 32, .002, 100100, DecompositionMode.TETRAHEDRON,
0.06, 0.055, 0.00015, 3, 3, True, False)
# Add a Triangle mesh
triangle = scene_manifest.add_physx_triangle_mesh_group(source_file_name + "_triangle", False, True, True, True, True, True)
scene_manifest.physx_mesh_group_add_selected_unselected_nodes(triangle, [first_mesh], all_except_first_mesh)
def update_manifest(scene):
import uuid, os
import azlmbr.scene.graph
@@ -62,6 +93,8 @@ def update_manifest(scene):
previous_entity_id = azlmbr.entity.InvalidEntityId
first_mesh = True
add_physx_meshes(scene_manifest, source_filename_only, mesh_name_list, all_node_paths)
# Loop every mesh node in the scene
for activeMeshIndex in range(len(mesh_name_list)):
mesh_name = mesh_name_list[activeMeshIndex]
@@ -82,10 +115,11 @@ def update_manifest(scene):
if node != mesh_path:
scene_manifest.mesh_group_unselect_node(mesh_group, node)
scene_manifest.mesh_group_add_cloth_rule(mesh_group, mesh_path, "Col0", 1, "Col0", 2, "Col0", 2, 3)
scene_manifest.mesh_group_add_cloth_rule(mesh_group, mesh_path, "Col0", ColorChannel.GREEN, "Col0",
ColorChannel.BLUE, "Col0", ColorChannel.BLUE, ColorChannel.ALPHA)
scene_manifest.mesh_group_add_advanced_mesh_rule(mesh_group, True, False, True, "Col0")
scene_manifest.mesh_group_add_skin_rule(mesh_group, 3, 0.002)
scene_manifest.mesh_group_add_tangent_rule(mesh_group, 1, 0)
scene_manifest.mesh_group_add_tangent_rule(mesh_group, TangentSpaceSource.MIKKT_GENERATION, TangentSpaceMethod.TSPACE_BASIC)
# Create an editor entity
entity_id = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "CreateEditorReadyEntity", mesh_group_name)
@@ -106,6 +140,26 @@ def update_manifest(scene):
if not result:
raise RuntimeError("UpdateComponentForEntity failed for Mesh component")
# Add a physics component referencing the triangle mesh we made for the first node
if previous_entity_id is None:
physx_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName",
entity_id, "{FD429282-A075-4966-857F-D0BBF186CFE6} EditorColliderComponent")
json_update = json.dumps({
"ShapeConfiguration": {
"PhysicsAsset": {
"Asset": {
"assetHint": os.path.join(source_relative_path, source_filename_only + "_triangle.pxmesh")
}
}
}
})
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, physx_mesh_component, json_update)
if not result:
raise RuntimeError("UpdateComponentForEntity failed for PhysX mesh component")
# an example of adding a material component to override the default material
if previous_entity_id is not None and first_mesh:
first_mesh = False
@@ -19,8 +19,8 @@
<RemoteProcedure Name="AutonomousToAuthorityNoParams" InvokeFrom="Autonomous" HandleOn="Authority" IsPublic="false" IsReliable="false" GenerateEventBindings="true" Description="" />
<RemoteProcedure Name="AuthorityToAutonomous" InvokeFrom="Authority" HandleOn="Autonomous" IsPublic="false" IsReliable="false" GenerateEventBindings="true" Description="" >
<Param Type="float" Name="SomeFloat" />
<RemoteProcedure Name="AuthorityToAutonomous_PlayerNumber" InvokeFrom="Authority" HandleOn="Autonomous" IsPublic="false" IsReliable="true" GenerateEventBindings="true" Description="" >
<Param Type="int" Name="player_number" />
</RemoteProcedure>
<RemoteProcedure Name="AuthorityToAutonomousNoParams" InvokeFrom="Authority" HandleOn="Autonomous" IsPublic="false" IsReliable="false" GenerateEventBindings="true" Description="" />
@@ -29,10 +29,10 @@
<Param Type="float" Name="SomeFloat" />
</RemoteProcedure>
<RemoteProcedure Name="AuthorityToClientNoParams" InvokeFrom="Authority" HandleOn="Client" IsPublic="false" IsReliable="false" GenerateEventBindings="true" Description="" />
<RemoteProcedure Name="AuthorityToClientNoParams_PlayFx" InvokeFrom="Authority" HandleOn="Client" IsPublic="false" IsReliable="true" GenerateEventBindings="true" Description="" />
<RemoteProcedure Name="ServerToAuthority" InvokeFrom="Server" HandleOn="Authority" IsPublic="false" IsReliable="false" GenerateEventBindings="true" Description="" >
<Param Type="float" Name="SomeFloat" />
<RemoteProcedure Name="ServerToAuthority_DealDamage" InvokeFrom="Server" HandleOn="Authority" IsPublic="false" IsReliable="true" GenerateEventBindings="true" Description="" >
<Param Type="float" Name="damage" />
</RemoteProcedure>
<RemoteProcedure Name="ServerToAuthorityNoParam" InvokeFrom="Server" HandleOn="Authority" IsPublic="false" IsReliable="false" GenerateEventBindings="true" Description="" />
@@ -13,7 +13,8 @@
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS)
# Only enable AWS automated tests on Windows
if(NOT "${PAL_PLATFORM_NAME}" STREQUAL "Windows")
set(SUPPORTED_PLATFORMS "Windows" "Linux")
if (NOT "${PAL_PLATFORM_NAME}" IN_LIST SUPPORTED_PLATFORMS)
return()
endif()
@@ -21,9 +22,8 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS)
NAME AutomatedTesting::AWSTests
TEST_SUITE awsi
TEST_SERIAL
PATH ${CMAKE_CURRENT_LIST_DIR}/${PAL_PLATFORM_NAME}/
PATH ${CMAKE_CURRENT_LIST_DIR}/
RUNTIME_DEPENDENCIES
Legacy::Editor
AZ::AssetProcessor
AutomatedTesting.GameLauncher
AutomatedTesting.Assets
+47 -16
View File
@@ -2,30 +2,61 @@
## Prerequisites
1. Build the O3DE Editor and AutomatedTesting.GameLauncher in Profile.
2. AWS CLI is installed and configured following [Configuration and Credential File Settings](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html).
3. [AWS Cloud Development Kit (CDK)](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html#getting_started_install) is installed.
2. Install the latest version of NodeJs.
3. AWS CLI is installed and configured following [Configuration and Credential File Settings](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html).
4. [AWS Cloud Development Kit (CDK)](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html#getting_started_install) is installed.
## Deploy CDK Applications
1. Go to the AWS IAM console and create an IAM role called o3de-automation-tests which adds your own account as as a trusted entity and uses the "AdministratorAccess" permissions policy.
2. Copy {engine_root}\scripts\build\Platform\Windows\deploy_cdk_applications.cmd to your engine root folder.
3. Open a new Command Prompt window at the engine root and set the following environment variables:
```
Set O3DE_AWS_PROJECT_NAME=AWSAUTO
Set O3DE_AWS_DEPLOY_REGION=us-east-1
Set O3DE_AWS_DEPLOY_ACCOUNT={your_aws_account_id}
Set ASSUME_ROLE_ARN=arn:aws:iam::{your_aws_account_id}:role/o3de-automation-tests
Set COMMIT_ID=HEAD
```
4. In the same Command Prompt window, Deploy the CDK applications for AWS gems by running deploy_cdk_applications.cmd.
2. Copy the following deployment script to your engine root folder:
* Windows (Command Prompt)
```
{engine_root}\scripts\build\Platform\Windows\deploy_cdk_applications.cmd
```
* Linux
```
{engine_root}/scripts/build/Platform/Linux/deploy_cdk_applications.sh
```
3. Open a new CLI window at the engine root and set the following environment variables:
* Windows
```
Set O3DE_AWS_PROJECT_NAME=AWSAUTO
Set O3DE_AWS_DEPLOY_REGION=us-east-1
Set ASSUME_ROLE_ARN=arn:aws:iam::{your_aws_account_id}:role/o3de-automation-tests
Set COMMIT_ID=HEAD
```
* Linux
```
export O3DE_AWS_PROJECT_NAME=AWSAUTO
export O3DE_AWS_DEPLOY_REGION=us-east-1
export ASSUME_ROLE_ARN=arn:aws:iam::{your_aws_account_id}:role/o3de-automation-tests
export COMMIT_ID=HEAD
```
4. In the same CLI window, Deploy the CDK applications for AWS gems by running deploy_cdk_applications.cmd.
## Run Automation Tests
### CLI
In the same Command Prompt window, run the following CLI command:
python\python.cmd -m pytest {path_to_the_test_file} --build-directory {directory_to_the_profile_build}
1. In the same CLI window, run the following CLI command:
* Windows
```
python\python.cmd -m pytest {path_to_the_test_file} --build-directory {directory_to_the_profile_build}
```
* Linux
```
python/python.sh -m pytest {path_to_the_test_file} --build-directory {directory_to_the_profile_build}
```
### Pycharm
You can also run any specific automation test directly from Pycharm by providing the "--build-directory" argument in the Run Configuration.
## Destroy CDK Applications
1. Copy {engine_root}\scripts\build\Platform\Windows\destroy_cdk_applications.cmd to your engine root folder.
2. In the same Command Prompt window, destroy the CDK applications for AWS gems by running destroy_cdk_applications.cmd.
1. Copy the following destruction script to your engine root folder:
* Windows
```
{engine_root}\scripts\build\Platform\Windows\destroy_cdk_applications.cmd
```
* Linux
```
{engine_root}/scripts/build/Platform/Linux/destroy_cdk_applications.sh
```
2. In the same CLI window, destroy the CDK applications for AWS gems by running destroy_cdk_applications.cmd.
@@ -1,289 +1,289 @@
"""
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 logging
import os
import pytest
import typing
from datetime import datetime
import ly_test_tools.log.log_monitor
from AWS.common import constants
from AWS.common.resource_mappings import AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY
from .aws_metrics_custom_thread import AWSMetricsThread
# fixture imports
from assetpipeline.ap_fixtures.asset_processor_fixture import asset_processor
from .aws_metrics_utils import aws_metrics_utils
AWS_METRICS_FEATURE_NAME = 'AWSMetrics'
logger = logging.getLogger(__name__)
def setup(launcher: pytest.fixture,
asset_processor: pytest.fixture) -> pytest.fixture:
"""
Set up the resource mapping configuration and start the log monitor.
:param launcher: Client launcher for running the test level.
:param asset_processor: asset_processor fixture.
:return log monitor object.
"""
asset_processor.start()
asset_processor.wait_for_idle()
file_to_monitor = os.path.join(launcher.workspace.paths.project_log(), constants.GAME_LOG_NAME)
# Initialize the log monitor.
log_monitor = ly_test_tools.log.log_monitor.LogMonitor(launcher=launcher, log_file_path=file_to_monitor)
return log_monitor
def monitor_metrics_submission(log_monitor: pytest.fixture) -> None:
"""
Monitor the messages and notifications for submitting metrics.
:param log_monitor: Log monitor to check the log messages.
"""
expected_lines = [
'(Script) - Submitted metrics without buffer.',
'(Script) - Submitted metrics with buffer.',
'(Script) - Flushed the buffered metrics.',
'(Script) - Metrics is sent successfully.'
]
unexpected_lines = [
'(Script) - Failed to submit metrics without buffer.',
'(Script) - Failed to submit metrics with buffer.',
'(Script) - Failed to send metrics.'
]
result = log_monitor.monitor_log_for_lines(
expected_lines=expected_lines,
unexpected_lines=unexpected_lines,
halt_on_unexpected=True)
# Assert the log monitor detected expected lines and did not detect any unexpected lines.
assert result, (
f'Log monitoring failed. Used expected_lines values: {expected_lines} & '
f'unexpected_lines values: {unexpected_lines}')
def query_metrics_from_s3(aws_metrics_utils: pytest.fixture, resource_mappings: pytest.fixture) -> None:
"""
Verify that the metrics events are delivered to the S3 bucket and can be queried.
:param aws_metrics_utils: aws_metrics_utils fixture.
:param resource_mappings: resource_mappings fixture.
"""
aws_metrics_utils.verify_s3_delivery(
resource_mappings.get_resource_name_id('AWSMetrics.AnalyticsBucketName')
)
logger.info('Metrics are sent to S3.')
aws_metrics_utils.run_glue_crawler(
resource_mappings.get_resource_name_id('AWSMetrics.EventsCrawlerName'))
# Remove the events_json table if exists so that the sample query can create a table with the same name.
aws_metrics_utils.delete_table(resource_mappings.get_resource_name_id('AWSMetrics.EventDatabaseName'), 'events_json')
aws_metrics_utils.run_named_queries(resource_mappings.get_resource_name_id('AWSMetrics.AthenaWorkGroupName'))
logger.info('Query metrics from S3 successfully.')
def verify_operational_metrics(aws_metrics_utils: pytest.fixture,
resource_mappings: pytest.fixture, start_time: datetime) -> None:
"""
Verify that operational health metrics are delivered to CloudWatch.
:param aws_metrics_utils: aws_metrics_utils fixture.
:param resource_mappings: resource_mappings fixture.
:param start_time: Time when the game launcher starts.
"""
aws_metrics_utils.verify_cloud_watch_delivery(
'AWS/Lambda',
'Invocations',
[{'Name': 'FunctionName',
'Value': resource_mappings.get_resource_name_id('AWSMetrics.AnalyticsProcessingLambdaName')}],
start_time)
logger.info('AnalyticsProcessingLambda metrics are sent to CloudWatch.')
aws_metrics_utils.verify_cloud_watch_delivery(
'AWS/Lambda',
'Invocations',
[{'Name': 'FunctionName',
'Value': resource_mappings.get_resource_name_id('AWSMetrics.EventProcessingLambdaName')}],
start_time)
logger.info('EventsProcessingLambda metrics are sent to CloudWatch.')
def update_kinesis_analytics_application_status(aws_metrics_utils: pytest.fixture,
resource_mappings: pytest.fixture, start_application: bool) -> None:
"""
Update the Kinesis analytics application to start or stop it.
:param aws_metrics_utils: aws_metrics_utils fixture.
:param resource_mappings: resource_mappings fixture.
:param start_application: whether to start or stop the application.
"""
if start_application:
aws_metrics_utils.start_kinesis_data_analytics_application(
resource_mappings.get_resource_name_id('AWSMetrics.AnalyticsApplicationName'))
else:
aws_metrics_utils.stop_kinesis_data_analytics_application(
resource_mappings.get_resource_name_id('AWSMetrics.AnalyticsApplicationName'))
@pytest.mark.SUITE_awsi
@pytest.mark.usefixtures('automatic_process_killer')
@pytest.mark.usefixtures('aws_credentials')
@pytest.mark.usefixtures('resource_mappings')
@pytest.mark.parametrize('assume_role_arn', [constants.ASSUME_ROLE_ARN])
@pytest.mark.parametrize('feature_name', [AWS_METRICS_FEATURE_NAME])
@pytest.mark.parametrize('profile_name', ['AWSAutomationTest'])
@pytest.mark.parametrize('project', ['AutomatedTesting'])
@pytest.mark.parametrize('region_name', [constants.AWS_REGION])
@pytest.mark.parametrize('resource_mappings_filename', [constants.AWS_RESOURCE_MAPPING_FILE_NAME])
@pytest.mark.parametrize('session_name', [constants.SESSION_NAME])
@pytest.mark.parametrize('stacks', [[f'{constants.AWS_PROJECT_NAME}-{AWS_METRICS_FEATURE_NAME}-{constants.AWS_REGION}']])
class TestAWSMetricsWindows(object):
"""
Test class to verify the real-time and batch analytics for metrics.
"""
@pytest.mark.parametrize('level', ['AWS/Metrics'])
def test_realtime_and_batch_analytics(self,
level: str,
launcher: pytest.fixture,
asset_processor: pytest.fixture,
workspace: pytest.fixture,
aws_utils: pytest.fixture,
resource_mappings: pytest.fixture,
aws_metrics_utils: pytest.fixture):
"""
Verify that the metrics events are sent to CloudWatch and S3 for analytics.
"""
# Start Kinesis analytics application on a separate thread to avoid blocking the test.
kinesis_analytics_application_thread = AWSMetricsThread(target=update_kinesis_analytics_application_status,
args=(aws_metrics_utils, resource_mappings, True))
kinesis_analytics_application_thread.start()
log_monitor = setup(launcher, asset_processor)
# Kinesis analytics application needs to be in the running state before we start the game launcher.
kinesis_analytics_application_thread.join()
launcher.args = ['+LoadLevel', level]
launcher.args.extend(['-rhi=null'])
start_time = datetime.utcnow()
with launcher.start(launch_ap=False):
monitor_metrics_submission(log_monitor)
# Verify that real-time analytics metrics are delivered to CloudWatch.
aws_metrics_utils.verify_cloud_watch_delivery(
AWS_METRICS_FEATURE_NAME,
'TotalLogins',
[],
start_time)
logger.info('Real-time metrics are sent to CloudWatch.')
# Run time-consuming operations on separate threads to avoid blocking the test.
operational_threads = list()
operational_threads.append(
AWSMetricsThread(target=query_metrics_from_s3,
args=(aws_metrics_utils, resource_mappings)))
operational_threads.append(
AWSMetricsThread(target=verify_operational_metrics,
args=(aws_metrics_utils, resource_mappings, start_time)))
operational_threads.append(
AWSMetricsThread(target=update_kinesis_analytics_application_status,
args=(aws_metrics_utils, resource_mappings, False)))
for thread in operational_threads:
thread.start()
for thread in operational_threads:
thread.join()
@pytest.mark.parametrize('level', ['AWS/Metrics'])
def test_realtime_and_batch_analytics_no_global_accountid(self,
level: str,
launcher: pytest.fixture,
asset_processor: pytest.fixture,
workspace: pytest.fixture,
aws_utils: pytest.fixture,
resource_mappings: pytest.fixture,
aws_metrics_utils: pytest.fixture):
"""
Verify that the metrics events are sent to CloudWatch and S3 for analytics.
"""
# Remove top-level account ID from resource mappings
resource_mappings.clear_select_keys([AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY])
# Start Kinesis analytics application on a separate thread to avoid blocking the test.
kinesis_analytics_application_thread = AWSMetricsThread(target=update_kinesis_analytics_application_status,
args=(aws_metrics_utils, resource_mappings, True))
kinesis_analytics_application_thread.start()
log_monitor = setup(launcher, asset_processor)
# Kinesis analytics application needs to be in the running state before we start the game launcher.
kinesis_analytics_application_thread.join()
launcher.args = ['+LoadLevel', level]
launcher.args.extend(['-rhi=null'])
start_time = datetime.utcnow()
with launcher.start(launch_ap=False):
monitor_metrics_submission(log_monitor)
# Verify that real-time analytics metrics are delivered to CloudWatch.
aws_metrics_utils.verify_cloud_watch_delivery(
AWS_METRICS_FEATURE_NAME,
'TotalLogins',
[],
start_time)
logger.info('Real-time metrics are sent to CloudWatch.')
# Run time-consuming operations on separate threads to avoid blocking the test.
operational_threads = list()
operational_threads.append(
AWSMetricsThread(target=query_metrics_from_s3,
args=(aws_metrics_utils, resource_mappings)))
operational_threads.append(
AWSMetricsThread(target=verify_operational_metrics,
args=(aws_metrics_utils, resource_mappings, start_time)))
operational_threads.append(
AWSMetricsThread(target=update_kinesis_analytics_application_status,
args=(aws_metrics_utils, resource_mappings, False)))
for thread in operational_threads:
thread.start()
for thread in operational_threads:
thread.join()
@pytest.mark.parametrize('level', ['AWS/Metrics'])
def test_unauthorized_user_request_rejected(self,
level: str,
launcher: pytest.fixture,
asset_processor: pytest.fixture,
workspace: pytest.fixture):
"""
Verify that unauthorized users cannot send metrics events to the AWS backed backend.
"""
log_monitor = setup(launcher, asset_processor)
# Set invalid AWS credentials.
launcher.args = ['+LoadLevel', level, '+cl_awsAccessKey', 'AKIAIOSFODNN7EXAMPLE',
'+cl_awsSecretKey', 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY']
launcher.args.extend(['-rhi=null'])
with launcher.start(launch_ap=False):
result = log_monitor.monitor_log_for_lines(
expected_lines=['(Script) - Failed to send metrics.'],
unexpected_lines=['(Script) - Metrics is sent successfully.'],
halt_on_unexpected=True)
assert result, 'Metrics events are sent successfully by unauthorized user'
logger.info('Unauthorized user is rejected to send metrics.')
def test_clean_up_s3_bucket(self,
aws_utils: pytest.fixture,
resource_mappings: pytest.fixture,
aws_metrics_utils: pytest.fixture):
"""
Clear the analytics bucket objects so that the S3 bucket can be destroyed during tear down.
"""
aws_metrics_utils.empty_bucket(
resource_mappings.get_resource_name_id('AWSMetrics.AnalyticsBucketName'))
"""
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 logging
import os
import pytest
import typing
from datetime import datetime
import ly_test_tools.log.log_monitor
from AWS.common import constants
from AWS.common.resource_mappings import AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY
from .aws_metrics_custom_thread import AWSMetricsThread
# fixture imports
from assetpipeline.ap_fixtures.asset_processor_fixture import asset_processor
from .aws_metrics_utils import aws_metrics_utils
AWS_METRICS_FEATURE_NAME = 'AWSMetrics'
logger = logging.getLogger(__name__)
def setup(launcher: pytest.fixture,
asset_processor: pytest.fixture) -> pytest.fixture:
"""
Set up the resource mapping configuration and start the log monitor.
:param launcher: Client launcher for running the test level.
:param asset_processor: asset_processor fixture.
:return log monitor object.
"""
asset_processor.start()
asset_processor.wait_for_idle()
file_to_monitor = os.path.join(launcher.workspace.paths.project_log(), constants.GAME_LOG_NAME)
# Initialize the log monitor.
log_monitor = ly_test_tools.log.log_monitor.LogMonitor(launcher=launcher, log_file_path=file_to_monitor)
return log_monitor
def monitor_metrics_submission(log_monitor: pytest.fixture) -> None:
"""
Monitor the messages and notifications for submitting metrics.
:param log_monitor: Log monitor to check the log messages.
"""
expected_lines = [
'(Script) - Submitted metrics without buffer.',
'(Script) - Submitted metrics with buffer.',
'(Script) - Flushed the buffered metrics.',
'(Script) - Metrics is sent successfully.'
]
unexpected_lines = [
'(Script) - Failed to submit metrics without buffer.',
'(Script) - Failed to submit metrics with buffer.',
'(Script) - Failed to send metrics.'
]
result = log_monitor.monitor_log_for_lines(
expected_lines=expected_lines,
unexpected_lines=unexpected_lines,
halt_on_unexpected=True)
# Assert the log monitor detected expected lines and did not detect any unexpected lines.
assert result, (
f'Log monitoring failed. Used expected_lines values: {expected_lines} & '
f'unexpected_lines values: {unexpected_lines}')
def query_metrics_from_s3(aws_metrics_utils: pytest.fixture, resource_mappings: pytest.fixture) -> None:
"""
Verify that the metrics events are delivered to the S3 bucket and can be queried.
:param aws_metrics_utils: aws_metrics_utils fixture.
:param resource_mappings: resource_mappings fixture.
"""
aws_metrics_utils.verify_s3_delivery(
resource_mappings.get_resource_name_id('AWSMetrics.AnalyticsBucketName')
)
logger.info('Metrics are sent to S3.')
aws_metrics_utils.run_glue_crawler(
resource_mappings.get_resource_name_id('AWSMetrics.EventsCrawlerName'))
# Remove the events_json table if exists so that the sample query can create a table with the same name.
aws_metrics_utils.delete_table(resource_mappings.get_resource_name_id('AWSMetrics.EventDatabaseName'), 'events_json')
aws_metrics_utils.run_named_queries(resource_mappings.get_resource_name_id('AWSMetrics.AthenaWorkGroupName'))
logger.info('Query metrics from S3 successfully.')
def verify_operational_metrics(aws_metrics_utils: pytest.fixture,
resource_mappings: pytest.fixture, start_time: datetime) -> None:
"""
Verify that operational health metrics are delivered to CloudWatch.
:param aws_metrics_utils: aws_metrics_utils fixture.
:param resource_mappings: resource_mappings fixture.
:param start_time: Time when the game launcher starts.
"""
aws_metrics_utils.verify_cloud_watch_delivery(
'AWS/Lambda',
'Invocations',
[{'Name': 'FunctionName',
'Value': resource_mappings.get_resource_name_id('AWSMetrics.AnalyticsProcessingLambdaName')}],
start_time)
logger.info('AnalyticsProcessingLambda metrics are sent to CloudWatch.')
aws_metrics_utils.verify_cloud_watch_delivery(
'AWS/Lambda',
'Invocations',
[{'Name': 'FunctionName',
'Value': resource_mappings.get_resource_name_id('AWSMetrics.EventProcessingLambdaName')}],
start_time)
logger.info('EventsProcessingLambda metrics are sent to CloudWatch.')
def update_kinesis_analytics_application_status(aws_metrics_utils: pytest.fixture,
resource_mappings: pytest.fixture, start_application: bool) -> None:
"""
Update the Kinesis analytics application to start or stop it.
:param aws_metrics_utils: aws_metrics_utils fixture.
:param resource_mappings: resource_mappings fixture.
:param start_application: whether to start or stop the application.
"""
if start_application:
aws_metrics_utils.start_kinesis_data_analytics_application(
resource_mappings.get_resource_name_id('AWSMetrics.AnalyticsApplicationName'))
else:
aws_metrics_utils.stop_kinesis_data_analytics_application(
resource_mappings.get_resource_name_id('AWSMetrics.AnalyticsApplicationName'))
@pytest.mark.SUITE_awsi
@pytest.mark.usefixtures('automatic_process_killer')
@pytest.mark.usefixtures('aws_credentials')
@pytest.mark.usefixtures('resource_mappings')
@pytest.mark.parametrize('assume_role_arn', [constants.ASSUME_ROLE_ARN])
@pytest.mark.parametrize('feature_name', [AWS_METRICS_FEATURE_NAME])
@pytest.mark.parametrize('profile_name', ['AWSAutomationTest'])
@pytest.mark.parametrize('project', ['AutomatedTesting'])
@pytest.mark.parametrize('region_name', [constants.AWS_REGION])
@pytest.mark.parametrize('resource_mappings_filename', [constants.AWS_RESOURCE_MAPPING_FILE_NAME])
@pytest.mark.parametrize('session_name', [constants.SESSION_NAME])
@pytest.mark.parametrize('stacks', [[f'{constants.AWS_PROJECT_NAME}-{AWS_METRICS_FEATURE_NAME}-{constants.AWS_REGION}']])
class TestAWSMetricsWindows(object):
"""
Test class to verify the real-time and batch analytics for metrics.
"""
@pytest.mark.parametrize('level', ['levels/aws/metrics/metrics.spawnable'])
def test_realtime_and_batch_analytics(self,
level: str,
launcher: pytest.fixture,
asset_processor: pytest.fixture,
workspace: pytest.fixture,
aws_utils: pytest.fixture,
resource_mappings: pytest.fixture,
aws_metrics_utils: pytest.fixture):
"""
Verify that the metrics events are sent to CloudWatch and S3 for analytics.
"""
# Start Kinesis analytics application on a separate thread to avoid blocking the test.
kinesis_analytics_application_thread = AWSMetricsThread(target=update_kinesis_analytics_application_status,
args=(aws_metrics_utils, resource_mappings, True))
kinesis_analytics_application_thread.start()
log_monitor = setup(launcher, asset_processor)
# Kinesis analytics application needs to be in the running state before we start the game launcher.
kinesis_analytics_application_thread.join()
launcher.args = ['+LoadLevel', level]
launcher.args.extend(['-rhi=null'])
start_time = datetime.utcnow()
with launcher.start(launch_ap=False):
monitor_metrics_submission(log_monitor)
# Verify that real-time analytics metrics are delivered to CloudWatch.
aws_metrics_utils.verify_cloud_watch_delivery(
AWS_METRICS_FEATURE_NAME,
'TotalLogins',
[],
start_time)
logger.info('Real-time metrics are sent to CloudWatch.')
# Run time-consuming operations on separate threads to avoid blocking the test.
operational_threads = list()
operational_threads.append(
AWSMetricsThread(target=query_metrics_from_s3,
args=(aws_metrics_utils, resource_mappings)))
operational_threads.append(
AWSMetricsThread(target=verify_operational_metrics,
args=(aws_metrics_utils, resource_mappings, start_time)))
operational_threads.append(
AWSMetricsThread(target=update_kinesis_analytics_application_status,
args=(aws_metrics_utils, resource_mappings, False)))
for thread in operational_threads:
thread.start()
for thread in operational_threads:
thread.join()
@pytest.mark.parametrize('level', ['levels/aws/metrics/metrics.spawnable'])
def test_realtime_and_batch_analytics_no_global_accountid(self,
level: str,
launcher: pytest.fixture,
asset_processor: pytest.fixture,
workspace: pytest.fixture,
aws_utils: pytest.fixture,
resource_mappings: pytest.fixture,
aws_metrics_utils: pytest.fixture):
"""
Verify that the metrics events are sent to CloudWatch and S3 for analytics.
"""
# Remove top-level account ID from resource mappings
resource_mappings.clear_select_keys([AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY])
# Start Kinesis analytics application on a separate thread to avoid blocking the test.
kinesis_analytics_application_thread = AWSMetricsThread(target=update_kinesis_analytics_application_status,
args=(aws_metrics_utils, resource_mappings, True))
kinesis_analytics_application_thread.start()
log_monitor = setup(launcher, asset_processor)
# Kinesis analytics application needs to be in the running state before we start the game launcher.
kinesis_analytics_application_thread.join()
launcher.args = ['+LoadLevel', level]
launcher.args.extend(['-rhi=null'])
start_time = datetime.utcnow()
with launcher.start(launch_ap=False):
monitor_metrics_submission(log_monitor)
# Verify that real-time analytics metrics are delivered to CloudWatch.
aws_metrics_utils.verify_cloud_watch_delivery(
AWS_METRICS_FEATURE_NAME,
'TotalLogins',
[],
start_time)
logger.info('Real-time metrics are sent to CloudWatch.')
# Run time-consuming operations on separate threads to avoid blocking the test.
operational_threads = list()
operational_threads.append(
AWSMetricsThread(target=query_metrics_from_s3,
args=(aws_metrics_utils, resource_mappings)))
operational_threads.append(
AWSMetricsThread(target=verify_operational_metrics,
args=(aws_metrics_utils, resource_mappings, start_time)))
operational_threads.append(
AWSMetricsThread(target=update_kinesis_analytics_application_status,
args=(aws_metrics_utils, resource_mappings, False)))
for thread in operational_threads:
thread.start()
for thread in operational_threads:
thread.join()
@pytest.mark.parametrize('level', ['levels/aws/metrics/metrics.spawnable'])
def test_unauthorized_user_request_rejected(self,
level: str,
launcher: pytest.fixture,
asset_processor: pytest.fixture,
workspace: pytest.fixture):
"""
Verify that unauthorized users cannot send metrics events to the AWS backed backend.
"""
log_monitor = setup(launcher, asset_processor)
# Set invalid AWS credentials.
launcher.args = ['+LoadLevel', level, '+cl_awsAccessKey', 'AKIAIOSFODNN7EXAMPLE',
'+cl_awsSecretKey', 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY']
launcher.args.extend(['-rhi=null'])
with launcher.start(launch_ap=False):
result = log_monitor.monitor_log_for_lines(
expected_lines=['(Script) - Failed to send metrics.'],
unexpected_lines=['(Script) - Metrics is sent successfully.'],
halt_on_unexpected=True)
assert result, 'Metrics events are sent successfully by unauthorized user'
logger.info('Unauthorized user is rejected to send metrics.')
def test_clean_up_s3_bucket(self,
aws_utils: pytest.fixture,
resource_mappings: pytest.fixture,
aws_metrics_utils: pytest.fixture):
"""
Clear the analytics bucket objects so that the S3 bucket can be destroyed during tear down.
"""
aws_metrics_utils.empty_bucket(
resource_mappings.get_resource_name_id('AWSMetrics.AnalyticsBucketName'))
@@ -1,29 +1,29 @@
"""
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 threading import Thread
class AWSMetricsThread(Thread):
"""
Custom thread for raising assertion errors on the main thread.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._error = None
def run(self) -> None:
try:
super().run()
except AssertionError as e:
self._error = e
def join(self, **kwargs) -> None:
super().join(**kwargs)
if self._error:
raise AssertionError(self._error)
"""
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 threading import Thread
class AWSMetricsThread(Thread):
"""
Custom thread for raising assertion errors on the main thread.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._error = None
def run(self) -> None:
try:
super().run()
except AssertionError as e:
self._error = e
def join(self, **kwargs) -> None:
super().join(**kwargs)
if self._error:
raise AssertionError(self._error)
@@ -1,239 +1,239 @@
"""
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 logging
import pathlib
import pytest
import typing
from datetime import datetime
from botocore.exceptions import WaiterError
from .aws_metrics_waiters import KinesisAnalyticsApplicationUpdatedWaiter, \
CloudWatchMetricsDeliveredWaiter, DataLakeMetricsDeliveredWaiter, GlueCrawlerReadyWaiter
logging.getLogger('boto').setLevel(logging.CRITICAL)
# Expected directory and file extension for the S3 objects.
EXPECTED_S3_DIRECTORY = 'firehose_events/'
EXPECTED_S3_OBJECT_EXTENSION = '.parquet'
class AWSMetricsUtils:
"""
Provide utils functions for the AWSMetrics gem to interact with the deployed resources.
"""
def __init__(self, aws_utils: pytest.fixture):
self._aws_util = aws_utils
def start_kinesis_data_analytics_application(self, application_name: str) -> None:
"""
Start the Kenisis Data Analytics application for real-time analytics.
:param application_name: Name of the Kenisis Data Analytics application.
"""
input_id = self.get_kinesis_analytics_application_input_id(application_name)
assert input_id, 'invalid Kinesis Data Analytics application input.'
client = self._aws_util.client('kinesisanalytics')
try:
client.start_application(
ApplicationName=application_name,
InputConfigurations=[
{
'Id': input_id,
'InputStartingPositionConfiguration': {
'InputStartingPosition': 'NOW'
}
},
]
)
except client.exceptions.ResourceInUseException:
# The application has been started.
return
try:
KinesisAnalyticsApplicationUpdatedWaiter(client, 'RUNNING').wait(application_name=application_name)
except WaiterError as e:
assert False, f'Failed to start the Kinesis Data Analytics application: {str(e)}.'
def get_kinesis_analytics_application_input_id(self, application_name: str) -> str:
"""
Get the input ID for the Kenisis Data Analytics application.
:param application_name: Name of the Kenisis Data Analytics application.
:return: Input ID for the Kenisis Data Analytics application.
"""
client = self._aws_util.client('kinesisanalytics')
response = client.describe_application(
ApplicationName=application_name
)
if not response:
return ''
input_descriptions = response.get('ApplicationDetail', {}).get('InputDescriptions', [])
if len(input_descriptions) != 1:
return ''
return input_descriptions[0].get('InputId', '')
def stop_kinesis_data_analytics_application(self, application_name: str) -> None:
"""
Stop the Kenisis Data Analytics application.
:param application_name: Name of the Kenisis Data Analytics application.
"""
client = self._aws_util.client('kinesisanalytics')
client.stop_application(
ApplicationName=application_name
)
try:
KinesisAnalyticsApplicationUpdatedWaiter(client, 'READY').wait(application_name=application_name)
except WaiterError as e:
assert False, f'Failed to stop the Kinesis Data Analytics application: {str(e)}.'
def verify_cloud_watch_delivery(self, namespace: str, metrics_name: str,
dimensions: typing.List[dict], start_time: datetime) -> None:
"""
Verify that the expected metrics is delivered to CloudWatch.
:param namespace: Namespace of the metrics.
:param metrics_name: Name of the metrics.
:param dimensions: Dimensions of the metrics.
:param start_time: Start time for generating the metrics.
"""
client = self._aws_util.client('cloudwatch')
try:
CloudWatchMetricsDeliveredWaiter(client).wait(
namespace=namespace,
metrics_name=metrics_name,
dimensions=dimensions,
start_time=start_time
)
except WaiterError as e:
assert False, f'Failed to deliver metrics to CloudWatch: {str(e)}.'
def verify_s3_delivery(self, analytics_bucket_name: str) -> None:
"""
Verify that metrics are delivered to S3 for batch analytics successfully.
:param analytics_bucket_name: Name of the deployed S3 bucket.
"""
client = self._aws_util.client('s3')
bucket_name = analytics_bucket_name
try:
DataLakeMetricsDeliveredWaiter(client).wait(bucket_name=bucket_name, prefix=EXPECTED_S3_DIRECTORY)
except WaiterError as e:
assert False, f'Failed to find the S3 directory for storing metrics data: {str(e)}.'
# Check whether the data is converted to the expected data format.
response = client.list_objects_v2(
Bucket=bucket_name,
Prefix=EXPECTED_S3_DIRECTORY
)
assert response.get('KeyCount', 0) != 0, f'Failed to deliver metrics to the S3 bucket {bucket_name}.'
s3_objects = response.get('Contents', [])
for s3_object in s3_objects:
key = s3_object.get('Key', '')
assert pathlib.Path(key).suffix == EXPECTED_S3_OBJECT_EXTENSION, \
f'Invalid data format is found in the S3 bucket {bucket_name}'
def run_glue_crawler(self, crawler_name: str) -> None:
"""
Run the Glue crawler and wait for it to finish.
:param crawler_name: Name of the Glue crawler
"""
client = self._aws_util.client('glue')
try:
client.start_crawler(
Name=crawler_name
)
except client.exceptions.CrawlerRunningException:
# The crawler has already been started.
return
try:
GlueCrawlerReadyWaiter(client).wait(crawler_name=crawler_name)
except WaiterError as e:
assert False, f'Failed to run the Glue crawler: {str(e)}.'
def run_named_queries(self, work_group: str) -> None:
"""
Run the named queries under the specific Athena work group.
:param work_group: Name of the Athena work group.
"""
client = self._aws_util.client('athena')
# List all the named queries.
response = client.list_named_queries(
WorkGroup=work_group
)
named_query_ids = response.get('NamedQueryIds', [])
# Run each of the queries.
for named_query_id in named_query_ids:
get_named_query_response = client.get_named_query(
NamedQueryId=named_query_id
)
named_query = get_named_query_response.get('NamedQuery', {})
start_query_execution_response = client.start_query_execution(
QueryString=named_query.get('QueryString', ''),
QueryExecutionContext={
'Database': named_query.get('Database', '')
},
WorkGroup=work_group
)
# Wait for the query to finish.
state = 'RUNNING'
while state == 'QUEUED' or state == 'RUNNING':
get_query_execution_response = client.get_query_execution(
QueryExecutionId=start_query_execution_response.get('QueryExecutionId', '')
)
state = get_query_execution_response.get('QueryExecution', {}).get('Status', {}).get('State', '')
assert state == 'SUCCEEDED', f'Failed to run the named query {named_query.get("Name", {})}'
def empty_bucket(self, bucket_name: str) -> None:
"""
Empty the S3 bucket following:
https://boto3.amazonaws.com/v1/documentation/api/latest/guide/migrations3.html
:param bucket_name: Name of the S3 bucket.
"""
s3 = self._aws_util.resource('s3')
bucket = s3.Bucket(bucket_name)
for key in bucket.objects.all():
key.delete()
def delete_table(self, database_name: str, table_name: str) -> None:
"""
Delete an existing Glue table.
:param database_name: Name of the Glue database.
:param table_name: Name of the table to delete.
"""
client = self._aws_util.client('glue')
client.delete_table(
DatabaseName=database_name,
Name=table_name
)
@pytest.fixture(scope='function')
def aws_metrics_utils(
request: pytest.fixture,
aws_utils: pytest.fixture):
"""
Fixture for the AWS metrics util functions.
:param request: _pytest.fixtures.SubRequest class that handles getting
a pytest fixture from a pytest function/fixture.
:param aws_utils: aws_utils fixture.
"""
aws_utils_obj = AWSMetricsUtils(aws_utils)
return aws_utils_obj
"""
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 logging
import pathlib
import pytest
import typing
from datetime import datetime
from botocore.exceptions import WaiterError
from .aws_metrics_waiters import KinesisAnalyticsApplicationUpdatedWaiter, \
CloudWatchMetricsDeliveredWaiter, DataLakeMetricsDeliveredWaiter, GlueCrawlerReadyWaiter
logging.getLogger('boto').setLevel(logging.CRITICAL)
# Expected directory and file extension for the S3 objects.
EXPECTED_S3_DIRECTORY = 'firehose_events/'
EXPECTED_S3_OBJECT_EXTENSION = '.parquet'
class AWSMetricsUtils:
"""
Provide utils functions for the AWSMetrics gem to interact with the deployed resources.
"""
def __init__(self, aws_utils: pytest.fixture):
self._aws_util = aws_utils
def start_kinesis_data_analytics_application(self, application_name: str) -> None:
"""
Start the Kenisis Data Analytics application for real-time analytics.
:param application_name: Name of the Kenisis Data Analytics application.
"""
input_id = self.get_kinesis_analytics_application_input_id(application_name)
assert input_id, 'invalid Kinesis Data Analytics application input.'
client = self._aws_util.client('kinesisanalytics')
try:
client.start_application(
ApplicationName=application_name,
InputConfigurations=[
{
'Id': input_id,
'InputStartingPositionConfiguration': {
'InputStartingPosition': 'NOW'
}
},
]
)
except client.exceptions.ResourceInUseException:
# The application has been started.
return
try:
KinesisAnalyticsApplicationUpdatedWaiter(client, 'RUNNING').wait(application_name=application_name)
except WaiterError as e:
assert False, f'Failed to start the Kinesis Data Analytics application: {str(e)}.'
def get_kinesis_analytics_application_input_id(self, application_name: str) -> str:
"""
Get the input ID for the Kenisis Data Analytics application.
:param application_name: Name of the Kenisis Data Analytics application.
:return: Input ID for the Kenisis Data Analytics application.
"""
client = self._aws_util.client('kinesisanalytics')
response = client.describe_application(
ApplicationName=application_name
)
if not response:
return ''
input_descriptions = response.get('ApplicationDetail', {}).get('InputDescriptions', [])
if len(input_descriptions) != 1:
return ''
return input_descriptions[0].get('InputId', '')
def stop_kinesis_data_analytics_application(self, application_name: str) -> None:
"""
Stop the Kenisis Data Analytics application.
:param application_name: Name of the Kenisis Data Analytics application.
"""
client = self._aws_util.client('kinesisanalytics')
client.stop_application(
ApplicationName=application_name
)
try:
KinesisAnalyticsApplicationUpdatedWaiter(client, 'READY').wait(application_name=application_name)
except WaiterError as e:
assert False, f'Failed to stop the Kinesis Data Analytics application: {str(e)}.'
def verify_cloud_watch_delivery(self, namespace: str, metrics_name: str,
dimensions: typing.List[dict], start_time: datetime) -> None:
"""
Verify that the expected metrics is delivered to CloudWatch.
:param namespace: Namespace of the metrics.
:param metrics_name: Name of the metrics.
:param dimensions: Dimensions of the metrics.
:param start_time: Start time for generating the metrics.
"""
client = self._aws_util.client('cloudwatch')
try:
CloudWatchMetricsDeliveredWaiter(client).wait(
namespace=namespace,
metrics_name=metrics_name,
dimensions=dimensions,
start_time=start_time
)
except WaiterError as e:
assert False, f'Failed to deliver metrics to CloudWatch: {str(e)}.'
def verify_s3_delivery(self, analytics_bucket_name: str) -> None:
"""
Verify that metrics are delivered to S3 for batch analytics successfully.
:param analytics_bucket_name: Name of the deployed S3 bucket.
"""
client = self._aws_util.client('s3')
bucket_name = analytics_bucket_name
try:
DataLakeMetricsDeliveredWaiter(client).wait(bucket_name=bucket_name, prefix=EXPECTED_S3_DIRECTORY)
except WaiterError as e:
assert False, f'Failed to find the S3 directory for storing metrics data: {str(e)}.'
# Check whether the data is converted to the expected data format.
response = client.list_objects_v2(
Bucket=bucket_name,
Prefix=EXPECTED_S3_DIRECTORY
)
assert response.get('KeyCount', 0) != 0, f'Failed to deliver metrics to the S3 bucket {bucket_name}.'
s3_objects = response.get('Contents', [])
for s3_object in s3_objects:
key = s3_object.get('Key', '')
assert pathlib.Path(key).suffix == EXPECTED_S3_OBJECT_EXTENSION, \
f'Invalid data format is found in the S3 bucket {bucket_name}'
def run_glue_crawler(self, crawler_name: str) -> None:
"""
Run the Glue crawler and wait for it to finish.
:param crawler_name: Name of the Glue crawler
"""
client = self._aws_util.client('glue')
try:
client.start_crawler(
Name=crawler_name
)
except client.exceptions.CrawlerRunningException:
# The crawler has already been started.
return
try:
GlueCrawlerReadyWaiter(client).wait(crawler_name=crawler_name)
except WaiterError as e:
assert False, f'Failed to run the Glue crawler: {str(e)}.'
def run_named_queries(self, work_group: str) -> None:
"""
Run the named queries under the specific Athena work group.
:param work_group: Name of the Athena work group.
"""
client = self._aws_util.client('athena')
# List all the named queries.
response = client.list_named_queries(
WorkGroup=work_group
)
named_query_ids = response.get('NamedQueryIds', [])
# Run each of the queries.
for named_query_id in named_query_ids:
get_named_query_response = client.get_named_query(
NamedQueryId=named_query_id
)
named_query = get_named_query_response.get('NamedQuery', {})
start_query_execution_response = client.start_query_execution(
QueryString=named_query.get('QueryString', ''),
QueryExecutionContext={
'Database': named_query.get('Database', '')
},
WorkGroup=work_group
)
# Wait for the query to finish.
state = 'RUNNING'
while state == 'QUEUED' or state == 'RUNNING':
get_query_execution_response = client.get_query_execution(
QueryExecutionId=start_query_execution_response.get('QueryExecutionId', '')
)
state = get_query_execution_response.get('QueryExecution', {}).get('Status', {}).get('State', '')
assert state == 'SUCCEEDED', f'Failed to run the named query {named_query.get("Name", {})}'
def empty_bucket(self, bucket_name: str) -> None:
"""
Empty the S3 bucket following:
https://boto3.amazonaws.com/v1/documentation/api/latest/guide/migrations3.html
:param bucket_name: Name of the S3 bucket.
"""
s3 = self._aws_util.resource('s3')
bucket = s3.Bucket(bucket_name)
for key in bucket.objects.all():
key.delete()
def delete_table(self, database_name: str, table_name: str) -> None:
"""
Delete an existing Glue table.
:param database_name: Name of the Glue database.
:param table_name: Name of the table to delete.
"""
client = self._aws_util.client('glue')
client.delete_table(
DatabaseName=database_name,
Name=table_name
)
@pytest.fixture(scope='function')
def aws_metrics_utils(
request: pytest.fixture,
aws_utils: pytest.fixture):
"""
Fixture for the AWS metrics util functions.
:param request: _pytest.fixtures.SubRequest class that handles getting
a pytest fixture from a pytest function/fixture.
:param aws_utils: aws_utils fixture.
"""
aws_utils_obj = AWSMetricsUtils(aws_utils)
return aws_utils_obj
@@ -1,139 +1,139 @@
"""
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 botocore.client
import logging
from datetime import timedelta
from AWS.common.custom_waiter import CustomWaiter, WaitState
logging.getLogger('boto').setLevel(logging.CRITICAL)
class KinesisAnalyticsApplicationUpdatedWaiter(CustomWaiter):
"""
Subclass of the base custom waiter class.
Wait for the Kinesis analytics application being updated to a specific status.
"""
def __init__(self, client: botocore.client, status: str):
"""
Initialize the waiter.
:param client: Boto3 client to use.
:param status: Expected status.
"""
super().__init__(
'KinesisAnalyticsApplicationUpdated',
'DescribeApplication',
'ApplicationDetail.ApplicationStatus',
{status: WaitState.SUCCESS},
client)
def wait(self, application_name: str):
"""
Wait for the expected status.
:param application_name: Name of the Kinesis analytics application.
"""
self._wait(ApplicationName=application_name)
class GlueCrawlerReadyWaiter(CustomWaiter):
"""
Subclass of the base custom waiter class.
Wait for the Glue crawler to finish its processing. Return when the crawler is in the "Stopping" status
to avoid wasting too much time in the automation tests on its shutdown process.
"""
def __init__(self, client: botocore.client):
"""
Initialize the waiter.
:param client: Boto3 client to use.
"""
super().__init__(
'GlueCrawlerReady',
'GetCrawler',
'Crawler.State',
{'STOPPING': WaitState.SUCCESS},
client)
def wait(self, crawler_name):
"""
Wait for the expected status.
:param crawler_name: Name of the Glue crawler.
"""
self._wait(Name=crawler_name)
class DataLakeMetricsDeliveredWaiter(CustomWaiter):
"""
Subclass of the base custom waiter class.
Wait for the expected directory being created in the S3 bucket.
"""
def __init__(self, client: botocore.client):
"""
Initialize the waiter.
:param client: Boto3 client to use.
"""
super().__init__(
'DataLakeMetricsDelivered',
'ListObjectsV2',
'KeyCount > `0`',
{True: WaitState.SUCCESS},
client)
def wait(self, bucket_name, prefix):
"""
Wait for the expected directory being created.
:param bucket_name: Name of the S3 bucket.
:param prefix: Name of the expected directory prefix.
"""
self._wait(Bucket=bucket_name, Prefix=prefix)
class CloudWatchMetricsDeliveredWaiter(CustomWaiter):
"""
Subclass of the base custom waiter class.
Wait for the expected metrics being delivered to CloudWatch.
"""
def __init__(self, client: botocore.client):
"""
Initialize the waiter.
:param client: Boto3 client to use.
"""
super().__init__(
'CloudWatchMetricsDelivered',
'GetMetricStatistics',
'length(Datapoints) > `0`',
{True: WaitState.SUCCESS},
client)
def wait(self, namespace, metrics_name, dimensions, start_time):
"""
Wait for the expected metrics being delivered.
:param namespace: Namespace of the metrics.
:param metrics_name: Name of the metrics.
:param dimensions: Dimensions of the metrics.
:param start_time: Start time for generating the metrics.
"""
self._wait(
Namespace=namespace,
MetricName=metrics_name,
Dimensions=dimensions,
StartTime=start_time,
EndTime=start_time + timedelta(0, self.timeout),
Period=60,
Statistics=[
'SampleCount'
],
Unit='Count'
)
"""
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 botocore.client
import logging
from datetime import timedelta
from AWS.common.custom_waiter import CustomWaiter, WaitState
logging.getLogger('boto').setLevel(logging.CRITICAL)
class KinesisAnalyticsApplicationUpdatedWaiter(CustomWaiter):
"""
Subclass of the base custom waiter class.
Wait for the Kinesis analytics application being updated to a specific status.
"""
def __init__(self, client: botocore.client, status: str):
"""
Initialize the waiter.
:param client: Boto3 client to use.
:param status: Expected status.
"""
super().__init__(
'KinesisAnalyticsApplicationUpdated',
'DescribeApplication',
'ApplicationDetail.ApplicationStatus',
{status: WaitState.SUCCESS},
client)
def wait(self, application_name: str):
"""
Wait for the expected status.
:param application_name: Name of the Kinesis analytics application.
"""
self._wait(ApplicationName=application_name)
class GlueCrawlerReadyWaiter(CustomWaiter):
"""
Subclass of the base custom waiter class.
Wait for the Glue crawler to finish its processing. Return when the crawler is in the "Stopping" status
to avoid wasting too much time in the automation tests on its shutdown process.
"""
def __init__(self, client: botocore.client):
"""
Initialize the waiter.
:param client: Boto3 client to use.
"""
super().__init__(
'GlueCrawlerReady',
'GetCrawler',
'Crawler.State',
{'STOPPING': WaitState.SUCCESS},
client)
def wait(self, crawler_name):
"""
Wait for the expected status.
:param crawler_name: Name of the Glue crawler.
"""
self._wait(Name=crawler_name)
class DataLakeMetricsDeliveredWaiter(CustomWaiter):
"""
Subclass of the base custom waiter class.
Wait for the expected directory being created in the S3 bucket.
"""
def __init__(self, client: botocore.client):
"""
Initialize the waiter.
:param client: Boto3 client to use.
"""
super().__init__(
'DataLakeMetricsDelivered',
'ListObjectsV2',
'KeyCount > `0`',
{True: WaitState.SUCCESS},
client)
def wait(self, bucket_name, prefix):
"""
Wait for the expected directory being created.
:param bucket_name: Name of the S3 bucket.
:param prefix: Name of the expected directory prefix.
"""
self._wait(Bucket=bucket_name, Prefix=prefix)
class CloudWatchMetricsDeliveredWaiter(CustomWaiter):
"""
Subclass of the base custom waiter class.
Wait for the expected metrics being delivered to CloudWatch.
"""
def __init__(self, client: botocore.client):
"""
Initialize the waiter.
:param client: Boto3 client to use.
"""
super().__init__(
'CloudWatchMetricsDelivered',
'GetMetricStatistics',
'length(Datapoints) > `0`',
{True: WaitState.SUCCESS},
client)
def wait(self, namespace, metrics_name, dimensions, start_time):
"""
Wait for the expected metrics being delivered.
:param namespace: Namespace of the metrics.
:param metrics_name: Name of the metrics.
:param dimensions: Dimensions of the metrics.
:param start_time: Start time for generating the metrics.
"""
self._wait(
Namespace=namespace,
MetricName=metrics_name,
Dimensions=dimensions,
StartTime=start_time,
EndTime=start_time + timedelta(0, self.timeout),
Period=60,
Statistics=[
'SampleCount'
],
Unit='Count'
)
@@ -40,7 +40,7 @@ class TestAWSClientAuthWindows(object):
Test class to verify AWS Client Auth gem features on Windows.
"""
@pytest.mark.parametrize('level', ['AWS/ClientAuth'])
@pytest.mark.parametrize('level', ['levels/aws/clientauth/clientauth.spawnable'])
def test_anonymous_credentials(self,
level: str,
launcher: pytest.fixture,
@@ -72,7 +72,7 @@ class TestAWSClientAuthWindows(object):
)
assert result, 'Anonymous credentials fetched successfully.'
@pytest.mark.parametrize('level', ['AWS/ClientAuth'])
@pytest.mark.parametrize('level', ['levels/aws/clientauth/clientauth.spawnable'])
def test_anonymous_credentials_no_global_accountid(self,
level: str,
launcher: pytest.fixture,
@@ -140,7 +140,7 @@ class TestAWSClientAuthWindows(object):
except cognito_idp.exceptions.UserNotFoundException:
pass
launcher.args = ['+LoadLevel', 'AWS/ClientAuthPasswordSignUp']
launcher.args = ['+LoadLevel', 'levels/aws/clientauthpasswordsignup/clientauthpasswordsignup.spawnable']
launcher.args.extend(['-rhi=null'])
with launcher.start(launch_ap=False):
@@ -158,7 +158,7 @@ class TestAWSClientAuthWindows(object):
Username='test1'
)
launcher.args = ['+LoadLevel', 'AWS/ClientAuthPasswordSignIn']
launcher.args = ['+LoadLevel', 'levels/aws/clientauthpasswordsignin/clientauthpasswordsignin.spawnable']
launcher.args.extend(['-rhi=null'])
with launcher.start(launch_ap=False):
@@ -4,4 +4,3 @@ For complete copyright and license terms please see the LICENSE at the root of t
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
@@ -84,7 +84,7 @@ def write_test_data_to_dynamodb_table(resource_mappings: pytest.fixture, aws_uti
@pytest.mark.parametrize('session_name', [constants.SESSION_NAME])
@pytest.mark.usefixtures('workspace')
@pytest.mark.parametrize('project', ['AutomatedTesting'])
@pytest.mark.parametrize('level', ['AWS/Core'])
@pytest.mark.parametrize('level', ['levels/aws/core/core.spawnable'])
@pytest.mark.usefixtures('resource_mappings')
@pytest.mark.parametrize('resource_mappings_filename', [constants.AWS_RESOURCE_MAPPING_FILE_NAME])
@pytest.mark.parametrize('stacks', [[f'{constants.AWS_PROJECT_NAME}-{AWS_CORE_FEATURE_NAME}',
@@ -47,18 +47,4 @@ if(PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_BUILD_TESTS_SUPPORTED)
COMPONENT
Atom
)
ly_add_pytest(
NAME AutomatedTesting::Atom_TestSuite_Main_GPU_Optimized
TEST_SUITE main
TEST_REQUIRES gpu
TEST_SERIAL
TIMEOUT 1200
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main_GPU_Optimized.py
RUNTIME_DEPENDENCIES
AssetProcessor
AutomatedTesting.Assets
Editor
COMPONENT
Atom
)
endif()
@@ -4,10 +4,18 @@ For complete copyright and license terms please see the LICENSE at the root of t
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
import logging
import os
import pytest
import ly_test_tools.environment.file_system as file_system
import editor_python_test_tools.hydra_test_utils as hydra
from ly_test_tools.o3de.editor_test import EditorSharedTest, EditorTestSuite
logger = logging.getLogger(__name__)
TEST_DIRECTORY = os.path.join(os.path.dirname(__file__), "tests")
@pytest.mark.parametrize("project", ["AutomatedTesting"])
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@@ -39,10 +47,6 @@ class TestAutomation(EditorTestSuite):
class AtomEditorComponents_DirectionalLightAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DirectionalLightAdded as test_module
@pytest.mark.test_case_id("C36525660")
class AtomEditorComponents_DisplayMapperAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DisplayMapperAdded as test_module
@pytest.mark.test_case_id("C36525661")
class AtomEditorComponents_EntityReferenceAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_EntityReferenceAdded as test_module
@@ -118,3 +122,73 @@ class TestAutomation(EditorTestSuite):
class ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges(EditorSharedTest):
from Atom.tests import hydra_ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges as test_module
@pytest.mark.parametrize("project", ["AutomatedTesting"])
@pytest.mark.parametrize("launcher_platform", ['windows_generic'])
class TestMaterialEditorBasicTests(object):
@pytest.fixture(autouse=True)
def setup_teardown(self, request, workspace, project):
def delete_files():
file_system.delete(
[
os.path.join(workspace.paths.project(), "Materials", "test_material.material"),
os.path.join(workspace.paths.project(), "Materials", "test_material_1.material"),
os.path.join(workspace.paths.project(), "Materials", "test_material_2.material"),
],
True,
True,
)
# Cleanup our newly created materials
delete_files()
def teardown():
# Cleanup our newly created materials
delete_files()
request.addfinalizer(teardown)
@pytest.mark.parametrize("exe_file_name", ["MaterialEditor"])
@pytest.mark.test_case_id("C34448113") # Creating a New Asset.
@pytest.mark.test_case_id("C34448114") # Opening an Existing Asset.
@pytest.mark.test_case_id("C34448115") # Closing Selected Material.
@pytest.mark.test_case_id("C34448116") # Closing All Materials.
@pytest.mark.test_case_id("C34448117") # Closing all but Selected Material.
@pytest.mark.test_case_id("C34448118") # Saving Material.
@pytest.mark.test_case_id("C34448119") # Saving as a New Material.
@pytest.mark.test_case_id("C34448120") # Saving as a Child Material.
@pytest.mark.test_case_id("C34448121") # Saving all Open Materials.
def test_MaterialEditorBasicTests(
self, request, workspace, project, launcher_platform, generic_launcher, exe_file_name):
expected_lines = [
"Material opened: True",
"Test asset doesn't exist initially: True",
"New asset created: True",
"New Material opened: True",
"Material closed: True",
"All documents closed: True",
"Close All Except Selected worked as expected: True",
"Actual Document saved with changes: True",
"Document saved as copy is saved with changes: True",
"Document saved as child is saved with changes: True",
"Save All worked as expected: True",
]
unexpected_lines = [
"Traceback (most recent call last):"
]
hydra.launch_and_validate_results(
request,
TEST_DIRECTORY,
generic_launcher,
"hydra_AtomMaterialEditor_BasicTests.py",
run_python="--runpython",
timeout=43,
expected_lines=expected_lines,
unexpected_lines=unexpected_lines,
halt_on_unexpected=True,
null_renderer=True,
log_file_name="MaterialEditor.log",
enable_prefab_system=False,
)
@@ -4,129 +4,82 @@ For complete copyright and license terms please see the LICENSE at the root of t
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
import datetime
import logging
import os
import zipfile
import pytest
import editor_python_test_tools.hydra_test_utils as hydra
import ly_test_tools.environment.file_system as file_system
from ly_test_tools.benchmark.data_aggregator import BenchmarkDataAggregator
from ly_test_tools.o3de.editor_test import EditorSharedTest, EditorTestSuite
from Atom.atom_utils.atom_component_helper import compare_screenshot_to_golden_image, golden_images_directory
import editor_python_test_tools.hydra_test_utils as hydra
from .atom_utils.atom_component_helper import compare_screenshot_similarity, ImageComparisonTestFailure
logger = logging.getLogger(__name__)
DEFAULT_SUBFOLDER_PATH = 'user/PythonTests/Automated/Screenshots'
TEST_DIRECTORY = os.path.join(os.path.dirname(__file__), "tests")
def golden_images_directory():
"""
Uses this file location to return the valid location for golden image files.
:return: The path to the golden_images directory, but raises an IOError if the golden_images directory is missing.
"""
current_file_directory = os.path.join(os.path.dirname(__file__))
golden_images_dir = os.path.join(current_file_directory, 'golden_images')
if not os.path.exists(golden_images_dir):
raise IOError(
f'golden_images" directory was not found at path "{golden_images_dir}"'
f'Please add a "golden_images" directory inside: "{current_file_directory}"'
)
return golden_images_dir
def create_screenshots_archive(screenshot_path):
"""
Creates a new zip file archive at archive_path containing all files listed within archive_path.
:param screenshot_path: location containing the files to archive, the zip archive file will also be saved here.
:return: None, but creates a new zip file archive inside path containing all of the files inside archive_path.
"""
files_to_archive = []
# Search for .png and .ppm files to add to the zip archive file.
for (folder_name, sub_folders, file_names) in os.walk(screenshot_path):
for file_name in file_names:
if file_name.endswith(".png") or file_name.endswith(".ppm"):
file_path = os.path.join(folder_name, file_name)
files_to_archive.append(file_path)
# Setup variables for naming the zip archive file.
timestamp = datetime.datetime.now().timestamp()
formatted_timestamp = datetime.datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d_%H-%M-%S")
screenshots_file = os.path.join(screenshot_path, f'zip_archive_{formatted_timestamp}.zip')
# Write all of the valid .png and .ppm files to the archive file.
with zipfile.ZipFile(screenshots_file, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=True) as zip_archive:
for file_path in files_to_archive:
file_name = os.path.basename(file_path)
zip_archive.write(file_path, file_name)
logger = logging.getLogger(__name__)
@pytest.mark.parametrize("project", ["AutomatedTesting"])
@pytest.mark.parametrize("launcher_platform", ["windows_editor"])
@pytest.mark.parametrize("level", ["Base"])
class TestAllComponentsIndepthTests(object):
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
class TestAutomation(EditorTestSuite):
# Remove -autotest_mode from global_extra_cmdline_args since we need rendering for these tests.
global_extra_cmdline_args = ["-BatchMode"] # Default is ["-BatchMode", "-autotest_mode"]
enable_prefab_system = False
@pytest.mark.parametrize("screenshot_name", ["AtomBasicLevelSetup.ppm"])
@pytest.mark.test_case_id("C34603773")
def test_BasicLevelSetup_SetsUpLevel(
self, request, editor, workspace, project, launcher_platform, level, screenshot_name):
"""
Please review the hydra script run by this test for more specific test info.
Tests that a basic rendering level setup can be created (lighting, meshes, materials, etc.).
"""
class AtomGPU_BasicLevelSetup_SetsUpLevel(EditorSharedTest):
use_null_renderer = False # Default is True
screenshot_name = "AtomBasicLevelSetup.ppm"
test_screenshots = [] # Gets set by setup()
screenshot_directory = "" # Gets set by setup()
# Clear existing test screenshots before starting test.
screenshot_directory = os.path.join(workspace.paths.project(), DEFAULT_SUBFOLDER_PATH)
test_screenshots = [os.path.join(screenshot_directory, screenshot_name)]
file_system.delete(test_screenshots, True, True)
def setup(self, workspace):
screenshot_directory = os.path.join(workspace.paths.project(), DEFAULT_SUBFOLDER_PATH)
test_screenshots = [os.path.join(screenshot_directory, self.screenshot_name)]
file_system.delete(test_screenshots, True, True)
golden_images = [os.path.join(golden_images_directory(), screenshot_name)]
level_creation_expected_lines = [
"Viewport is set to the expected size: True",
"Exited game mode"
]
unexpected_lines = ["Traceback (most recent call last):"]
from Atom.tests import hydra_AtomGPU_BasicLevelSetup as test_module
hydra.launch_and_validate_results(
request,
TEST_DIRECTORY,
editor,
"hydra_GPUTest_BasicLevelSetup.py",
timeout=180,
expected_lines=level_creation_expected_lines,
unexpected_lines=unexpected_lines,
halt_on_unexpected=True,
cfg_args=[level],
null_renderer=False,
enable_prefab_system=False,
)
similarity_threshold = 0.99
for test_screenshot, golden_image in zip(test_screenshots, golden_images):
screenshot_comparison_result = compare_screenshot_similarity(
test_screenshot, golden_image, similarity_threshold, True, screenshot_directory)
if screenshot_comparison_result != "Screenshots match":
raise Exception(f"Screenshot test failed: {screenshot_comparison_result}")
assert compare_screenshot_to_golden_image(screenshot_directory, test_screenshots, golden_images, 0.99) is True
@pytest.mark.test_case_id("C34525095")
def test_LightComponent_ScreenshotMatchesGoldenImage(
self, request, editor, workspace, project, launcher_platform, level):
"""
Please review the hydra script run by this test for more specific test info.
Tests that the Light component screenshots in a rendered level appear the same as the golden images.
"""
class AtomGPU_LightComponent_AreaLightScreenshotsMatchGoldenImages(EditorSharedTest):
use_null_renderer = False # Default is True
screenshot_names = [
"AreaLight_1.ppm",
"AreaLight_2.ppm",
"AreaLight_3.ppm",
"AreaLight_4.ppm",
"AreaLight_5.ppm",
]
test_screenshots = [] # Gets set by setup()
screenshot_directory = "" # Gets set by setup()
# Clear existing test screenshots before starting test.
def setup(self, workspace):
screenshot_directory = os.path.join(workspace.paths.project(), DEFAULT_SUBFOLDER_PATH)
for screenshot in self.screenshot_names:
screenshot_path = os.path.join(screenshot_directory, screenshot)
self.test_screenshots.append(screenshot_path)
file_system.delete(self.test_screenshots, True, True)
golden_images = []
for golden_image in screenshot_names:
golden_image_path = os.path.join(golden_images_directory(), golden_image)
golden_images.append(golden_image_path)
from Atom.tests import hydra_AtomGPU_AreaLightScreenshotTest as test_module
assert compare_screenshot_to_golden_image(screenshot_directory, test_screenshots, golden_images, 0.99) is True
@pytest.mark.test_case_id("C34525110")
class AtomGPU_LightComponent_SpotLightScreenshotsMatchGoldenImages(EditorSharedTest):
use_null_renderer = False # Default is True
screenshot_names = [
"SpotLight_1.ppm",
"SpotLight_2.ppm",
"SpotLight_3.ppm",
@@ -134,40 +87,25 @@ class TestAllComponentsIndepthTests(object):
"SpotLight_5.ppm",
"SpotLight_6.ppm",
]
screenshot_directory = os.path.join(workspace.paths.project(), DEFAULT_SUBFOLDER_PATH)
test_screenshots = []
for screenshot in screenshot_names:
screenshot_path = os.path.join(screenshot_directory, screenshot)
test_screenshots.append(screenshot_path)
file_system.delete(test_screenshots, True, True)
test_screenshots = [] # Gets set by setup()
screenshot_directory = "" # Gets set by setup()
# Clear existing test screenshots before starting test.
def setup(self, workspace):
screenshot_directory = os.path.join(workspace.paths.project(), DEFAULT_SUBFOLDER_PATH)
for screenshot in self.screenshot_names:
screenshot_path = os.path.join(screenshot_directory, screenshot)
self.test_screenshots.append(screenshot_path)
file_system.delete(self.test_screenshots, True, True)
golden_images = []
for golden_image in screenshot_names:
golden_image_path = os.path.join(golden_images_directory(), golden_image)
golden_images.append(golden_image_path)
expected_lines = ["spot_light Controller|Configuration|Shadows|Shadowmap size: SUCCESS"]
unexpected_lines = ["Traceback (most recent call last):"]
hydra.launch_and_validate_results(
request,
TEST_DIRECTORY,
editor,
"hydra_GPUTest_LightComponent.py",
timeout=180,
expected_lines=expected_lines,
unexpected_lines=unexpected_lines,
halt_on_unexpected=True,
cfg_args=[level],
null_renderer=False,
enable_prefab_system=False,
)
from Atom.tests import hydra_AtomGPU_SpotLightScreenshotTest as test_module
similarity_threshold = 0.99
for test_screenshot, golden_image in zip(test_screenshots, golden_images):
screenshot_comparison_result = compare_screenshot_similarity(
test_screenshot, golden_image, similarity_threshold, True, screenshot_directory)
if screenshot_comparison_result != "Screenshots match":
raise ImageComparisonTestFailure(f"Screenshot test failed: {screenshot_comparison_result}")
assert compare_screenshot_to_golden_image(screenshot_directory, test_screenshots, golden_images, 0.99) is True
@pytest.mark.parametrize('rhi', ['dx12', 'vulkan'])
@@ -198,7 +136,7 @@ class TestPerformanceBenchmarkSuite(object):
hydra.launch_and_validate_results(
request,
TEST_DIRECTORY,
os.path.join(os.path.dirname(__file__), "tests"),
editor,
"hydra_GPUTest_AtomFeatureIntegrationBenchmark.py",
timeout=600,
@@ -235,7 +173,7 @@ class TestMaterialEditor(object):
hydra.launch_and_validate_results(
request,
TEST_DIRECTORY,
os.path.join(os.path.dirname(__file__), "tests"),
generic_launcher,
editor_script="",
run_python="--runpython",
@@ -1,46 +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
"""
import os
import pytest
import ly_test_tools.environment.file_system as file_system
from ly_test_tools.o3de.editor_test import EditorSharedTest, EditorTestSuite
from ly_test_tools.image.screenshot_compare_qssim import qssim as compare_screenshots
from .atom_utils.atom_component_helper import create_screenshots_archive, golden_images_directory
DEFAULT_SUBFOLDER_PATH = 'user/PythonTests/Automated/Screenshots'
@pytest.mark.xfail(reason="Optimized tests are experimental, we will enable xfail and monitor them temporarily.")
@pytest.mark.parametrize("project", ["AutomatedTesting"])
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
class TestAutomation(EditorTestSuite):
# Remove -autotest_mode from global_extra_cmdline_args since we need rendering for these tests.
global_extra_cmdline_args = ["-BatchMode"] # Default is ["-BatchMode", "-autotest_mode"]
enable_prefab_system = False
@pytest.mark.test_case_id("C34603773")
class AtomGPU_BasicLevelSetup_SetsUpLevel(EditorSharedTest):
use_null_renderer = False # Default is True
screenshot_name = "AtomBasicLevelSetup.ppm"
test_screenshots = [] # Gets set by setup()
screenshot_directory = "" # Gets set by setup()
# Clear existing test screenshots before starting test.
def setup(self, workspace):
screenshot_directory = os.path.join(workspace.paths.project(), DEFAULT_SUBFOLDER_PATH)
test_screenshots = [os.path.join(screenshot_directory, self.screenshot_name)]
file_system.delete(test_screenshots, True, True)
from Atom.tests import hydra_AtomGPU_BasicLevelSetup as test_module
golden_images = [os.path.join(golden_images_directory(), screenshot_name)]
for test_screenshot, golden_screenshot in zip(test_screenshots, golden_images):
compare_screenshots(test_screenshot, golden_screenshot)
create_screenshots_archive(screenshot_directory)
@@ -12,19 +12,13 @@ import pytest
import ly_test_tools.environment.file_system as file_system
import editor_python_test_tools.hydra_test_utils as hydra
from ly_test_tools.o3de.editor_test import EditorSharedTest, EditorTestSuite
from Atom.atom_utils.atom_constants import LIGHT_TYPES
logger = logging.getLogger(__name__)
TEST_DIRECTORY = os.path.join(os.path.dirname(__file__), "tests")
class TestAtomEditorComponentsSandbox(object):
# It requires at least one test
def test_Dummy(self, request, editor, level, workspace, project, launcher_platform):
pass
@pytest.mark.parametrize("project", ["AutomatedTesting"])
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("level", ["auto_test"])
@@ -90,72 +84,22 @@ class TestAtomEditorComponentsMain(object):
@pytest.mark.parametrize("project", ["AutomatedTesting"])
@pytest.mark.parametrize("launcher_platform", ['windows_generic'])
@pytest.mark.system
class TestMaterialEditorBasicTests(object):
@pytest.fixture(autouse=True)
def setup_teardown(self, request, workspace, project):
def delete_files():
file_system.delete(
[
os.path.join(workspace.paths.project(), "Materials", "test_material.material"),
os.path.join(workspace.paths.project(), "Materials", "test_material_1.material"),
os.path.join(workspace.paths.project(), "Materials", "test_material_2.material"),
],
True,
True,
)
# Cleanup our newly created materials
delete_files()
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
class TestAutomation(EditorTestSuite):
def teardown():
# Cleanup our newly created materials
delete_files()
enable_prefab_system = False
request.addfinalizer(teardown)
#this test is intermittently timing out without ever having executed. sandboxing while we investigate cause.
@pytest.mark.test_case_id("C36525660")
class AtomEditorComponents_DisplayMapperAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DisplayMapperAdded as test_module
@pytest.mark.parametrize("exe_file_name", ["MaterialEditor"])
@pytest.mark.test_case_id("C34448113") # Creating a New Asset.
@pytest.mark.test_case_id("C34448114") # Opening an Existing Asset.
@pytest.mark.test_case_id("C34448115") # Closing Selected Material.
@pytest.mark.test_case_id("C34448116") # Closing All Materials.
@pytest.mark.test_case_id("C34448117") # Closing all but Selected Material.
@pytest.mark.test_case_id("C34448118") # Saving Material.
@pytest.mark.test_case_id("C34448119") # Saving as a New Material.
@pytest.mark.test_case_id("C34448120") # Saving as a Child Material.
@pytest.mark.test_case_id("C34448121") # Saving all Open Materials.
def test_MaterialEditorBasicTests(
self, request, workspace, project, launcher_platform, generic_launcher, exe_file_name):
expected_lines = [
"Material opened: True",
"Test asset doesn't exist initially: True",
"New asset created: True",
"New Material opened: True",
"Material closed: True",
"All documents closed: True",
"Close All Except Selected worked as expected: True",
"Actual Document saved with changes: True",
"Document saved as copy is saved with changes: True",
"Document saved as child is saved with changes: True",
"Save All worked as expected: True",
]
unexpected_lines = [
"Traceback (most recent call last):"
]
hydra.launch_and_validate_results(
request,
TEST_DIRECTORY,
generic_launcher,
"hydra_AtomMaterialEditor_BasicTests.py",
run_python="--runpython",
timeout=120,
expected_lines=expected_lines,
unexpected_lines=unexpected_lines,
halt_on_unexpected=True,
null_renderer=True,
log_file_name="MaterialEditor.log",
enable_prefab_system=False,
)
# this test causes editor to crash when using slices. once automation transitions to prefabs it should pass
@pytest.mark.test_case_id("C36529666")
class AtomEditorComponentsLevel_DiffuseGlobalIlluminationAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponentsLevel_DiffuseGlobalIlluminationAdded as test_module
# this test causes editor to crash when using slices. once automation transitions to prefabs it should pass
@pytest.mark.test_case_id("C36525660")
class AtomEditorComponentsLevel_DisplayMapperAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponentsLevel_DisplayMapperAdded as test_module
@@ -93,161 +93,165 @@ def compare_screenshot_similarity(
return result
def create_basic_atom_level(level_name):
def compare_screenshot_to_golden_image(
screenshot_directory, test_screenshots, golden_images, similarity_threshold=0.99):
"""
Creates a new level inside the Editor matching level_name & adds the following:
1. "default_level" entity to hold all other entities.
2. Adds Grid, Global Skylight (IBL), ground Mesh, Directional Light, Sphere w/ material+mesh, & Camera components.
3. Each of these components has its settings tweaked slightly to match the ideal scene to test Atom rendering.
:param level_name: name of the level to create and apply this basic setup to.
Compares a list of test_screenshots to a list of golden_images and return True if they match within the
similarity threshold set. Otherwise, it will raise ImageComparisonTestFailure with a failure message.
:param screenshot_directory: path to the directory containing screenshots for creating .zip archives.
:param test_screenshots: list of test screenshot path strings.
:param golden_images: list of golden image path strings.
:param similarity_threshold: float threshold tolerance to set when comparing screenshots to golden images.
"""
for test_screenshot, golden_image in zip(test_screenshots, golden_images):
screenshot_comparison_result = compare_screenshot_similarity(
test_screenshot, golden_image, similarity_threshold, True, screenshot_directory)
if screenshot_comparison_result != "Screenshots match":
raise ImageComparisonTestFailure(f"Screenshot test failed: {screenshot_comparison_result}")
return True
def initial_viewport_setup(screen_width=1280, screen_height=720):
"""
For setting up the initial viewport resolution to expected default values before running a screenshot test.
Defaults to 1280 x 720 resolution (in pixels).
:param screen_width: Width in pixels to set the viewport width size to.
:param screen_height: Height in pixels to set the viewport height size to.
:return: None
"""
import azlmbr.asset as asset
import azlmbr.bus as bus
import azlmbr.camera as camera
import azlmbr.editor as editor
import azlmbr.entity as entity
import azlmbr.legacy.general as general
import azlmbr.math as math
import azlmbr.object
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.editor_test_helper import EditorTestHelper
helper = EditorTestHelper(log_prefix="Atom_EditorTestHelper")
# Wait for Editor idle loop before executing Python hydra scripts.
general.idle_enable(True)
# Basic setup for opened level.
helper.open_level(level_name="Base")
general.idle_wait(1.0)
general.update_viewport()
general.idle_wait(0.5) # half a second is more than enough for updating the viewport.
# Close out problematic windows, FPS meters, and anti-aliasing.
if general.is_helpers_shown(): # Turn off the helper gizmos if visible
general.toggle_helpers()
general.idle_wait(1.0)
if general.is_pane_visible("Error Report"): # Close Error Report windows that block focus.
general.close_pane("Error Report")
if general.is_pane_visible("Error Log"): # Close Error Log windows that block focus.
general.close_pane("Error Log")
general.idle_wait(1.0)
general.run_console("r_displayInfo=0")
general.idle_wait(1.0)
# Delete all existing entities & create default_level entity
search_filter = azlmbr.entity.SearchFilter()
all_entities = entity.SearchBus(azlmbr.bus.Broadcast, "SearchEntities", search_filter)
editor.ToolsApplicationRequestBus(bus.Broadcast, "DeleteEntities", all_entities)
default_level = hydra.Entity("default_level")
default_position = math.Vector3(0.0, 0.0, 0.0)
default_level.create_entity(default_position, ["Grid"])
default_level.get_set_test(0, "Controller|Configuration|Secondary Grid Spacing", 1.0)
# Set the viewport up correctly after adding the parent default_level entity.
screen_width = 1280
screen_height = 720
degree_radian_factor = 0.0174533 # Used by "Rotation" property for the Transform component.
general.set_viewport_size(screen_width, screen_height)
general.update_viewport()
helper.wait_for_condition(
function=lambda: helper.isclose(a=general.get_viewport_size().x, b=screen_width, rel_tol=0.1)
and helper.isclose(a=general.get_viewport_size().y, b=screen_height, rel_tol=0.1),
timeout_in_seconds=4.0
)
result = helper.isclose(a=general.get_viewport_size().x, b=screen_width, rel_tol=0.1) and helper.isclose(
a=general.get_viewport_size().y, b=screen_height, rel_tol=0.1)
general.log(general.get_viewport_size().x)
general.log(general.get_viewport_size().y)
general.log(general.get_viewport_size().z)
general.log(f"Viewport is set to the expected size: {result}")
general.log("Basic level created")
general.run_console("r_DisplayInfo = 0")
# Create global_skylight entity and set the properties
global_skylight = hydra.Entity("global_skylight")
global_skylight.create_entity(
entity_position=default_position,
components=["HDRi Skybox", "Global Skylight (IBL)"],
parent_id=default_level.id)
global_skylight_asset_path = os.path.join("LightingPresets", "default_iblskyboxcm.exr.streamingimage")
global_skylight_asset_value = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", global_skylight_asset_path, math.Uuid(), False)
global_skylight.get_set_test(0, "Controller|Configuration|Cubemap Texture", global_skylight_asset_value)
global_skylight.get_set_test(1, "Controller|Configuration|Diffuse Image", global_skylight_asset_value)
global_skylight.get_set_test(1, "Controller|Configuration|Specular Image", global_skylight_asset_value)
# Create ground_plane entity and set the properties
ground_plane = hydra.Entity("ground_plane")
ground_plane.create_entity(
entity_position=default_position,
components=["Material"],
parent_id=default_level.id)
azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalUniformScale", ground_plane.id, 32.0)
def enter_exit_game_mode_take_screenshot(screenshot_name, enter_game_tuple, exit_game_tuple, timeout_in_seconds=4):
"""
Enters game mode, takes a screenshot named screenshot_name (must include file extension), and exits game mode.
:param screenshot_name: string representing the name of the screenshot file, including file extension.
:param enter_game_tuple: tuple where the 1st string is success & 2nd string is failure for entering the game.
:param exit_game_tuple: tuple where the 1st string is success & 2nd string is failure for exiting the game.
:param timeout_in_seconds: int or float seconds to wait for entering/exiting game mode.
:return: None
"""
import azlmbr.legacy.general as general
# Work around to add the correct Atom Mesh component and asset.
mesh_type_id = azlmbr.globals.property.EditorMeshComponentTypeId
ground_plane.components.append(
editor.EditorComponentAPIBus(
bus.Broadcast, "AddComponentsOfType", ground_plane.id, [mesh_type_id]
).GetValue()[0]
)
from editor_python_test_tools.utils import TestHelper
from Atom.atom_utils.screenshot_utils import ScreenshotHelper
TestHelper.enter_game_mode(enter_game_tuple)
TestHelper.wait_for_condition(function=lambda: general.is_in_game_mode(), timeout_in_seconds=timeout_in_seconds)
ScreenshotHelper(general.idle_wait_frames).capture_screenshot_blocking(screenshot_name)
TestHelper.exit_game_mode(exit_game_tuple)
TestHelper.wait_for_condition(function=lambda: not general.is_in_game_mode(), timeout_in_seconds=timeout_in_seconds)
def create_basic_atom_rendering_scene():
"""
Sets up a new scene inside the Editor for testing Atom rendering GPU output.
Setup: Deletes all existing entities before creating the scene.
The created scene includes:
1. "Default Level" entity that holds all of the other entities.
2. "Grid" entity: Contains a Grid component.
3. "Global Skylight (IBL)" entity: Contains HDRI Skybox & Global Skylight (IBL) components.
4. "Ground Plane" entity: Contains Material & Mesh components.
5. "Directional Light" entity: Contains Directional Light component.
6. "Sphere" entity: Contains Material & Mesh components.
7. "Camera" entity: Contains Camera component.
:return: None
"""
import azlmbr.math as math
import azlmbr.paths
from editor_python_test_tools.asset_utils import Asset
from editor_python_test_tools.editor_entity_utils import EditorEntity
from Atom.atom_utils.atom_constants import AtomComponentProperties
DEGREE_RADIAN_FACTOR = 0.0174533
# Setup: Deletes all existing entities before creating the scene.
search_filter = azlmbr.entity.SearchFilter()
all_entities = azlmbr.entity.SearchBus(azlmbr.bus.Broadcast, "SearchEntities", search_filter)
azlmbr.editor.ToolsApplicationRequestBus(azlmbr.bus.Broadcast, "DeleteEntities", all_entities)
# 1. "Default Level" entity that holds all of the other entities.
default_level_entity_name = "Default Level"
default_level_entity = EditorEntity.create_editor_entity_at(math.Vector3(0.0, 0.0, 0.0), default_level_entity_name)
# 2. "Grid" entity: Contains a Grid component.
grid_entity = EditorEntity.create_editor_entity(AtomComponentProperties.grid(), default_level_entity.id)
grid_component = grid_entity.add_component(AtomComponentProperties.grid())
secondary_grid_spacing_value = 1.0
grid_component.set_component_property_value(
AtomComponentProperties.grid('Secondary Grid Spacing'), secondary_grid_spacing_value)
# 3. "Global Skylight (IBL)" entity: Contains HDRI Skybox & Global Skylight (IBL) components.
global_skylight_entity = EditorEntity.create_editor_entity(
AtomComponentProperties.global_skylight(), default_level_entity.id)
hdri_skybox_component = global_skylight_entity.add_component(AtomComponentProperties.hdri_skybox())
global_skylight_component = global_skylight_entity.add_component(AtomComponentProperties.global_skylight())
global_skylight_image_asset_path = os.path.join("LightingPresets", "default_iblskyboxcm.exr.streamingimage")
global_skylight_image_asset = Asset.find_asset_by_path(global_skylight_image_asset_path, False)
hdri_skybox_component.set_component_property_value(
AtomComponentProperties.hdri_skybox('Cubemap Texture'), global_skylight_image_asset.id)
global_skylight_diffuse_image_asset_path = os.path.join(
"LightingPresets", "default_iblskyboxcm_ibldiffuse.exr.streamingimage")
global_skylight_diffuse_image_asset = Asset.find_asset_by_path(global_skylight_diffuse_image_asset_path, False)
global_skylight_component.set_component_property_value(
AtomComponentProperties.global_skylight('Diffuse Image'), global_skylight_diffuse_image_asset.id)
global_skylight_specular_image_asset_path = os.path.join(
"LightingPresets", "default_iblskyboxcm_iblspecular.exr.streamingimage")
global_skylight_specular_image_asset = Asset.find_asset_by_path(
global_skylight_specular_image_asset_path, False)
global_skylight_component.set_component_property_value(
AtomComponentProperties.global_skylight('Specular Image'), global_skylight_specular_image_asset.id)
# 4. "Ground Plane" entity: Contains Material & Mesh components.
ground_plane_name = "Ground Plane"
ground_plane_entity = EditorEntity.create_editor_entity(ground_plane_name, default_level_entity.id)
ground_plane_material_component = ground_plane_entity.add_component(AtomComponentProperties.material())
ground_plane_entity.set_local_uniform_scale(32.0)
ground_plane_mesh_component = ground_plane_entity.add_component(AtomComponentProperties.mesh())
ground_plane_mesh_asset_path = os.path.join("TestData", "Objects", "plane.azmodel")
ground_plane_mesh_asset_value = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", ground_plane_mesh_asset_path, math.Uuid(), False)
ground_plane.get_set_test(1, "Controller|Configuration|Mesh Asset", ground_plane_mesh_asset_value)
# Add Atom Material component and asset.
ground_plane_mesh_asset = Asset.find_asset_by_path(ground_plane_mesh_asset_path, False)
ground_plane_mesh_component.set_component_property_value(
AtomComponentProperties.mesh('Mesh Asset'), ground_plane_mesh_asset.id)
ground_plane_material_asset_path = os.path.join("Materials", "Presets", "PBR", "metal_chrome.azmaterial")
ground_plane_material_asset_value = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", ground_plane_material_asset_path, math.Uuid(), False)
ground_plane.get_set_test(0, "Default Material|Material Asset", ground_plane_material_asset_value)
ground_plane_material_asset = Asset.find_asset_by_path(ground_plane_material_asset_path, False)
ground_plane_material_component.set_component_property_value(
AtomComponentProperties.material('Material Asset'), ground_plane_material_asset.id)
# Create directional_light entity and set the properties
directional_light = hydra.Entity("directional_light")
directional_light.create_entity(
entity_position=math.Vector3(0.0, 0.0, 10.0),
components=["Directional Light"],
parent_id=default_level.id)
directional_light_rotation = math.Vector3(degree_radian_factor * -90.0, 0.0, 0.0)
azlmbr.components.TransformBus(
azlmbr.bus.Event, "SetLocalRotation", directional_light.id, directional_light_rotation)
# 5. "Directional Light" entity: Contains Directional Light component.
directional_light_entity = EditorEntity.create_editor_entity_at(
math.Vector3(0.0, 0.0, 10.0), AtomComponentProperties.directional_light(), default_level_entity.id)
directional_light_entity.add_component(AtomComponentProperties.directional_light())
directional_light_entity_rotation = math.Vector3(DEGREE_RADIAN_FACTOR * -90.0, 0.0, 0.0)
directional_light_entity.set_local_rotation(directional_light_entity_rotation)
# Create sphere entity and set the properties
sphere_entity = hydra.Entity("sphere")
sphere_entity.create_entity(
entity_position=math.Vector3(0.0, 0.0, 1.0),
components=["Material"],
parent_id=default_level.id)
# Work around to add the correct Atom Mesh component and asset.
sphere_entity.components.append(
editor.EditorComponentAPIBus(
bus.Broadcast, "AddComponentsOfType", sphere_entity.id, [mesh_type_id]
).GetValue()[0]
)
# 6. "Sphere" entity: Contains Material & Mesh components.
sphere_entity = EditorEntity.create_editor_entity_at(
math.Vector3(0.0, 0.0, 1.0), "Sphere", default_level_entity.id)
sphere_mesh_component = sphere_entity.add_component(AtomComponentProperties.mesh())
sphere_mesh_asset_path = os.path.join("Models", "sphere.azmodel")
sphere_mesh_asset_value = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", sphere_mesh_asset_path, math.Uuid(), False)
sphere_entity.get_set_test(1, "Controller|Configuration|Mesh Asset", sphere_mesh_asset_value)
# Add Atom Material component and asset.
sphere_mesh_asset = Asset.find_asset_by_path(sphere_mesh_asset_path, False)
sphere_mesh_component.set_component_property_value(
AtomComponentProperties.mesh('Mesh Asset'), sphere_mesh_asset.id)
sphere_material_component = sphere_entity.add_component(AtomComponentProperties.material())
sphere_material_asset_path = os.path.join("Materials", "Presets", "PBR", "metal_brass_polished.azmaterial")
sphere_material_asset_value = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", sphere_material_asset_path, math.Uuid(), False)
sphere_entity.get_set_test(0, "Default Material|Material Asset", sphere_material_asset_value)
sphere_material_asset = Asset.find_asset_by_path(sphere_material_asset_path, False)
sphere_material_component.set_component_property_value(
AtomComponentProperties.material('Material Asset'), sphere_material_asset.id)
# Create camera component and set the properties
camera_entity = hydra.Entity("camera")
camera_entity.create_entity(
entity_position=math.Vector3(5.5, -12.0, 9.0),
components=["Camera"],
parent_id=default_level.id)
rotation = math.Vector3(
degree_radian_factor * -27.0, degree_radian_factor * -12.0, degree_radian_factor * 25.0
)
azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalRotation", camera_entity.id, rotation)
camera_entity.get_set_test(0, "Controller|Configuration|Field of view", 60.0)
camera.EditorCameraViewRequestBus(azlmbr.bus.Event, "ToggleCameraAsActiveView", camera_entity.id)
# 7. "Camera" entity: Contains Camera component.
camera_entity = EditorEntity.create_editor_entity_at(
math.Vector3(5.5, -12.0, 9.0), AtomComponentProperties.camera(), default_level_entity.id)
camera_component = camera_entity.add_component(AtomComponentProperties.camera())
camera_entity_rotation = math.Vector3(
DEGREE_RADIAN_FACTOR * -27.0, DEGREE_RADIAN_FACTOR * -12.0, DEGREE_RADIAN_FACTOR * 25.0)
camera_entity.set_local_rotation(camera_entity_rotation)
camera_fov_value = 60.0
camera_component.set_component_property_value(AtomComponentProperties.camera('Field of view'), camera_fov_value)
azlmbr.camera.EditorCameraViewRequestBus(azlmbr.bus.Event, "ToggleCameraAsActiveView", camera_entity.id)
@@ -19,6 +19,20 @@ LIGHT_TYPES = {
}
# Attenuation Radius Mode options for the Light component.
ATTENUATION_RADIUS_MODE = {
'automatic': 1,
'explicit': 0,
}
# Qualiity Level settings for Diffuse Global Illumination level component
GLOBAL_ILLUMINATION_QUALITY = {
'Low': 0,
'Medium': 1,
'High': 2,
}
class AtomComponentProperties:
"""
Holds Atom component related constants
@@ -116,6 +130,21 @@ class AtomComponentProperties:
}
return properties[property]
@staticmethod
def diffuse_global_illumination(property: str = 'name') -> str:
"""
Diffuse Global Illumination level component properties.
Controls global settings for Diffuse Probe Grid components.
- 'Quality Level' from atom_constants.py GLOBAL_ILLUMINATION_QUALITY
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Diffuse Global Illumination',
'Quality Level': 'Controller|Configuration|Quality Level'
}
return properties[property]
@staticmethod
def diffuse_probe_grid(property: str = 'name') -> str:
"""
@@ -148,7 +177,8 @@ class AtomComponentProperties:
@staticmethod
def display_mapper(property: str = 'name') -> str:
"""
Display Mapper component properties.
Display Mapper level component properties.
- 'Enable LDR color grading LUT' toggles the use of LDR color grading LUT
- 'LDR color Grading LUT' is the Low Definition Range (LDR) color grading for Look-up Textures (LUT) which is
an Asset.id value corresponding to a lighting asset file.
:param property: From the last element of the property tree path. Default 'name' for component name string.
@@ -156,6 +186,7 @@ class AtomComponentProperties:
"""
properties = {
'name': 'Display Mapper',
'Enable LDR color grading LUT': 'Controller|Configuration|Enable LDR color grading LUT',
'LDR color Grading LUT': 'Controller|Configuration|LDR color Grading LUT',
}
return properties[property]
@@ -252,13 +283,27 @@ class AtomComponentProperties:
def light(property: str = 'name') -> str:
"""
Light component properties.
- 'Attenuation Radius Mode' controls whether the attenuation radius is calculated automatically or explicitly.
- 'Color' the RGB value to set for the color of the light.
- 'Enable shadow' toggle for enabling shadows for the light.
- 'Enable shutters' toggle for enabling shutters for the light.
- 'Inner angle' inner angle value for the shutters (in degrees)
- 'Intensity' the intensity of the light in the set photometric unit (float with no ceiling).
- 'Light type' from atom_constants.py LIGHT_TYPES
- 'Outer angle' outer angle value for the shutters (in degrees)
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Light',
'Attenuation Radius Mode': 'Controller|Configuration|Attenuation radius|Mode',
'Color': 'Controller|Configuration|Color',
'Enable shadow': 'Controller|Configuration|Shadows|Enable shadow',
'Enable shutters': 'Controller|Configuration|Shutters|Enable shutters',
'Inner angle': 'Controller|Configuration|Shutters|Inner angle',
'Intensity': 'Controller|Configuration|Intensity',
'Light type': 'Controller|Configuration|Light type',
'Outer angle': 'Controller|Configuration|Shutters|Outer angle',
}
return properties[property]
@@ -162,6 +162,13 @@ def select_model_config(configname):
azlmbr.materialeditor.MaterialViewportRequestBus(azlmbr.bus.Broadcast, "SelectModelPresetByName", configname)
def destroy_main_window():
"""
Closes the Material Editor window
"""
azlmbr.atomtools.AtomToolsMainWindowFactoryRequestBus(azlmbr.bus.Broadcast, "DestroyMainWindow")
def wait_for_condition(function, timeout_in_seconds=1.0):
# type: (function, float) -> bool
"""
@@ -0,0 +1,109 @@
"""
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
"""
class Tests:
creation_undo = (
"UNDO Level component addition success",
"UNDO Level component addition failed")
creation_redo = (
"REDO Level component addition success",
"REDO Level component addition failed")
diffuse_global_illumination_component = (
"Level has a Diffuse Global Illumination component",
"Level failed to find Diffuse Global Illumination component")
diffuse_global_illumination_quality = (
"Quality Level set",
"Quality Level could not be set")
enter_game_mode = (
"Entered game mode",
"Failed to enter game mode")
exit_game_mode = (
"Exited game mode",
"Couldn't exit game mode")
def AtomEditorComponentsLevel_DiffuseGlobalIllumination_AddedToEntity():
"""
Summary:
Tests the Diffuse Global Illumination level component can be added to the level entity and is stable.
Test setup:
- Wait for Editor idle loop.
- Open the "Base" level.
Expected Behavior:
The component can be added, used in game mode, and has accurate required components.
Creation and deletion undo/redo should also work.
Test Steps:
1) Add Diffuse Global Illumination level component to the level entity.
2) UNDO the level component addition.
3) REDO the level component addition.
4) Set Quality Level property to Low
5) Enter/Exit game mode.
6) Look for errors and asserts.
:return: None
"""
import azlmbr.legacy.general as general
from editor_python_test_tools.editor_entity_utils import EditorLevelEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties, GLOBAL_ILLUMINATION_QUALITY
with Tracer() as error_tracer:
# Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
TestHelper.init_idle()
TestHelper.open_level("", "Base")
# Test steps begin.
# 1. Add Diffuse Global Illumination level component to the level entity.
diffuse_global_illumination_component = EditorLevelEntity.add_component(
AtomComponentProperties.diffuse_global_illumination())
Report.critical_result(
Tests.diffuse_global_illumination_component,
EditorLevelEntity.has_component(AtomComponentProperties.diffuse_global_illumination()))
# 2. UNDO the level component addition.
# -> UNDO component addition.
general.undo()
general.idle_wait_frames(1)
Report.result(Tests.creation_undo,
not EditorLevelEntity.has_component(AtomComponentProperties.diffuse_global_illumination()))
# 3. REDO the level component addition.
# -> REDO component addition.
general.redo()
general.idle_wait_frames(1)
Report.result(Tests.creation_redo,
EditorLevelEntity.has_component(AtomComponentProperties.diffuse_global_illumination()))
# 4. Set Quality Level property to Low
diffuse_global_illumination_component.set_component_property_value(
AtomComponentProperties.diffuse_global_illumination('Quality Level', GLOBAL_ILLUMINATION_QUALITY['Low']))
quality = diffuse_global_illumination_component.get_component_property_value(
AtomComponentProperties.diffuse_global_illumination('Quality Level'))
Report.result(diffuse_global_illumination_quality, quality == GLOBAL_ILLUMINATION_QUALITY['Low'])
# 5. Enter/Exit game mode.
TestHelper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
TestHelper.exit_game_mode(Tests.exit_game_mode)
# 6. Look for errors and asserts.
TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(AtomEditorComponentsLevel_DiffuseGlobalIllumination_AddedToEntity)
@@ -0,0 +1,124 @@
"""
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
"""
class Tests:
creation_undo = (
"UNDO level component addition success",
"UNDO level component addition failed")
creation_redo = (
"REDO Level component addition success",
"REDO Level component addition failed")
display_mapper_component = (
"Level has a Display Mapper component",
"Level failed to find Display Mapper component")
ldr_color_grading_lut = (
"LDR color Grading LUT asset set",
"LDR color Grading LUT asset could not be set")
enable_ldr_color_grading_lut = (
"Enable LDR color grading LUT set",
"Enable LDR color grading LUT could not be set")
enter_game_mode = (
"Entered game mode",
"Failed to enter game mode")
exit_game_mode = (
"Exited game mode",
"Couldn't exit game mode")
def AtomEditorComponentsLevel_DisplayMapper_AddedToEntity():
"""
Summary:
Tests the Display Mapper level component can be added to the level entity and has the expected functionality.
Test setup:
- Wait for Editor idle loop.
- Open the "Base" level.
Expected Behavior:
The component can be added, used in game mode, and has accurate required components.
Creation and deletion undo/redo should also work.
Test Steps:
1) Add Display Mapper level component to the level entity.
2) UNDO the level component addition.
3) REDO the level component addition.
4) Set LDR color Grading LUT asset.
5) Set Enable LDR color grading LUT property True
6) Enter/Exit game mode.
7) Look for errors and asserts.
:return: None
"""
import os
import azlmbr.legacy.general as general
from editor_python_test_tools.asset_utils import Asset
from editor_python_test_tools.editor_entity_utils import EditorLevelEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties
with Tracer() as error_tracer:
# Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
TestHelper.init_idle()
TestHelper.open_level("", "Base")
# Test steps begin.
# 1. Add Display Mapper level component to the level entity.
display_mapper_component = EditorLevelEntity.add_component(AtomComponentProperties.display_mapper())
Report.critical_result(
Tests.display_mapper_component,
EditorLevelEntity.has_component(AtomComponentProperties.display_mapper()))
# 2. UNDO the level component addition.
# -> UNDO component addition.
general.undo()
general.idle_wait_frames(1)
Report.result(Tests.creation_undo, not EditorLevelEntity.has_component(AtomComponentProperties.display_mapper()))
# 3. REDO the level component addition.
# -> REDO component addition.
general.redo()
general.idle_wait_frames(1)
Report.result(Tests.creation_redo, EditorLevelEntity.has_component(AtomComponentProperties.display_mapper()))
# 4. Set LDR color Grading LUT asset.
display_mapper_asset_path = os.path.join("TestData", "test.lightingpreset.azasset")
display_mapper_asset = Asset.find_asset_by_path(display_mapper_asset_path, False)
display_mapper_component.set_component_property_value(
AtomComponentProperties.display_mapper('LDR color Grading LUT'), display_mapper_asset.id)
Report.result(
Tests.ldr_color_grading_lut,
display_mapper_component.get_component_property_value(
AtomComponentProperties.display_mapper('LDR color Grading LUT')) == display_mapper_asset.id)
# 5. Set Enable LDR color grading LUT property True
display_mapper_component.set_component_property_value(
AtomComponentProperties.display_mapper('Enable LDR color grading LUT'), True)
Report.result(
Test.enable_ldr_color_grading_lut,
display_mapper_component.get_component_property_value(
AtomComponentProperties.display_mapper('Enable LDR color grading LUT')) is True)
# 6. Enter/Exit game mode.
TestHelper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
TestHelper.exit_game_mode(Tests.exit_game_mode)
# 7. Look for errors and asserts.
TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(AtomEditorComponentsLevel_DisplayMapper_AddedToEntity)
@@ -7,15 +7,6 @@ SPDX-License-Identifier: Apache-2.0 OR MIT
class Tests:
camera_creation = (
"Camera Entity successfully created",
"Camera Entity failed to be created")
camera_component_added = (
"Camera component was added to entity",
"Camera component failed to be added to entity")
camera_component_check = (
"Entity has a Camera component",
"Entity failed to find Camera component")
creation_undo = (
"UNDO Entity creation success",
"UNDO Entity creation failed")
@@ -43,6 +34,9 @@ class Tests:
ldr_color_grading_lut = (
"LDR color Grading LUT asset set",
"LDR color Grading LUT asset could not be set")
enable_ldr_color_grading_lut = (
"Enable LDR color grading LUT set",
"Enable LDR color grading LUT could not be set")
entity_deleted = (
"Entity deleted",
"Entity was not deleted")
@@ -72,14 +66,15 @@ def AtomEditorComponents_DisplayMapper_AddedToEntity():
2) Add Display Mapper component to Display Mapper entity.
3) UNDO the entity creation and component addition.
4) REDO the entity creation and component addition.
5) Enter/Exit game mode.
6) Test IsHidden.
7) Test IsVisible.
8) Set LDR color Grading LUT asset.
9) Delete Display Mapper entity.
10) UNDO deletion.
11) REDO deletion.
12) Look for errors and asserts.
5) Set LDR color Grading LUT asset.
6) Set Enable LDR color grading LUT property True
7) Enter/Exit game mode.
8) Test IsHidden.
9) Test IsVisible.
10) Delete Display Mapper entity.
11) UNDO deletion.
12) REDO deletion.
13) Look for errors and asserts.
:return: None
"""
@@ -133,21 +128,7 @@ def AtomEditorComponents_DisplayMapper_AddedToEntity():
general.idle_wait_frames(1)
Report.result(Tests.creation_redo, display_mapper_entity.exists())
# 5. Enter/Exit game mode.
TestHelper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
TestHelper.exit_game_mode(Tests.exit_game_mode)
# 6. Test IsHidden.
display_mapper_entity.set_visibility_state(False)
Report.result(Tests.is_hidden, display_mapper_entity.is_hidden() is True)
# 7. Test IsVisible.
display_mapper_entity.set_visibility_state(True)
general.idle_wait_frames(1)
Report.result(Tests.is_visible, display_mapper_entity.is_visible() is True)
# 8. Set LDR color Grading LUT asset.
# 5. Set LDR color Grading LUT asset.
display_mapper_asset_path = os.path.join("TestData", "test.lightingpreset.azasset")
display_mapper_asset = Asset.find_asset_by_path(display_mapper_asset_path, False)
display_mapper_component.set_component_property_value(
@@ -157,19 +138,41 @@ def AtomEditorComponents_DisplayMapper_AddedToEntity():
display_mapper_component.get_component_property_value(
AtomComponentProperties.display_mapper("LDR color Grading LUT")) == display_mapper_asset.id)
# 9. Delete Display Mapper entity.
# 6. Set Enable LDR color grading LUT property True
display_mapper_component.set_component_property_value(
AtomComponentProperties.display_mapper('Enable LDR color grading LUT'), True)
Report.result(
Tests.enable_ldr_color_grading_lut,
display_mapper_component.get_component_property_value(
AtomComponentProperties.display_mapper('Enable LDR color grading LUT')) is True)
# 7. Enter/Exit game mode.
TestHelper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
TestHelper.exit_game_mode(Tests.exit_game_mode)
# 8. Test IsHidden.
display_mapper_entity.set_visibility_state(False)
Report.result(Tests.is_hidden, display_mapper_entity.is_hidden() is True)
# 9. Test IsVisible.
display_mapper_entity.set_visibility_state(True)
general.idle_wait_frames(1)
Report.result(Tests.is_visible, display_mapper_entity.is_visible() is True)
# 10. Delete Display Mapper entity.
display_mapper_entity.delete()
Report.result(Tests.entity_deleted, not display_mapper_entity.exists())
# 10. UNDO deletion.
# 11. UNDO deletion.
general.undo()
Report.result(Tests.deletion_undo, display_mapper_entity.exists())
# 11. REDO deletion.
# 12. REDO deletion.
general.redo()
Report.result(Tests.deletion_redo, not display_mapper_entity.exists())
# 12. Look for errors and asserts.
# 13. Look for errors and asserts.
TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
@@ -0,0 +1,199 @@
"""
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
"""
class Tests:
area_light_entity_created = (
"Area Light entity successfully created",
"Area Light entity failed to be created")
area_light_entity_deleted = (
"Area Light entity was deleted",
"Area Light entity was not deleted")
enter_game_mode = (
"Entered game mode",
"Failed to enter game mode")
exit_game_mode = (
"Exited game mode",
"Couldn't exit game mode")
light_component_added = (
"Light component was added",
"Light component wasn't added")
light_component_attenuation_radius_property_set = (
"Light component Attenuation Radius Property was set",
"Light component Attenuation Radius Property was not set")
light_component_color_property_set = (
"Light component Color property was set",
"Light component Color property was not set")
light_component_intensity_property_set = (
"Light component Intensity property was set",
"Light component Intensity property was not set")
light_component_light_type_property_set = (
"Light type property was set",
"Light type property was not set")
def AtomGPU_LightComponent_AreaLightScreenshotsMatchGoldenImages():
"""
Summary:
Light component test using the Capsule, Spot (disk), and Point (sphere) Light type property options.
Sets each scene up and then takes a screenshot of each scene for test comparison.
Test setup:
- Wait for Editor idle loop.
- Open the "Base" level.
- Close error windows and display helpers then update the viewport size.
- Runs the create_basic_atom_rendering_scene() function to setup the test scene.
Expected Behavior:
The test scripts sets up the scenes correctly and takes accurate screenshots.
Test Steps:
1. Create Area Light entity with no components.
2. Add a Light component to the Area Light entity.
3. Set the Light type property to Capsule for the Light component.
4. Set the Light component's Color property to 255, 0, 0.
5. Enter game mode and take a screenshot then exit game mode.
6. Set the Intensity property of the Light component to 0.0.
7. Set the Attenuation Radius Mode property of the Light component to 1 (automatic).
8. Enter game mode and take a screenshot then exit game mode.
9. Set the Intensity property of the Light component to 1000.0
10. Enter game mode and take a screenshot then exit game mode.
11. Set the Light type property to Spot (disk) for the Light component & rotate DEGREE_RADIAN_FACTOR * 90 degrees.
12. Enter game mode and take a screenshot then exit game mode.
13. Set the Light type property to Point (sphere) instead of Spot (disk) for the Light component.
14. Enter game mode and take a screenshot then exit game mode.
15. Delete the Area Light entity.
16. Look for errors.
:return: None
"""
import azlmbr.legacy.general as general
import azlmbr.paths
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties, ATTENUATION_RADIUS_MODE, LIGHT_TYPES
from Atom.atom_utils.atom_component_helper import (
initial_viewport_setup, create_basic_atom_rendering_scene, enter_exit_game_mode_take_screenshot)
DEGREE_RADIAN_FACTOR = 0.0174533
with Tracer() as error_tracer:
# Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
TestHelper.init_idle()
TestHelper.open_level("", "Base")
# Setup: Close error windows and display helpers then update the viewport size.
TestHelper.close_error_windows()
TestHelper.close_display_helpers()
initial_viewport_setup()
general.update_viewport()
# Setup: Runs the create_basic_atom_rendering_scene() function to setup the test scene.
create_basic_atom_rendering_scene()
# Test steps begin.
# 1. Create Area Light entity with no components.
area_light_entity_name = "Area Light"
area_light_entity = EditorEntity.create_editor_entity_at(
azlmbr.math.Vector3(0.0, 0.0, 0.0), area_light_entity_name)
Report.critical_result(Tests.area_light_entity_created, area_light_entity.exists())
# 2. Add a Light component to the Area Light entity.
light_component = area_light_entity.add_component(AtomComponentProperties.light())
Report.critical_result(
Tests.light_component_added, area_light_entity.has_component(AtomComponentProperties.light()))
# 3. Set the Light type property to Capsule for the Light component.
light_component.set_component_property_value(
AtomComponentProperties.light('Light type'), LIGHT_TYPES['capsule'])
Report.result(
Tests.light_component_light_type_property_set,
light_component.get_component_property_value(
AtomComponentProperties.light('Light type')) == LIGHT_TYPES['capsule'])
# 4. Set the Light component's Color property to 255, 0, 0.
light_component_color_value = azlmbr.math.Color(255.0, 0.0, 0.0, 0.0)
light_component.set_component_property_value(
AtomComponentProperties.light('Color'), light_component_color_value)
Report.result(
Tests.light_component_color_property_set,
light_component.get_component_property_value(
AtomComponentProperties.light('Color')) == light_component_color_value)
# 5. Enter game mode and take a screenshot then exit game mode.
enter_exit_game_mode_take_screenshot("AreaLight_1.ppm", Tests.enter_game_mode, Tests.exit_game_mode)
# 6. Set the Intensity property of the Light component to 0.0.
light_component.set_component_property_value(AtomComponentProperties.light('Intensity'), 0.0)
Report.result(
Tests.light_component_intensity_property_set,
light_component.get_component_property_value(AtomComponentProperties.light('Intensity')) == 0.0)
# 7. Set the Attenuation Radius Mode property of the Light component to 1 (automatic).
light_component.set_component_property_value(
AtomComponentProperties.light('Attenuation Radius Mode'), ATTENUATION_RADIUS_MODE['automatic'])
Report.result(
Tests.light_component_attenuation_radius_property_set,
light_component.get_component_property_value(
AtomComponentProperties.light('Attenuation Radius Mode')) == ATTENUATION_RADIUS_MODE['automatic'])
# 8. Enter game mode and take a screenshot then exit game mode.
enter_exit_game_mode_take_screenshot("AreaLight_2.ppm", Tests.enter_game_mode, Tests.exit_game_mode)
# 9. Set the Intensity property of the Light component to 1000.0
light_component.set_component_property_value(AtomComponentProperties.light('Intensity'), 1000.0)
Report.result(
Tests.light_component_intensity_property_set,
light_component.get_component_property_value(AtomComponentProperties.light('Intensity')) == 1000.0)
# 10. Enter game mode and take a screenshot then exit game mode.
enter_exit_game_mode_take_screenshot("AreaLight_3.ppm", Tests.enter_game_mode, Tests.exit_game_mode)
# 11. Set the Light type property to Spot (disk) for the Light component &
# rotate DEGREE_RADIAN_FACTOR * 90 degrees.
light_component.set_component_property_value(
AtomComponentProperties.light('Light type'), LIGHT_TYPES['spot_disk'])
area_light_rotation = azlmbr.math.Vector3(DEGREE_RADIAN_FACTOR * 90.0, 0.0, 0.0)
azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalRotation", area_light_entity.id, area_light_rotation)
Report.result(
Tests.light_component_light_type_property_set,
light_component.get_component_property_value(
AtomComponentProperties.light('Light type')) == LIGHT_TYPES['spot_disk'])
# 12. Enter game mode and take a screenshot then exit game mode.
enter_exit_game_mode_take_screenshot("AreaLight_4.ppm", Tests.enter_game_mode, Tests.exit_game_mode)
# 13. Set the Light type property to Point (sphere) instead of Spot (disk) for the Light component.
light_component.set_component_property_value(
AtomComponentProperties.light('Light type'), LIGHT_TYPES['sphere'])
Report.result(
Tests.light_component_light_type_property_set,
light_component.get_component_property_value(
AtomComponentProperties.light('Light type')) == LIGHT_TYPES['sphere'])
# 14. Enter game mode and take a screenshot then exit game mode.
enter_exit_game_mode_take_screenshot("AreaLight_5.ppm", Tests.enter_game_mode, Tests.exit_game_mode)
# 15. Delete the Area Light entity.
area_light_entity.delete()
Report.result(Tests.area_light_entity_deleted, not area_light_entity.exists())
# 16. Look for errors.
TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(AtomGPU_LightComponent_AreaLightScreenshotsMatchGoldenImages)
@@ -80,6 +80,7 @@ def AtomGPU_BasicLevelSetup_SetsUpLevel():
Test setup:
- Wait for Editor idle loop.
- Open the "Base" level.
- Deletes all existing entities before creating the scene.
Expected Behavior:
The scene can be setup for a basic level.
@@ -115,7 +116,6 @@ def AtomGPU_BasicLevelSetup_SetsUpLevel():
"""
import os
from math import isclose
import azlmbr.legacy.general as general
import azlmbr.math as math
@@ -126,21 +126,11 @@ def AtomGPU_BasicLevelSetup_SetsUpLevel():
from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties
from Atom.atom_utils.atom_component_helper import initial_viewport_setup
from Atom.atom_utils.screenshot_utils import ScreenshotHelper
SCREENSHOT_NAME = "AtomBasicLevelSetup"
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720
DEGREE_RADIAN_FACTOR = 0.0174533
def initial_viewport_setup(screen_width, screen_height):
general.set_viewport_size(screen_width, screen_height)
general.update_viewport()
TestHelper.wait_for_condition(
function=lambda: isclose(a=general.get_viewport_size().x, b=SCREEN_WIDTH, rel_tol=0.1)
and isclose(a=general.get_viewport_size().y, b=SCREEN_HEIGHT, rel_tol=0.1),
timeout_in_seconds=4.0
)
SCREENSHOT_NAME = "AtomBasicLevelSetup"
with Tracer() as error_tracer:
# Test setup begins.
@@ -148,11 +138,16 @@ def AtomGPU_BasicLevelSetup_SetsUpLevel():
TestHelper.init_idle()
TestHelper.open_level("", "Base")
# Setup: Deletes all existing entities before creating the scene.
search_filter = azlmbr.entity.SearchFilter()
all_entities = azlmbr.entity.SearchBus(azlmbr.bus.Broadcast, "SearchEntities", search_filter)
azlmbr.editor.ToolsApplicationRequestBus(azlmbr.bus.Broadcast, "DeleteEntities", all_entities)
# Test steps begin.
# 1. Close error windows and display helpers then update the viewport size.
TestHelper.close_error_windows()
TestHelper.close_display_helpers()
initial_viewport_setup(SCREEN_WIDTH, SCREEN_HEIGHT)
initial_viewport_setup()
general.update_viewport()
# 2. Create Default Level Entity.
@@ -0,0 +1,248 @@
"""
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
"""
class Tests:
directional_light_component_disabled = (
"Disabled Directional Light component",
"Couldn't disable Directional Light component")
enter_game_mode = (
"Entered game mode",
"Failed to enter game mode")
exit_game_mode = (
"Exited game mode",
"Couldn't exit game mode")
global_skylight_component_disabled = (
"Disabled Global Skylight (IBL) component",
"Couldn't disable Global Skylight (IBL) component")
hdri_skybox_component_disabled = (
"Disabled HDRi Skybox component",
"Couldn't disable HDRi Skybox component")
light_component_added = (
"Light component added",
"Couldn't add Light component")
light_component_color_property_set = (
"Color property was set",
"Color property was not set")
light_component_enable_shadow_property_set = (
"Enable shadow property was set",
"Enable shadow property was not set")
light_component_enable_shutters_property_set = (
"Enable shutters property was set",
"Enable shutters property was not set")
light_component_inner_angle_property_set = (
"Inner angle property was set",
"Inner angle property was not set")
light_component_intensity_property_set = (
"Intensity property was set",
"Intensity property was not set")
light_component_light_type_property_set = (
"Light type property was set",
"Light type property was not set")
light_component_outer_angle_property_set = (
"Outer angle property was set",
"Outer angle property was not set")
material_component_material_asset_property_set = (
"Material Asset property was set",
"Material Asset property was not set")
spot_light_entity_created = (
"Spot Light entity created",
"Couldn't create Spot Light entity")
def AtomGPU_LightComponent_SpotLightScreenshotsMatchGoldenImages():
"""
Summary:
Light component test using the Spot (disk) Light type property option and modifying the shadows and colors.
Sets each scene up and then takes a screenshot of each scene for test comparison.
Test setup:
- Wait for Editor idle loop.
- Open the "Base" level.
- Close error windows and display helpers then update the viewport size.
- Runs the create_basic_atom_rendering_scene() function to setup the test scene.
Expected Behavior:
The test scripts sets up the scenes correctly and takes accurate screenshots.
Test Steps:
1. Find the Directional Light entity then disable its Directional Light component.
2. Disable Global Skylight (IBL) component on the Global Skylight (IBL) entity.
3. Disable HDRi Skybox component on the Global Skylight (IBL) entity.
4. Create a Spot Light entity and rotate it.
5. Attach a Light component to the Spot Light entity.
6. Set the Light component Light Type to Spot (disk).
7. Enter game mode and take a screenshot then exit game mode.
8. Change the default material asset for the Ground Plane entity.
9. Enter game mode and take a screenshot then exit game mode.
10. Increase the Intensity value of the Light component.
11. Enter game mode and take a screenshot then exit game mode.
12. Change the Light component Color property value.
13. Enter game mode and take a screenshot then exit game mode.
14. Change the Light component Enable shutters, Inner angle, and Outer angle property values.
15. Enter game mode and take a screenshot then exit game mode.
16. Change the Light component Enable shadow and Shadowmap size property values then move Spot Light entity.
17. Enter game mode and take a screenshot then exit game mode.
18. Look for errors.
:return: None
"""
import os
import azlmbr.legacy.general as general
import azlmbr.paths
from editor_python_test_tools.asset_utils import Asset
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties, LIGHT_TYPES
from Atom.atom_utils.atom_component_helper import (
initial_viewport_setup, create_basic_atom_rendering_scene, enter_exit_game_mode_take_screenshot)
DEGREE_RADIAN_FACTOR = 0.0174533
with Tracer() as error_tracer:
# Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
TestHelper.init_idle()
TestHelper.open_level("", "Base")
# Setup: Close error windows and display helpers then update the viewport size.
TestHelper.close_error_windows()
TestHelper.close_display_helpers()
initial_viewport_setup()
general.update_viewport()
# Setup: Runs the create_basic_atom_rendering_scene() function to setup the test scene.
create_basic_atom_rendering_scene()
# Test steps begin.
# 1. Find the Directional Light entity then disable its Directional Light component.
directional_light_entity = EditorEntity.find_editor_entity(AtomComponentProperties.directional_light())
directional_light_component = directional_light_entity.get_components_of_type(
[AtomComponentProperties.directional_light()])[0]
directional_light_component.disable_component()
Report.critical_result(Tests.directional_light_component_disabled, not directional_light_component.is_enabled())
# 2. Disable Global Skylight (IBL) component on the Global Skylight (IBL) entity.
global_skylight_entity = EditorEntity.find_editor_entity(AtomComponentProperties.global_skylight())
global_skylight_component = global_skylight_entity.get_components_of_type(
[AtomComponentProperties.global_skylight()])[0]
global_skylight_component.disable_component()
Report.critical_result(Tests.global_skylight_component_disabled, not global_skylight_component.is_enabled())
# 3. Disable HDRi Skybox component on the Global Skylight (IBL) entity.
hdri_skybox_component = global_skylight_entity.get_components_of_type(
[AtomComponentProperties.hdri_skybox()])[0]
hdri_skybox_component.disable_component()
Report.critical_result(Tests.hdri_skybox_component_disabled, not hdri_skybox_component.is_enabled())
# 4. Create a Spot Light entity and rotate it.
spot_light_name = "Spot Light"
spot_light_entity = EditorEntity.create_editor_entity_at(
azlmbr.math.Vector3(0.7, -2.0, 1.0), spot_light_name)
rotation = azlmbr.math.Vector3(DEGREE_RADIAN_FACTOR * 300.0, 0.0, 0.0)
spot_light_entity.set_local_rotation(rotation)
Report.critical_result(Tests.spot_light_entity_created, spot_light_entity.exists())
# 5. Attach a Light component to the Spot Light entity.
light_component = spot_light_entity.add_component(AtomComponentProperties.light())
Report.critical_result(Tests.light_component_added, light_component.is_enabled())
# 6. Set the Light component Light Type to Spot (disk).
light_component.set_component_property_value(
AtomComponentProperties.light('Light type'), LIGHT_TYPES['spot_disk'])
Report.result(
Tests.light_component_light_type_property_set,
light_component.get_component_property_value(
AtomComponentProperties.light('Light type')) == LIGHT_TYPES['spot_disk'])
# 7. Enter game mode and take a screenshot then exit game mode.
enter_exit_game_mode_take_screenshot("SpotLight_1.ppm", Tests.enter_game_mode, Tests.exit_game_mode)
# 8. Change the default material asset for the Ground Plane entity.
ground_plane_name = "Ground Plane"
ground_plane_entity = EditorEntity.find_editor_entity(ground_plane_name)
ground_plane_material_component_name = AtomComponentProperties.material()
ground_plane_material_component = ground_plane_entity.get_components_of_type(
[ground_plane_material_component_name])[0]
ground_plane_material_asset_path = os.path.join(
"Materials", "Presets", "Macbeth", "22_neutral_5-0_0-70d.azmaterial")
ground_plane_material_asset = Asset.find_asset_by_path(ground_plane_material_asset_path, False)
ground_plane_material_component.set_component_property_value(
AtomComponentProperties.material('Material Asset'), ground_plane_material_asset.id)
Report.result(
Tests.material_component_material_asset_property_set,
ground_plane_material_component.get_component_property_value(
AtomComponentProperties.material('Material Asset')) == ground_plane_material_asset.id)
# 9. Enter game mode and take a screenshot then exit game mode.
enter_exit_game_mode_take_screenshot("SpotLight_2.ppm", Tests.enter_game_mode, Tests.exit_game_mode)
# 10. Increase the Intensity value of the Light component.
light_component.set_component_property_value(AtomComponentProperties.light('Intensity'), 800.0)
Report.result(
Tests.light_component_intensity_property_set,
light_component.get_component_property_value(
AtomComponentProperties.light('Intensity')) == 800.0)
# 11. Enter game mode and take a screenshot then exit game mode.
enter_exit_game_mode_take_screenshot("SpotLight_3.ppm", Tests.enter_game_mode, Tests.exit_game_mode)
# 12. Change the Light component Color property value.
color_value = azlmbr.math.Color(47.0 / 255.0, 75.0 / 255.0, 37.0 / 255.0, 255.0 / 255.0)
light_component.set_component_property_value(AtomComponentProperties.light('Color'), color_value)
Report.result(
Tests.light_component_color_property_set,
light_component.get_component_property_value(AtomComponentProperties.light('Color')) == color_value)
# 13. Enter game mode and take a screenshot then exit game mode.
enter_exit_game_mode_take_screenshot("SpotLight_4.ppm", Tests.enter_game_mode, Tests.exit_game_mode)
# 14. Change the Light component Enable shutters, Inner angle, and Outer angle property values.
enable_shutters = True
inner_angle = 60.0
outer_angle = 75.0
light_component.set_component_property_value(AtomComponentProperties.light('Enable shutters'), enable_shutters)
light_component.set_component_property_value(AtomComponentProperties.light('Inner angle'), inner_angle)
light_component.set_component_property_value(AtomComponentProperties.light('Outer angle'), outer_angle)
Report.result(
Tests.light_component_enable_shutters_property_set,
light_component.get_component_property_value(
AtomComponentProperties.light('Enable shutters')) == enable_shutters)
Report.result(
Tests.light_component_inner_angle_property_set,
light_component.get_component_property_value(AtomComponentProperties.light('Inner angle')) == inner_angle)
Report.result(
Tests.light_component_outer_angle_property_set,
light_component.get_component_property_value(AtomComponentProperties.light('Outer angle')) == outer_angle)
# 15. Enter game mode and take a screenshot then exit game mode.
enter_exit_game_mode_take_screenshot("SpotLight_5.ppm", Tests.enter_game_mode, Tests.exit_game_mode)
# 16. Change the Light component Enable shadow and slightly move Spot Light entity.
light_component.set_component_property_value(AtomComponentProperties.light('Enable shadow'), True)
Report.result(
Tests.light_component_enable_shadow_property_set,
light_component.get_component_property_value(AtomComponentProperties.light('Enable shadow')) is True)
spot_light_entity.set_world_rotation(azlmbr.math.Vector3(0.7, -2.0, 1.9))
# 17. Enter game mode and take a screenshot then exit game mode.
enter_exit_game_mode_take_screenshot("SpotLight_6.ppm", Tests.enter_game_mode, Tests.exit_game_mode)
# 18. Look for errors.
TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(AtomGPU_LightComponent_SpotLightScreenshotsMatchGoldenImages)
@@ -186,6 +186,11 @@ def run():
material_editor.set_property(document2_id, property2_name, initial_color)
material_editor.save_all()
material_editor.close_all_documents()
material_editor.wait_for_condition(lambda:
(not material_editor.is_open(document1_id)) and
(not material_editor.is_open(document2_id)) and
(not material_editor.is_open(document3_id)), 2.0)
material_editor.destroy_main_window()
if __name__ == "__main__":
@@ -1,200 +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
"""
import os
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.editor_test_helper import EditorTestHelper
from Atom.atom_utils.screenshot_utils import ScreenshotHelper
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720
DEGREE_RADIAN_FACTOR = 0.0174533
helper = EditorTestHelper(log_prefix="Test_Atom_BasicLevelSetup")
def run():
"""
1. View -> Layouts -> Restore Default Layout, sets the viewport to ratio 16:9 @ 1280 x 720
2. Runs console command r_DisplayInfo = 0
3. Deletes all entities currently present in the level.
4. Creates a "default_level" entity to hold all other entities, setting the translate values to x:0, y:0, z:0
5. Adds a Grid component to the "default_level" & updates its Grid Spacing to 1.0m
6. Adds a "global_skylight" entity to "default_level", attaching an HDRi Skybox w/ a Cubemap Texture.
7. Adds a Global Skylight (IBL) component w/ diffuse image and specular image to "global_skylight" entity.
8. Adds a "ground_plane" entity to "default_level", attaching a Mesh component & Material component.
9. Adds a "directional_light" entity to "default_level" & adds a Directional Light component.
10. Adds a "sphere" entity to "default_level" & adds a Mesh component with a Material component to it.
11. Adds a "camera" entity to "default_level" & adds a Camera component with 80 degree FOV and Transform values:
Translate - x:5.5m, y:-12.0m, z:9.0m
Rotate - x:-27.0, y:-12.0, z:25.0
12. Finally enters game mode, takes a screenshot, & exits game mode.
:return: None
"""
import azlmbr.asset as asset
import azlmbr.bus as bus
import azlmbr.camera as camera
import azlmbr.entity as entity
import azlmbr.legacy.general as general
import azlmbr.math as math
import azlmbr.paths
import azlmbr.editor as editor
def initial_viewport_setup(screen_width, screen_height):
general.set_viewport_size(screen_width, screen_height)
general.update_viewport()
helper.wait_for_condition(
function=lambda: helper.isclose(a=general.get_viewport_size().x, b=SCREEN_WIDTH, rel_tol=0.1)
and helper.isclose(a=general.get_viewport_size().y, b=SCREEN_HEIGHT, rel_tol=0.1),
timeout_in_seconds=4.0
)
result = helper.isclose(a=general.get_viewport_size().x, b=SCREEN_WIDTH, rel_tol=0.1) and helper.isclose(
a=general.get_viewport_size().y, b=SCREEN_HEIGHT, rel_tol=0.1)
general.log(general.get_viewport_size().x)
general.log(general.get_viewport_size().y)
general.log(general.get_viewport_size().z)
general.log(f"Viewport is set to the expected size: {result}")
general.run_console("r_DisplayInfo = 0")
def after_level_load():
"""Function to call after creating/opening a level to ensure it loads."""
# Give everything a second to initialize.
general.idle_enable(True)
general.idle_wait(1.0)
general.update_viewport()
general.idle_wait(0.5) # half a second is more than enough for updating the viewport.
# Close out problematic windows, FPS meters, and anti-aliasing.
if general.is_helpers_shown(): # Turn off the helper gizmos if visible
general.toggle_helpers()
general.idle_wait(1.0)
if general.is_pane_visible("Error Report"): # Close Error Report windows that block focus.
general.close_pane("Error Report")
if general.is_pane_visible("Error Log"): # Close Error Log windows that block focus.
general.close_pane("Error Log")
general.idle_wait(1.0)
general.run_console("r_displayInfo=0")
general.idle_wait(1.0)
# Wait for Editor idle loop before executing Python hydra scripts.
general.idle_enable(True)
# Basic setup for opened level.
helper.open_level(level_name="Base")
after_level_load()
initial_viewport_setup(SCREEN_WIDTH, SCREEN_HEIGHT)
# Create default_level entity
search_filter = azlmbr.entity.SearchFilter()
all_entities = entity.SearchBus(azlmbr.bus.Broadcast, "SearchEntities", search_filter)
editor.ToolsApplicationRequestBus(bus.Broadcast, "DeleteEntities", all_entities)
default_level = hydra.Entity("default_level")
position = math.Vector3(0.0, 0.0, 0.0)
default_level.create_entity(position, ["Grid"])
default_level.get_set_test(0, "Controller|Configuration|Secondary Grid Spacing", 1.0)
# Create global_skylight entity and set the properties
global_skylight = hydra.Entity("global_skylight")
global_skylight.create_entity(
entity_position=math.Vector3(0.0, 0.0, 0.0),
components=["HDRi Skybox", "Global Skylight (IBL)"],
parent_id=default_level.id
)
global_skylight_image_asset_path = os.path.join("LightingPresets", "default_iblskyboxcm.exr.streamingimage")
global_skylight_image_asset = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", global_skylight_image_asset_path, math.Uuid(), False)
global_skylight.get_set_test(0, "Controller|Configuration|Cubemap Texture", global_skylight_image_asset)
hydra.get_set_test(global_skylight, 1, "Controller|Configuration|Diffuse Image", global_skylight_image_asset)
hydra.get_set_test(global_skylight, 1, "Controller|Configuration|Specular Image", global_skylight_image_asset)
# Create ground_plane entity and set the properties
ground_plane = hydra.Entity("ground_plane")
ground_plane.create_entity(
entity_position=math.Vector3(0.0, 0.0, 0.0),
components=["Material"],
parent_id=default_level.id
)
azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalUniformScale", ground_plane.id, 32.0)
# Work around to add the correct Atom Mesh component and asset.
mesh_type_id = azlmbr.globals.property.EditorMeshComponentTypeId
ground_plane.components.append(
editor.EditorComponentAPIBus(
bus.Broadcast, "AddComponentsOfType", ground_plane.id, [mesh_type_id]
).GetValue()[0]
)
ground_plane_mesh_asset_path = os.path.join("TestData", "Objects", "plane.azmodel")
ground_plane_mesh_asset = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", ground_plane_mesh_asset_path, math.Uuid(), False)
hydra.get_set_test(ground_plane, 1, "Controller|Configuration|Mesh Asset", ground_plane_mesh_asset)
# Add Atom Material component and asset.
ground_plane_material_asset_path = os.path.join("Materials", "Presets", "PBR", "metal_chrome.azmaterial")
ground_plane_material_asset = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", ground_plane_material_asset_path, math.Uuid(), False)
ground_plane.get_set_test(0, "Default Material|Material Asset", ground_plane_material_asset)
# Create directional_light entity and set the properties
directional_light = hydra.Entity("directional_light")
directional_light.create_entity(
entity_position=math.Vector3(0.0, 0.0, 10.0),
components=["Directional Light"],
parent_id=default_level.id
)
rotation = math.Vector3(DEGREE_RADIAN_FACTOR * -90.0, 0.0, 0.0)
azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalRotation", directional_light.id, rotation)
# Create sphere entity and set the properties
sphere = hydra.Entity("sphere")
sphere.create_entity(
entity_position=math.Vector3(0.0, 0.0, 1.0),
components=["Material"],
parent_id=default_level.id
)
# Work around to add the correct Atom Mesh component and asset.
sphere.components.append(
editor.EditorComponentAPIBus(
bus.Broadcast, "AddComponentsOfType", sphere.id, [mesh_type_id]
).GetValue()[0]
)
sphere_mesh_asset_path = os.path.join("Models", "sphere.azmodel")
sphere_mesh_asset = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", sphere_mesh_asset_path, math.Uuid(), False)
hydra.get_set_test(sphere, 1, "Controller|Configuration|Mesh Asset", sphere_mesh_asset)
# Add Atom Material component and asset.
sphere_material_asset_path = os.path.join("Materials", "Presets", "PBR", "metal_brass_polished.azmaterial")
sphere_material_asset = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", sphere_material_asset_path, math.Uuid(), False)
sphere.get_set_test(0, "Default Material|Material Asset", sphere_material_asset)
# Create camera component and set the properties
camera_entity = hydra.Entity("camera")
position = math.Vector3(5.5, -12.0, 9.0)
camera_entity.create_entity(components=["Camera"], entity_position=position, parent_id=default_level.id)
rotation = math.Vector3(
DEGREE_RADIAN_FACTOR * -27.0, DEGREE_RADIAN_FACTOR * -12.0, DEGREE_RADIAN_FACTOR * 25.0
)
azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalRotation", camera_entity.id, rotation)
camera_entity.get_set_test(0, "Controller|Configuration|Field of view", 60.0)
camera.EditorCameraViewRequestBus(azlmbr.bus.Event, "ToggleCameraAsActiveView", camera_entity.id)
# Enter game mode, take screenshot, & exit game mode.
general.idle_wait(0.5)
general.enter_game_mode()
general.idle_wait(1.0)
helper.wait_for_condition(function=lambda: general.is_in_game_mode(), timeout_in_seconds=2.0)
ScreenshotHelper(general.idle_wait_frames).capture_screenshot_blocking(f"{'AtomBasicLevelSetup'}.ppm")
general.exit_game_mode()
helper.wait_for_condition(function=lambda: not general.is_in_game_mode(), timeout_in_seconds=2.0)
if __name__ == "__main__":
run()
@@ -1,261 +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
"""
import os
import sys
import azlmbr.asset as asset
import azlmbr.bus as bus
import azlmbr.editor as editor
import azlmbr.math as math
import azlmbr.paths
import azlmbr.legacy.general as general
sys.path.append(os.path.join(azlmbr.paths.projectroot, "Gem", "PythonTests"))
import editor_python_test_tools.hydra_editor_utils as hydra
from Atom.atom_utils import atom_component_helper, atom_constants, screenshot_utils
from editor_python_test_tools.editor_test_helper import EditorTestHelper
helper = EditorTestHelper(log_prefix="Atom_EditorTestHelper")
LEVEL_NAME = "Base"
LIGHT_COMPONENT = "Light"
LIGHT_TYPE_PROPERTY = 'Controller|Configuration|Light type'
DEGREE_RADIAN_FACTOR = 0.0174533
def run():
"""
Sets up the tests by making sure the required level is created & setup correctly.
It then executes 2 test cases - see each associated test function's docstring for more info.
Finally prints the string "Light component tests completed" after completion
Tests will fail immediately if any of these log lines are found:
1. Trace::Assert
2. Trace::Error
3. Traceback (most recent call last):
:return: None
"""
atom_component_helper.create_basic_atom_level(level_name=LEVEL_NAME)
# Run tests.
area_light_test()
spot_light_test()
general.log("Light component tests completed.")
def area_light_test():
"""
Basic test for the "Light" component attached to an "area_light" entity.
Test Case - Light Component: Capsule, Spot (disk), and Point (sphere):
1. Creates "area_light" entity w/ a Light component that has a Capsule Light type w/ the color set to 255, 0, 0
2. Enters game mode to take a screenshot for comparison, then exits game mode.
3. Sets the Light component Intensity Mode to Lumens (default).
4. Ensures the Light component Mode is Automatic (default).
5. Sets the Intensity value of the Light component to 0.0
6. Enters game mode again, takes another screenshot for comparison, then exits game mode.
7. Updates the Intensity value of the Light component to 1000.0
8. Enters game mode again, takes another screenshot for comparison, then exits game mode.
9. Swaps the Capsule light type option to Spot (disk) light type on the Light component
10. Updates "area_light" entity Transform rotate value to x: 90.0, y:0.0, z:0.0
11. Enters game mode again, takes another screenshot for comparison, then exits game mode.
12. Swaps the Spot (disk) light type for the Point (sphere) light type in the Light component.
13. Enters game mode again, takes another screenshot for comparison, then exits game mode.
14. Deletes the Light component from the "area_light" entity and verifies its successful.
"""
# Create an "area_light" entity with "Light" component using Light type of "Capsule"
area_light_entity_name = "area_light"
area_light = hydra.Entity(area_light_entity_name)
area_light.create_entity(math.Vector3(-1.0, -2.0, 3.0), [LIGHT_COMPONENT])
general.log(
f"{area_light_entity_name}_test: Component added to the entity: "
f"{hydra.has_components(area_light.id, [LIGHT_COMPONENT])}")
light_component_id_pair = hydra.attach_component_to_entity(area_light.id, LIGHT_COMPONENT)
# Select the "Capsule" light type option.
azlmbr.editor.EditorComponentAPIBus(
azlmbr.bus.Broadcast,
'SetComponentProperty',
light_component_id_pair,
LIGHT_TYPE_PROPERTY,
atom_constants.LIGHT_TYPES['capsule']
)
# Update color and take screenshot in game mode
color = math.Color(255.0, 0.0, 0.0, 0.0)
area_light.get_set_test(0, "Controller|Configuration|Color", color)
general.idle_wait(1.0)
screenshot_utils.take_screenshot_game_mode("AreaLight_1", area_light_entity_name)
# Update intensity value to 0.0 and take screenshot in game mode
area_light.get_set_test(0, "Controller|Configuration|Attenuation Radius|Mode", 1)
area_light.get_set_test(0, "Controller|Configuration|Intensity", 0.0)
general.idle_wait(1.0)
screenshot_utils.take_screenshot_game_mode("AreaLight_2", area_light_entity_name)
# Update intensity value to 1000.0 and take screenshot in game mode
area_light.get_set_test(0, "Controller|Configuration|Intensity", 1000.0)
general.idle_wait(1.0)
screenshot_utils.take_screenshot_game_mode("AreaLight_3", area_light_entity_name)
# Swap the "Capsule" light type option to "Spot (disk)" light type
azlmbr.editor.EditorComponentAPIBus(
azlmbr.bus.Broadcast,
'SetComponentProperty',
light_component_id_pair,
LIGHT_TYPE_PROPERTY,
atom_constants.LIGHT_TYPES['spot_disk']
)
area_light_rotation = math.Vector3(DEGREE_RADIAN_FACTOR * 90.0, 0.0, 0.0)
azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalRotation", area_light.id, area_light_rotation)
general.idle_wait(1.0)
screenshot_utils.take_screenshot_game_mode("AreaLight_4", area_light_entity_name)
# Swap the "Spot (disk)" light type to the "Point (sphere)" light type and take screenshot.
azlmbr.editor.EditorComponentAPIBus(
azlmbr.bus.Broadcast,
'SetComponentProperty',
light_component_id_pair,
LIGHT_TYPE_PROPERTY,
atom_constants.LIGHT_TYPES['sphere']
)
general.idle_wait(1.0)
screenshot_utils.take_screenshot_game_mode("AreaLight_5", area_light_entity_name)
editor.ToolsApplicationRequestBus(bus.Broadcast, "DeleteEntityById", area_light.id)
def spot_light_test():
"""
Basic test for the Light component attached to a "spot_light" entity.
Test Case - Light Component: Spot (disk) with shadows & colors:
1. Creates "spot_light" entity w/ a Light component attached to it.
2. Selects the "directional_light" entity already present in the level and disables it.
3. Selects the "global_skylight" entity already present in the level and disables the HDRi Skybox component,
as well as the Global Skylight (IBL) component.
4. Enters game mode to take a screenshot for comparison, then exits game mode.
5. Selects the "ground_plane" entity and changes updates the material to a new material.
6. Enters game mode to take a screenshot for comparison, then exits game mode.
7. Selects the "spot_light" entity and increases the Light component Intensity to 800 lm
8. Enters game mode to take a screenshot for comparison, then exits game mode.
9. Selects the "spot_light" entity and sets the Light component Color to 47, 75, 37
10. Enters game mode to take a screenshot for comparison, then exits game mode.
11. Selects the "spot_light" entity and modifies the Shutter controls to the following values:
- Enable shutters: True
- Inner Angle: 60.0
- Outer Angle: 75.0
12. Enters game mode to take a screenshot for comparison, then exits game mode.
13. Selects the "spot_light" entity and modifies the Shadow controls to the following values:
- Enable Shadow: True
- ShadowmapSize: 256
14. Modifies the world translate position of the "spot_light" entity to 0.7, -2.0, 1.9 (for casting shadows better)
15. Enters game mode to take a screenshot for comparison, then exits game mode.
"""
# Disable "Directional Light" component for the "directional_light" entity
# "directional_light" entity is created by the create_basic_atom_level() function by default.
directional_light_entity_id = hydra.find_entity_by_name("directional_light")
directional_light = hydra.Entity(name='directional_light', id=directional_light_entity_id)
directional_light_component_type = azlmbr.editor.EditorComponentAPIBus(
azlmbr.bus.Broadcast, 'FindComponentTypeIdsByEntityType', ["Directional Light"], 0)[0]
directional_light_component = azlmbr.editor.EditorComponentAPIBus(
azlmbr.bus.Broadcast, 'GetComponentOfType', directional_light.id, directional_light_component_type
).GetValue()
editor.EditorComponentAPIBus(bus.Broadcast, "DisableComponents", [directional_light_component])
general.idle_wait(0.5)
# Disable "Global Skylight (IBL)" and "HDRi Skybox" components for the "global_skylight" entity
global_skylight_entity_id = hydra.find_entity_by_name("global_skylight")
global_skylight = hydra.Entity(name='global_skylight', id=global_skylight_entity_id)
global_skylight_component_type = azlmbr.editor.EditorComponentAPIBus(
azlmbr.bus.Broadcast, 'FindComponentTypeIdsByEntityType', ["Global Skylight (IBL)"], 0)[0]
global_skylight_component = azlmbr.editor.EditorComponentAPIBus(
azlmbr.bus.Broadcast, 'GetComponentOfType', global_skylight.id, global_skylight_component_type
).GetValue()
editor.EditorComponentAPIBus(bus.Broadcast, "DisableComponents", [global_skylight_component])
hdri_skybox_component_type = azlmbr.editor.EditorComponentAPIBus(
azlmbr.bus.Broadcast, 'FindComponentTypeIdsByEntityType', ["HDRi Skybox"], 0)[0]
hdri_skybox_component = azlmbr.editor.EditorComponentAPIBus(
azlmbr.bus.Broadcast, 'GetComponentOfType', global_skylight.id, hdri_skybox_component_type
).GetValue()
editor.EditorComponentAPIBus(bus.Broadcast, "DisableComponents", [hdri_skybox_component])
general.idle_wait(0.5)
# Create a "spot_light" entity with "Light" component using Light Type of "Spot (disk)"
spot_light_entity_name = "spot_light"
spot_light = hydra.Entity(spot_light_entity_name)
spot_light.create_entity(math.Vector3(0.7, -2.0, 1.0), [LIGHT_COMPONENT])
general.log(
f"{spot_light_entity_name}_test: Component added to the entity: "
f"{hydra.has_components(spot_light.id, [LIGHT_COMPONENT])}")
rotation = math.Vector3(DEGREE_RADIAN_FACTOR * 300.0, 0.0, 0.0)
azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalRotation", spot_light.id, rotation)
light_component_type = hydra.attach_component_to_entity(spot_light.id, LIGHT_COMPONENT)
editor.EditorComponentAPIBus(
azlmbr.bus.Broadcast,
'SetComponentProperty',
light_component_type,
LIGHT_TYPE_PROPERTY,
atom_constants.LIGHT_TYPES['spot_disk']
)
general.idle_wait(1.0)
screenshot_utils.take_screenshot_game_mode("SpotLight_1", spot_light_entity_name)
# Change default material of ground plane entity and take screenshot
ground_plane_entity_id = hydra.find_entity_by_name("ground_plane")
ground_plane = hydra.Entity(name='ground_plane', id=ground_plane_entity_id)
ground_plane_asset_path = os.path.join("Materials", "Presets", "MacBeth", "22_neutral_5-0_0-70d.azmaterial")
ground_plane_asset_value = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", ground_plane_asset_path, math.Uuid(), False)
material_property_path = "Default Material|Material Asset"
material_component_type = azlmbr.editor.EditorComponentAPIBus(
azlmbr.bus.Broadcast, 'FindComponentTypeIdsByEntityType', ["Material"], 0)[0]
material_component = azlmbr.editor.EditorComponentAPIBus(
azlmbr.bus.Broadcast, 'GetComponentOfType', ground_plane.id, material_component_type).GetValue()
editor.EditorComponentAPIBus(
azlmbr.bus.Broadcast,
'SetComponentProperty',
material_component,
material_property_path,
ground_plane_asset_value
)
general.idle_wait(1.0)
screenshot_utils.take_screenshot_game_mode("SpotLight_2", spot_light_entity_name)
# Increase intensity value of the Spot light and take screenshot in game mode
spot_light.get_set_test(0, "Controller|Configuration|Intensity", 800.0)
general.idle_wait(1.0)
screenshot_utils.take_screenshot_game_mode("SpotLight_3", spot_light_entity_name)
# Update the Spot light color and take screenshot in game mode
color_value = math.Color(47.0 / 255.0, 75.0 / 255.0, 37.0 / 255.0, 255.0 / 255.0)
spot_light.get_set_test(0, "Controller|Configuration|Color", color_value)
general.idle_wait(1.0)
screenshot_utils.take_screenshot_game_mode("SpotLight_4", spot_light_entity_name)
# Update the Shutter controls of the Light component and take screenshot
spot_light.get_set_test(0, "Controller|Configuration|Shutters|Enable shutters", True)
spot_light.get_set_test(0, "Controller|Configuration|Shutters|Inner angle", 60.0)
spot_light.get_set_test(0, "Controller|Configuration|Shutters|Outer angle", 75.0)
general.idle_wait(1.0)
screenshot_utils.take_screenshot_game_mode("SpotLight_5", spot_light_entity_name)
# Update the Shadow controls, move the spot_light entity world translate position and take screenshot
spot_light.get_set_test(0, "Controller|Configuration|Shadows|Enable shadow", True)
spot_light.get_set_test(0, "Controller|Configuration|Shadows|Shadowmap size", 256.0)
azlmbr.components.TransformBus(
azlmbr.bus.Event, "SetWorldTranslation", spot_light.id, math.Vector3(0.7, -2.0, 1.9))
general.idle_wait(1.0)
screenshot_utils.take_screenshot_game_mode("SpotLight_6", spot_light_entity_name)
if __name__ == "__main__":
run()
@@ -25,7 +25,7 @@ class EditorComponent:
"""
EditorComponent class used to set and get the component property value using path
EditorComponent object is returned from either of
EditorEntity.add_component() or Entity.add_components() or EditorEntity.get_component_objects()
EditorEntity.add_component() or Entity.add_components() or EditorEntity.get_components_of_type()
which also assigns self.id and self.type_id to the EditorComponent object.
"""
@@ -94,6 +94,13 @@ class EditorComponent:
"""
return editor.EditorComponentAPIBus(bus.Broadcast, "IsComponentEnabled", self.id)
def disable_component(self):
"""
Used to disable the component using its id value.
:return: None
"""
editor.EditorComponentAPIBus(bus.Broadcast, "DisableComponents", [self.id])
@staticmethod
def get_type_ids(component_names: list) -> list:
"""
@@ -107,7 +114,6 @@ class EditorComponent:
return type_ids
def convert_to_azvector3(xyz) -> azlmbr.math.Vector3:
"""
Converts a vector3-like element into a azlmbr.math.Vector3
@@ -120,6 +126,7 @@ def convert_to_azvector3(xyz) -> azlmbr.math.Vector3:
else:
raise ValueError("vector must be a 3 element list/tuple or azlmbr.math.Vector3")
class EditorEntity:
"""
Entity class is used to create and interact with Editor Entities.
@@ -136,10 +143,11 @@ class EditorEntity:
# Creation functions
@classmethod
def find_editor_entity(cls, entity_name: str, must_be_unique : bool = False) -> EditorEntity:
def find_editor_entity(cls, entity_name: str, must_be_unique: bool = False) -> EditorEntity:
"""
Given Entity name, outputs entity object
:param entity_name: Name of entity to find
:param must_be_unique: bool that asserts the entity_name specified is unique when set to True
:return: EditorEntity class object
"""
entities = cls.find_editor_entities([entity_name])
@@ -147,14 +155,14 @@ class EditorEntity:
if must_be_unique:
assert len(entities) == 1, f"Failure: Multiple entities with name: '{entity_name}' when expected only one"
entity = cls(entities[0])
entity = entities[0]
return entity
@classmethod
def find_editor_entities(cls, entity_names: List[str]) -> EditorEntity:
def find_editor_entities(cls, entity_names: List[str]) -> List[EditorEntity]:
"""
Given Entities names, returns a list of EditorEntity
:param entity_name: Name of entity to find
:param entity_names: List of entity names to find
:return: List[EditorEntity] class object
"""
searchFilter = azlmbr.entity.SearchFilter()
@@ -438,7 +446,7 @@ class EditorEntity:
def set_local_rotation(self, new_rotation) -> None:
"""
Sets the set the local rotation(relative to the parent) of the current entity.
:param vector3_rotation: The math.Vector3 value to use for rotation on the entity (uses radians).
:param new_rotation: The math.Vector3 value to use for rotation on the entity (uses radians).
:return: None
"""
new_rotation = convert_to_azvector3(new_rotation)
@@ -454,7 +462,7 @@ class EditorEntity:
def set_local_translation(self, new_translation) -> None:
"""
Sets the local translation(relative to the parent) of the current entity.
:param vector3_translation: The math.Vector3 value to use for translation on the entity.
:param new_translation: The math.Vector3 value to use for translation on the entity.
:return: None
"""
new_translation = convert_to_azvector3(new_translation)
@@ -470,3 +478,99 @@ class EditorEntity:
assert self.id.isValid(), "A valid entity id is required to focus on its owning prefab."
focus_prefab_result = azlmbr.prefab.PrefabFocusPublicRequestBus(bus.Broadcast, "FocusOnOwningPrefab", self.id)
assert focus_prefab_result.IsSuccess(), f"Prefab operation 'FocusOnOwningPrefab' failed. Error: {focus_prefab_result.GetError()}"
class EditorLevelEntity:
"""
EditorLevel class used to add and fetch level components.
Level entity is a special entity that you do not create/destroy independently of larger systems of level creation.
This collects a number of staticmethods that do not rely on entityId since Level entity is found internally by
EditorLevelComponentAPIBus requests.
"""
@staticmethod
def get_type_ids(component_names: list) -> list:
"""
Used to get type ids of given components list for EntityType Level
:param: component_names: List of components to get type ids
:return: List of type ids of given components.
"""
type_ids = editor.EditorComponentAPIBus(
bus.Broadcast, "FindComponentTypeIdsByEntityType", component_names, azlmbr.entity.EntityType().Level
)
return type_ids
@staticmethod
def add_component(component_name: str) -> EditorComponent:
"""
Used to add new component to Level.
:param component_name: String of component name to add.
:return: Component object of newly added component.
"""
component = EditorLevelEntity.add_components([component_name])[0]
return component
@staticmethod
def add_components(component_names: list) -> List[EditorComponent]:
"""
Used to add multiple components
:param: component_names: List of components to add to level
:return: List of newly added components to the level
"""
components = []
type_ids = EditorLevelEntity.get_type_ids(component_names)
for type_id in type_ids:
new_comp = EditorComponent()
new_comp.type_id = type_id
add_component_outcome = editor.EditorLevelComponentAPIBus(
bus.Broadcast, "AddComponentsOfType", [type_id]
)
assert (
add_component_outcome.IsSuccess()
), f"Failure: Could not add component: '{new_comp.get_component_name()}' to level"
new_comp.id = add_component_outcome.GetValue()[0]
components.append(new_comp)
return components
@staticmethod
def get_components_of_type(component_names: list) -> List[EditorComponent]:
"""
Used to get components of type component_name that already exists on the level
:param component_names: List of names of components to check
:return: List of Level Component objects of given component name
"""
component_list = []
type_ids = EditorLevelEntity.get_type_ids(component_names)
for type_id in type_ids:
component = EditorComponent()
component.type_id = type_id
get_component_of_type_outcome = editor.EditorLevelComponentAPIBus(
bus.Broadcast, "GetComponentOfType", type_id
)
assert (
get_component_of_type_outcome.IsSuccess()
), f"Failure: Level does not have component:'{component.get_component_name()}'"
component.id = get_component_of_type_outcome.GetValue()
component_list.append(component)
return component_list
@staticmethod
def has_component(component_name: str) -> bool:
"""
Used to verify if the level has the specified component
:param component_name: Name of component to check for
:return: True, if level has specified component. Else, False
"""
type_ids = EditorLevelEntity.get_type_ids([component_name])
return editor.EditorLevelComponentAPIBus(bus.Broadcast, "HasComponentOfType", type_ids[0])
@staticmethod
def count_components_of_type(component_name: str) -> int:
"""
Used to get a count of the specified level component attached to the level
:param component_name: Name of component to check for
:return: integer count of occurences of level component attached to level or zero if none are present
"""
type_ids = EditorLevelEntity.get_type_ids([component_name])
return editor.EditorLevelComponentAPIBus(bus.Broadcast, "CountComponentsOfType", type_ids[0])
@@ -5,15 +5,22 @@ For complete copyright and license terms please see the LICENSE at the root of t
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
from typing import List
from math import isclose
import collections.abc
import azlmbr.bus as bus
import azlmbr.editor as editor
import azlmbr.entity as entity
import azlmbr.legacy.general as general
import azlmbr.object
from typing import List
from math import isclose
import collections.abc
from editor_python_test_tools.utils import TestHelper as helper
def open_base_level():
helper.init_idle()
helper.open_level("Prefab", "Base")
def find_entity_by_name(entity_name):
@@ -46,6 +53,18 @@ def get_component_type_id(component_name):
return component_type_id
def get_level_component_type_id(component_name):
"""
Gets the component_type_id from a given component name
:param component_name: String of component name to search for
:return component type ID
"""
type_ids_list = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeIdsByEntityType', [component_name],
entity.EntityType().Level)
component_type_id = type_ids_list[0]
return component_type_id
def add_level_component(component_name):
"""
Adds the specified component to the Level Inspector
@@ -145,6 +164,20 @@ def get_component_property_value(component, component_propertyPath):
print(f'FAILURE: Could not get value from {component_propertyPath}')
return None
def set_component_property_value(component, component_propertyPath, value):
"""
Given a component name and component property path, set component property value
:param component: Component object to act on.
:param componentPropertyPath: String of component property. (e.g. 'Settings|Visible')
:param value: new value for the variable being changed in the component
"""
componentPropertyObj = editor.EditorComponentAPIBus(bus.Broadcast, 'SetComponentProperty', component,
component_propertyPath, value)
if componentPropertyObj.IsSuccess():
print(f'{component_propertyPath} set to {value}')
else:
print(f'FAILURE: Could not set value in {component_propertyPath}')
def get_property_tree(component):
"""
@@ -58,7 +58,10 @@ def launch_and_validate_results(request, test_directory, editor, editor_script,
if null_renderer:
editor.args.extend(["-rhi=Null"])
if enable_prefab_system:
editor.args.extend(["--regset=/Amazon/Preferences/EnablePrefabSystem=true"])
from os import path
editor.args.extend([
"--regset=/Amazon/Preferences/EnablePrefabSystem=true",
f"--regset-file={os.path.join(workspace.paths.engine_root(), 'Registry', 'prefab.test.setreg')}"])
else:
editor.args.extend(["--regset=/Amazon/Preferences/EnablePrefabSystem=false"])
@@ -39,7 +39,6 @@ def get_prefab_file_path(prefab_path):
prefab_path = name + ".prefab"
return prefab_path
def get_all_entity_ids():
return entity.SearchBus(bus.Broadcast, 'SearchEntities', entity.SearchFilter())
@@ -88,8 +88,7 @@ class TestHelper:
general.idle_wait_frames(200)
@staticmethod
def enter_game_mode(msgtuple_success_fail : Tuple[str, str]):
# type: (tuple) -> None
def enter_game_mode(msgtuple_success_fail: Tuple[str, str]) -> None:
"""
:param msgtuple_success_fail: The tuple with the expected/unexpected messages for entering game mode.
@@ -102,31 +101,56 @@ class TestHelper:
Report.critical_result(msgtuple_success_fail, general.is_in_game_mode())
@staticmethod
def multiplayer_enter_game_mode(msgtuple_success_fail : Tuple[str, str], sv_default_player_spawn_asset : str):
# type: (tuple) -> None
def find_line(window, line, print_infos):
"""
Looks for an expected line in a list of tracer log lines
:param window: The log's window name. For example, logs printed via script-canvas use the "Script" window.
:param line: The log message to search for.
:param print_infos: A list of PrintInfos collected by Tracer to search. Example options: your_tracer.warnings, your_tracer.errors, your_tracer.asserts, or your_tracer.prints
:return: True if the line is found, otherwise false.
"""
for printInfo in print_infos:
if printInfo.window == window.strip() and printInfo.message.strip() == line:
return True
return False
@staticmethod
def succeed_if_log_line_found(window, line, print_infos, time_out):
"""
Looks for a line in a list of tracer log lines and reports success if found.
:param window: The log's window name. For example, logs printed via script-canvas use the "Script" window.
:param line: The log message we're hoping to find.
:param print_infos: A list of PrintInfos collected by Tracer to search. Example options: your_tracer.warnings, your_tracer.errors, your_tracer.asserts, or your_tracer.prints
:param time_out: The total amount of time to wait before giving up looking for the expected line.
:return: No return value, but if the message is found, a successful critical result is reported; otherwise failure.
"""
TestHelper.wait_for_condition(lambda : TestHelper.find_line(window, line, print_infos), time_out)
Report.critical_result(("Found expected line: " + line, "Failed to find expected line: " + line), TestHelper.find_line(window, line, print_infos))
@staticmethod
def fail_if_log_line_found(window, line, print_infos, time_out):
"""
Reports a failure if a log line in a list of tracer log lines is found.
:param window: The log's window name. For example, logs printed via script-canvas use the "Script" window.
:param line: The log message we're hoping to not find.
:param print_infos: A list of PrintInfos collected by Tracer to search. Example options: your_tracer.warnings, your_tracer.errors, your_tracer.asserts, or your_tracer.prints
:param time_out: The total amount of time to wait before giving up looking for the unexpected line. If time runs out and we don't see the unexpected line then report a success.
:return: No return value, but if the line is found, a failed critical result is reported; otherwise success.
"""
TestHelper.wait_for_condition(lambda : TestHelper.find_line(window, line, print_infos), time_out)
Report.critical_result(("Unexpected line not found: " + line, "Unexpected line found: " + line), not TestHelper.find_line(window, line, print_infos))
@staticmethod
def multiplayer_enter_game_mode(msgtuple_success_fail: Tuple[str, str], sv_default_player_spawn_asset: str) -> None:
"""
:param msgtuple_success_fail: The tuple with the expected/unexpected messages for entering game mode.
:param sv_default_player_spawn_asset: The path to the network player prefab that will be automatically spawned upon entering gamemode. The engine default is "prefabs/player.network.spawnable"
:return: None
"""
# looks for an expected line in a list of tracers lines
# lines: the tracer list of lines to search. options are section_tracer.warnings, section_tracer.errors, section_tracer.asserts, section_tracer.prints
# return: true if the line is found, otherwise false
def find_expected_line(expected_line, lines):
found_lines = [printInfo.message.strip() for printInfo in lines]
return expected_line in found_lines
def wait_for_critical_expected_line(expected_line, lines, time_out):
TestHelper.wait_for_condition(lambda : find_expected_line(expected_line, lines), time_out)
Report.critical_result(("Found expected line: " + expected_line, "Failed to find expected line: " + expected_line), find_expected_line(expected_line, lines))
def wait_for_critical_unexpected_line(unexpected_line, lines, time_out):
TestHelper.wait_for_condition(lambda : find_expected_line(unexpected_line, lines), time_out)
Report.critical_result(("Unexpected line not found: " + unexpected_line, "Unexpected line found: " + unexpected_line), not find_expected_line(unexpected_line, lines))
Report.info("Entering game mode")
if sv_default_player_spawn_asset :
general.set_cvar("sv_defaultPlayerSpawnAsset", sv_default_player_spawn_asset)
@@ -137,16 +161,20 @@ class TestHelper:
multiplayer.PythonEditorFuncs_enter_game_mode()
# make sure the server launcher binary exists
wait_for_critical_unexpected_line("LaunchEditorServer failed! The ServerLauncher binary is missing!", section_tracer.errors, 0.5)
TestHelper.fail_if_log_line_found("MultiplayerEditor", "LaunchEditorServer failed! The ServerLauncher binary is missing!", section_tracer.errors, 0.5)
# make sure the server launcher is running
waiter.wait_for(lambda: process_utils.process_exists("AutomatedTesting.ServerLauncher", ignore_extensions=True), timeout=5.0, exc=AssertionError("AutomatedTesting.ServerLauncher has NOT launched!"), interval=1.0)
# make sure the editor connects to the editor-server and sends the level data packet
wait_for_critical_expected_line("Editor is sending the editor-server the level data packet.", section_tracer.prints, 5.0)
TestHelper.succeed_if_log_line_found("EditorServer", "MultiplayerEditorConnection: Editor-server activation has found and connected to the editor.", section_tracer.prints, 15.0)
# make sure the editor finally connects to the editor-server network simulation
wait_for_critical_expected_line("Editor-server ready. Editor has successfully connected to the editor-server's network simulation.", section_tracer.prints, 5.0)
TestHelper.succeed_if_log_line_found("MultiplayerEditor", "Editor is sending the editor-server the level data packet.", section_tracer.prints, 5.0)
TestHelper.succeed_if_log_line_found("EditorServer", "Logger: Editor Server completed receiving the editor's level assets, responding to Editor...", section_tracer.prints, 5.0)
TestHelper.succeed_if_log_line_found("MultiplayerEditorConnection", "Editor-server ready. Editor has successfully connected to the editor-server's network simulation.", section_tracer.prints, 5.0)
TestHelper.fail_if_log_line_found("EditorServer", f"MultiplayerSystemComponent: SpawnDefaultPlayerPrefab failed. Missing sv_defaultPlayerSpawnAsset at path '{sv_default_player_spawn_asset.lower()}'.", section_tracer.prints, 0.5)
TestHelper.wait_for_condition(lambda : multiplayer.PythonEditorFuncs_is_in_game_mode(), 5.0)
Report.critical_result(msgtuple_success_fail, multiplayer.PythonEditorFuncs_is_in_game_mode())
@@ -32,3 +32,6 @@ class TestAutomation(TestAutomationBase):
from .tests import Multiplayer_AutoComponent_NetworkInput as test_module
self._run_prefab_test(request, workspace, editor, test_module)
def test_Multiplayer_AutoComponent_RPC(self, request, workspace, editor, launcher_platform):
from .tests import Multiplayer_AutoComponent_RPC as test_module
self._run_prefab_test(request, workspace, editor, test_module)
@@ -0,0 +1,83 @@
"""
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
"""
# Test Case Title : Check that the four network RPCs can be sent and received
# fmt: off
class TestSuccessFailTuples():
enter_game_mode = ("Entered game mode", "Failed to enter game mode")
exit_game_mode = ("Exited game mode", "Couldn't exit game mode")
find_network_player = ("Found network player", "Couldn't find network player")
# fmt: on
def Multiplayer_AutoComponent_RPC():
r"""
Summary:
Runs a test to make sure that RPCs can be sent and received via script canvas
Level Description:
- Dynamic
1. Although the level is nearly empty, when the server and editor connect the server will spawn and replicate the player network prefab.
a. The player network prefab has a NetworkTestPlayerComponent.AutoComponent and a script canvas attached which sends and receives various RPCs.
Print logs occur upon sending and receiving the RPCs; we are testing to make sure the expected events and values are received.
- Static
1. NetLevelEntity. This is a networked entity which has a script attached. Used for cross-entity communication. The net-player prefab will send this level entity Server->Authority RPCs
Expected Outcome:
We should see editor logs stating that RPCs have been sent and received.
However, if the script receives unexpected values for the Process event we will see print logs for bad data as well.
:return:
"""
import azlmbr.legacy.general as general
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import Tracer
from editor_python_test_tools.utils import TestHelper as helper
from ly_remote_console.remote_console_commands import RemoteConsole as RemoteConsole
level_name = "AutoComponent_RPC"
player_prefab_name = "Player"
player_prefab_path = f"levels/multiplayer/{level_name}/{player_prefab_name}.network.spawnable"
helper.init_idle()
# 1) Open Level
helper.open_level("Multiplayer", level_name)
with Tracer() as section_tracer:
# 2) Enter game mode
helper.multiplayer_enter_game_mode(TestSuccessFailTuples.enter_game_mode, player_prefab_path.lower())
# 3) Make sure the network player was spawned
player_id = general.find_game_entity(player_prefab_name)
Report.critical_result(TestSuccessFailTuples.find_network_player, player_id.IsValid())
# 4) Check the editor logs for expected and unexpected log output
PLAYERID_RPC_WAIT_TIME_SECONDS = 1.0 # The player id is sent from the server as soon as the player script is spawned. 1 second should be more than enough time to send/receive that RPC.
helper.succeed_if_log_line_found('EditorServer', 'Script: AutoComponent_RPC: Sending client PlayerNumber 1', section_tracer.prints, PLAYERID_RPC_WAIT_TIME_SECONDS)
helper.succeed_if_log_line_found('Script', "AutoComponent_RPC: I'm Player #1", section_tracer.prints, PLAYERID_RPC_WAIT_TIME_SECONDS)
# Uncomment once editor game-play mode supports level entities with net-binding
#PLAYFX_RPC_WAIT_TIME_SECONDS = 1.1 # The server will send an RPC to play an fx on the client every second.
#helper.succeed_if_log_line_found('EditorServer', "Script: AutoComponent_RPC_NetLevelEntity Activated on entity: NetLevelEntity", section_tracer.prints, PLAYFX_RPC_WAIT_TIME_SECONDS)
#helper.succeed_if_log_line_found('EditorServer', "Script: AutoComponent_RPC_NetLevelEntity: Authority sending RPC to play some fx.", section_tracer.prints, PLAYFX_RPC_WAIT_TIME_SECONDS)
#helper.succeed_if_log_line_found('Script', "AutoComponent_RPC_NetLevelEntity: I'm a client playing some superficial fx.", section_tracer.prints, PLAYFX_RPC_WAIT_TIME_SECONDS)
# Exit game mode
helper.exit_game_mode(TestSuccessFailTuples.exit_game_mode)
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(Multiplayer_AutoComponent_RPC)
@@ -25,8 +25,8 @@ class TestAutomation(TestAutomationBase):
def test_NvCloth_AddClothSimulationToMesh(self, request, workspace, editor, launcher_platform):
from .tests import NvCloth_AddClothSimulationToMesh as test_module
self._run_test(request, workspace, editor, test_module, use_null_renderer = self.use_null_renderer, enable_prefab_system=False)
self._run_test(request, workspace, editor, test_module, use_null_renderer = self.use_null_renderer)
def test_NvCloth_AddClothSimulationToActor(self, request, workspace, editor, launcher_platform):
from .tests import NvCloth_AddClothSimulationToActor as test_module
self._run_test(request, workspace, editor, test_module, use_null_renderer = self.use_null_renderer, enable_prefab_system=False)
self._run_test(request, workspace, editor, test_module, use_null_renderer = self.use_null_renderer)
@@ -98,38 +98,32 @@ class FileManagement:
"""
file_map = FileManagement._load_file_map()
backup_path = FileManagement.backup_folder_path
backup_file_name = "{}.bak".format(file_name)
backup_file = os.path.join(backup_path, backup_file_name)
# If backup directory DNE, make one
if not os.path.exists(backup_path):
os.mkdir(backup_path)
# If "traditional" backup file exists, delete it (myFile.txt.bak)
if os.path.exists(backup_file):
fs.delete([backup_file], True, False)
# Find my next storage name (myFile_1.txt.bak)
backup_storage_file_name = FileManagement._next_available_name(backup_file_name, file_map)
if backup_storage_file_name is None:
# Find my next storage name (myFile_1.txt)
backup_file_name = FileManagement._next_available_name(file_name, file_map)
if backup_file_name is None:
# If _next_available_name returns None, we have backed up MAX_BACKUPS of files name [file_name]
raise Exception(
"FileManagement class ran out of backups per name. Max: {}".format(FileManagement.MAX_BACKUPS)
)
backup_storage_file = os.path.join(backup_path, backup_storage_file_name)
# If this backup file already exists, delete it.
backup_storage_file = "{}.bak".format(os.path.normpath(os.path.join(backup_path, backup_file_name)))
if os.path.exists(backup_storage_file):
# This file should not exists, but if it does it's about to get clobbered!
fs.unlock_file(backup_storage_file)
# Create "traditional" backup file (myFile.txt.bak)
fs.create_backup(os.path.join(file_path, file_name), backup_path)
# Copy "traditional" backup file into storage backup (myFile_1.txt.bak)
FileManagement._copy_file(backup_file_name, backup_path, backup_storage_file_name, backup_path)
fs.lock_file(backup_storage_file)
# Delete "traditional" back up file
fs.unlock_file(backup_file)
fs.delete([backup_file], True, False)
fs.delete([backup_storage_file], True, False)
# Create backup file (myFile_1.txt.bak)
original_file = os.path.normpath(os.path.join(file_path, file_name))
fs.create_backup(original_file, backup_path, backup_file_name)
# Update file map with new file
file_map[os.path.join(file_path, file_name)] = backup_storage_file_name
file_map[original_file] = backup_file_name
FileManagement._save_file_map(file_map)
# Unlock original file to get it ready to be edited by the test
fs.unlock_file(os.path.join(file_path, file_name))
fs.unlock_file(original_file)
@staticmethod
def _restore_file(file_name, file_path):
@@ -143,20 +137,15 @@ class FileManagement:
"""
file_map = FileManagement._load_file_map()
backup_path = FileManagement.backup_folder_path
src_file = os.path.join(file_path, file_name)
src_file = os.path.normpath(os.path.join(file_path, file_name))
if src_file in file_map:
backup_file = os.path.join(backup_path, file_map[src_file])
if os.path.exists(backup_file):
fs.unlock_file(backup_file)
fs.unlock_file(src_file)
# Make temporary copy of backed up file to restore from
temp_file = "{}.bak".format(file_name)
FileManagement._copy_file(file_map[src_file], backup_path, temp_file, backup_path)
fs.restore_backup(src_file, backup_path)
fs.lock_file(src_file)
# Delete backup file
fs.delete([os.path.join(backup_path, temp_file)], True, False)
backup_file_name = file_map[src_file]
backup_file = "{}.bak".format(os.path.join(backup_path, backup_file_name))
fs.unlock_file(src_file)
if fs.restore_backup(src_file, backup_path, backup_file_name):
fs.delete([backup_file], True, False)
# Remove from file map
del file_map[src_file]
FileManagement._save_file_map(file_map)
@@ -12,7 +12,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS)
NAME AutomatedTesting::PrefabTests
TEST_SUITE main
TEST_SERIAL
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main.py
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main_Optimized.py
RUNTIME_DEPENDENCIES
Legacy::Editor
AZ::AssetProcessor
@@ -0,0 +1,52 @@
"""
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 pytest
from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, EditorParallelTest, EditorTestSuite
@pytest.mark.SUITE_main
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("project", ["AutomatedTesting"])
class TestAutomationNoAutoTestMode(EditorTestSuite):
# Enable only -BatchMode for these tests. Some tests cannot run in -autotest_mode due to UI interactions
global_extra_cmdline_args = ["-BatchMode"]
class test_CreatePrefab_UnderAnEntity(EditorSharedTest):
from .tests.create_prefab import CreatePrefab_UnderAnEntity as test_module
class test_CreatePrefab_UnderAnotherPrefab(EditorSharedTest):
from .tests.create_prefab import CreatePrefab_UnderAnotherPrefab as test_module
class test_DeleteEntity_UnderAnotherPrefab(EditorSharedTest):
from .tests.delete_entity import DeleteEntity_UnderAnotherPrefab as test_module
class test_DeleteEntity_UnderLevelPrefab(EditorSharedTest):
from .tests.delete_entity import DeleteEntity_UnderLevelPrefab as test_module
class test_ReparentPrefab_UnderAnotherPrefab(EditorSharedTest):
from .tests.reparent_prefab import ReparentPrefab_UnderAnotherPrefab as test_module
class test_DetachPrefab_UnderAnotherPrefab(EditorSharedTest):
from .tests.detach_prefab import DetachPrefab_UnderAnotherPrefab as test_module
class test_OpenLevel_ContainingTwoEntities(EditorSharedTest):
from .tests.open_level import OpenLevel_ContainingTwoEntities as test_module
class test_CreatePrefab_WithSingleEntity(EditorSharedTest):
from .tests.create_prefab import CreatePrefab_WithSingleEntity as test_module
class test_InstantiatePrefab_ContainingASingleEntity(EditorSharedTest):
from .tests.instantiate_prefab import InstantiatePrefab_ContainingASingleEntity as test_module
class test_DeletePrefab_ContainingASingleEntity(EditorSharedTest):
from .tests.delete_prefab import DeletePrefab_ContainingASingleEntity as test_module
class test_DuplicatePrefab_ContainingASingleEntity(EditorSharedTest):
from .tests.duplicate_prefab import DuplicatePrefab_ContainingASingleEntity as test_module
@@ -24,6 +24,7 @@ def CreatePrefab_WithSingleEntity():
# Creates a prefab from the new entity
Prefab.create_prefab(car_prefab_entities, CAR_PREFAB_FILE_NAME)
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(CreatePrefab_WithSingleEntity)
@@ -7,8 +7,8 @@ SPDX-License-Identifier: Apache-2.0 OR MIT
def DetachPrefab_UnderAnotherPrefab():
CAR_PREFAB_FILE_NAME = 'car_prefab'
WHEEL_PREFAB_FILE_NAME = 'wheel_prefab'
CAR_PREFAB_FILE_NAME = 'car_prefab2'
WHEEL_PREFAB_FILE_NAME = 'wheel_prefab2'
import editor_python_test_tools.pyside_utils as pyside_utils
@@ -12,12 +12,36 @@ import sys
import os
import pytest
import logging
import sqlite3
pytest.importorskip('ly_test_tools')
import ly_test_tools.environment.file_system as file_system
import ly_test_tools.log.log_monitor
import ly_test_tools.environment.waiter as waiter
def detect_product(sql_connection, platform, target):
cur = sql_connection.cursor()
product_target = f'{platform}/{target}'
print(f'Detecting {product_target} in assetdb.sqlite')
hits = 0
for row in cur.execute(f'select ProductID from Products where ProductName is "{product_target}"'):
hits = hits + 1
assert hits == 1
def find_products(cache_folder, platform):
con = sqlite3.connect(os.path.join(cache_folder, 'assetdb.sqlite'))
detect_product(con, platform, 'gem/pythontests/pythonassetbuilder/test_asset.mock_asset')
detect_product(con, platform, 'gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_z_positive_1.azmodel')
detect_product(con, platform, 'gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_z_negative_1.azmodel')
detect_product(con, platform, 'gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_y_positive_1.azmodel')
detect_product(con, platform, 'gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_y_negative_1.azmodel')
detect_product(con, platform, 'gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_x_positive_1.azmodel')
detect_product(con, platform, 'gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_x_negative_1.azmodel')
detect_product(con, platform, 'gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center_1.azmodel')
con.close()
@pytest.mark.SUITE_periodic
@pytest.mark.parametrize('launcher_platform', ['windows_editor'])
@pytest.mark.parametrize('project', ['AutomatedTesting'])
@@ -25,16 +49,7 @@ import ly_test_tools.environment.waiter as waiter
class TestPythonAssetProcessing(object):
def test_DetectPythonCreatedAsset(self, request, editor, level, launcher_platform):
unexpected_lines = []
expected_lines = [
'Mock asset exists',
'AssetId found for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_z_positive_1.azmodel) found',
'AssetId found for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_z_negative_1.azmodel) found',
'AssetId found for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_y_positive_1.azmodel) found',
'AssetId found for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_y_negative_1.azmodel) found',
'AssetId found for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_x_positive_1.azmodel) found',
'AssetId found for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_x_negative_1.azmodel) found',
'AssetId found for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center_1.azmodel) found'
]
expected_lines = []
timeout = 180
halt_on_unexpected = False
test_directory = os.path.join(os.path.dirname(__file__))
@@ -50,3 +65,9 @@ class TestPythonAssetProcessing(object):
exc=("Log file '{}' was never opened by another process.".format(editorlog_file)),
interval=1)
log_monitor.monitor_log_for_lines(expected_lines, unexpected_lines, halt_on_unexpected, timeout)
cache_folder = editor.workspace.paths.cache()
platform = editor.workspace.asset_processor_platform
if platform == 'windows':
platform = 'pc'
find_products(cache_folder, platform)
@@ -0,0 +1,144 @@
"""
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
"""
class HeightTests:
single_gradient_height_correct = (
"Successfully retrieved height for gradient1.",
"Failed to retrieve height for gradient1."
)
double_gradient_height_correct = (
"Successfully retrieved height when two gradients exist.",
"Failed to retrieve height when two gradients exist."
)
triple_gradient_height_correct = (
"Successfully retrieved height when three gradients exist.",
"Failed to retrieve height when three gradients exist."
)
terrain_data_changed_call_count_correct = (
"OnTerrainDataChanged called expected number of times.",
"OnTerrainDataChanged call count incorrect."
)
def TerrainHeightGradientList_AddRemoveGradientWorks():
"""
Summary:
Test aspects of the TerrainHeightGradientList through the BehaviorContext and the Property Tree.
:return: None
"""
import os
import math as sys_math
import azlmbr.legacy.general as general
import azlmbr.bus as bus
import azlmbr.math as math
import azlmbr.terrain as terrain
import azlmbr.editor as editor
import azlmbr.vegetation as vegetation
import azlmbr.entity as EntityId
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
import editor_python_test_tools.pyside_utils as pyside_utils
from editor_python_test_tools.editor_entity_utils import EditorEntity
terrain_changed_call_count = 0
expected_terrain_changed_calls = 0
aabb_component_name = "Axis Aligned Box Shape"
gradientlist_component_name = "Terrain Height Gradient List"
layerspawner_component_name = "Terrain Layer Spawner"
gradient_value_path = "Configuration|Value"
def create_entity_at(entity_name, components_to_add, x, y, z):
entity = hydra.Entity(entity_name)
entity.create_entity(math.Vector3(x, y, z), components_to_add)
return entity
def on_terrain_changed(args):
nonlocal terrain_changed_call_count
terrain_changed_call_count += 1
def set_component_path_val(entity, component, path, value):
entity.get_set_test(component, path, value)
def set_gradients_check_height(main_entity, gradient_list, expected_height, test_results):
nonlocal expected_terrain_changed_calls
test_tolerance = 0.01
gradient_list_path = "Configuration|Gradient Entities"
set_component_path_val(main_entity, 1, gradient_list_path, gradient_list)
expected_terrain_changed_calls += 1
# Wait until the terrain data has been updated.
helper.wait_for_condition(lambda: terrain_changed_call_count == expected_terrain_changed_calls, 2.0)
# Get the height at the origin.
height = terrain.TerrainDataRequestBus(bus.Broadcast, "GetHeightFromFloats", 0.0, 0.0, 0)
Report.result(test_results, sys_math.isclose(height, expected_height, abs_tol=test_tolerance))
helper.init_idle()
# Open a level.
helper.open_level("Physics", "Base")
helper.wait_for_condition(lambda: general.get_current_level_name() == "Base", 2.0)
general.idle_wait_frames(1)
# Add a terrain world component
world_component = hydra.add_level_component("Terrain World")
aabb_height = 1024.0
box_dimensions = math.Vector3(1.0, 1.0, aabb_height);
# Create a main entity with a LayerSpawner, AAbb and HeightGradientList.
main_entity = create_entity_at("entity2", [layerspawner_component_name, gradientlist_component_name, aabb_component_name], 0.0, 0.0, aabb_height/2.0)
# Create three gradient entities.
gradient_entity1 = create_entity_at("Constant Gradient1", ["Constant Gradient"], 0.0, 0.0, 0.0);
gradient_entity2 = create_entity_at("Constant Gradient2", ["Constant Gradient"], 0.0, 0.0, 0.0);
gradient_entity3 = create_entity_at("Constant Gradient3", ["Constant Gradient"], 0.0, 0.0, 0.0);
# Give everything a chance to finish initializing.
general.idle_wait_frames(1)
# Set the gradients to different values.
gradient_values = [0.5, 0.8, 0.3]
set_component_path_val(gradient_entity1, 0, gradient_value_path, gradient_values[0])
set_component_path_val(gradient_entity2, 0, gradient_value_path, gradient_values[1])
set_component_path_val(gradient_entity3, 0, gradient_value_path, gradient_values[2])
# Give the TerrainSystem time to tick.
general.idle_wait_frames(1)
# Set the dimensions of the Aabb.
set_component_path_val(main_entity, 2, "Axis Aligned Box Shape|Box Configuration|Dimensions", box_dimensions)
# Set up a handler to wait for notifications from the TerrainSystem.
handler = azlmbr.terrain.TerrainDataNotificationBusHandler()
handler.connect()
handler.add_callback("OnTerrainDataChanged", on_terrain_changed)
# Add a gradient to GradientList, then check the height returned from the TerrainSystem is correct.
set_gradients_check_height(main_entity, [gradient_entity1.id], aabb_height * gradient_values[0], HeightTests.single_gradient_height_correct)
# Add gradient2 and check height at the origin, this should have changed to match the second gradient value.
set_gradients_check_height(main_entity, [gradient_entity1.id, gradient_entity2.id], aabb_height * gradient_values[1], HeightTests.double_gradient_height_correct)
# Add gradient3, the height should still be the second value, as that was the highest.
set_gradients_check_height(main_entity, [gradient_entity1.id, gradient_entity2.id, gradient_entity3.id], aabb_height * gradient_values[1], HeightTests.triple_gradient_height_correct)
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(TerrainHeightGradientList_AddRemoveGradientWorks)
@@ -0,0 +1,166 @@
"""
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
"""
class MacroMaterialTests:
setup_test = (
"Setup successful",
"Setup failed"
)
material_changed_not_called_when_inactive = (
"OnTerrainMacroMaterialRegionChanged not called successfully",
"OnTerrainMacroMaterialRegionChanged called when component inactive."
)
material_created = (
"MaterialCreated called successfully",
"MaterialCreated failed"
)
material_destroyed = (
"MaterialDestroyed called successfully",
"MaterialDestroyed failed"
)
material_recreated = (
"MaterialCreated called successfully on second test",
"MaterialCreated failed on second test"
)
material_changed_call_on_aabb_change = (
"OnTerrainMacroMaterialRegionChanged called successfully",
"Timed out waiting for OnTerrainMacroMaterialRegionChanged"
)
def TerrainMacroMaterialComponent_MacroMaterialActivates():
"""
Summary:
Load an empty level, create a MacroMaterialComponent and check assigning textures results in the correct callbacks.
:return: None
"""
import os
import math as sys_math
import azlmbr.legacy.general as general
import azlmbr.asset as asset
import azlmbr.bus as bus
import azlmbr.math as math
import azlmbr.terrain as terrain
import azlmbr.editor as editor
import azlmbr.vegetation as vegetation
import azlmbr.entity as EntityId
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
import editor_python_test_tools.pyside_utils as pyside_utils
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.asset_utils import Asset
material_created_called = False
material_changed_called = False
material_region_changed_called = False
material_destroyed_called = False
def create_entity_at(entity_name, components_to_add, x, y, z):
entity = EditorEntity.create_editor_entity_at([x, y, z], entity_name)
for component in components_to_add:
entity.add_component(component)
return entity
def on_macro_material_created(args):
nonlocal material_created_called
material_created_called = True
def on_macro_material_changed(args):
nonlocal material_changed_called
material_changed_called = True
def on_macro_material_region_changed(args):
nonlocal material_region_changed_called
material_region_changed_called = True
def on_macro_material_destroyed(args):
nonlocal material_destroyed_called
material_destroyed_called = True
helper.init_idle()
# Open a level.
helper.open_level("Physics", "Base")
helper.wait_for_condition(lambda: general.get_current_level_name() == "Base", 2.0)
general.idle_wait_frames(1)
# Set up a handler to wait for notifications from the TerrainSystem.
handler = terrain.TerrainMacroMaterialAutomationBusHandler()
handler.connect()
handler.add_callback("OnTerrainMacroMaterialCreated", on_macro_material_created)
handler.add_callback("OnTerrainMacroMaterialChanged", on_macro_material_changed)
handler.add_callback("OnTerrainMacroMaterialRegionChanged", on_macro_material_region_changed)
handler.add_callback("OnTerrainMacroMaterialDestroyed", on_macro_material_destroyed)
macro_material_entity = create_entity_at("macro", ["Terrain Macro Material", "Axis Aligned Box Shape"], 0.0, 0.0, 0.0)
# Check that no macro material callbacks happened. It should be "inactive" as it has no assets assigned.
setup_success = not material_created_called and not material_changed_called and not material_region_changed_called and not material_destroyed_called
Report.result(MacroMaterialTests.setup_test, setup_success)
# Find the aabb component.
aabb_component_type_id_type = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'FindComponentTypeIdsByEntityType', ["Axis Aligned Box Shape"], 0)[0]
aabb_component_id = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'GetComponentOfType', macro_material_entity.id, aabb_component_type_id_type).GetValue()
# Change the aabb dimensions
material_region_changed_called = False
box_dimensions_path = "Axis Aligned Box Shape|Box Configuration|Dimensions"
editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", aabb_component_id, box_dimensions_path, math.Vector3(1.0, 1.0, 1.0))
# Check we don't receive a callback. The macro material component should be inactive as it has no images assigned.
general.idle_wait_frames(1)
Report.result(MacroMaterialTests.material_changed_not_called_when_inactive, material_region_changed_called == False)
# Find the macro material component.
macro_material_id_type = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'FindComponentTypeIdsByEntityType', ["Terrain Macro Material"], 0)[0]
macro_material_component_id = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'GetComponentOfType', macro_material_entity.id, macro_material_id_type).GetValue()
# Find a color image asset.
color_image_path = os.path.join("assets", "textures", "image.png.streamingimage")
color_image_asset = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", color_image_path, math.Uuid(), False)
# Assign the image to the MacroMaterial component, which should result in a created message.
material_created_called = False
color_texture_path = "Configuration|Color Texture"
editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", macro_material_component_id, color_texture_path, color_image_asset)
call_result = helper.wait_for_condition(lambda: material_created_called == True, 2.0)
Report.result(MacroMaterialTests.material_created, call_result)
# Find a normal image asset.
normal_image_path = os.path.join("assets", "textures", "normal.png.streamingimage")
normal_image_asset = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", normal_image_path, math.Uuid(), False)
# Assign the normal image to the MacroMaterial component, which should result in a created message.
material_created_called = False
material_destroyed_called = False
normal_texture_path = "Configuration|Normal Texture"
editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", macro_material_component_id, normal_texture_path, normal_image_asset)
# Check the MacroMaterial was destroyed and recreated.
destroyed_call_result = helper.wait_for_condition(lambda: material_destroyed_called == True, 2.0)
Report.result(MacroMaterialTests.material_destroyed, destroyed_call_result)
recreated_call_result = helper.wait_for_condition(lambda: material_created_called == True, 2.0)
Report.result(MacroMaterialTests.material_recreated, recreated_call_result)
# Change the aabb dimensions.
box_dimensions_path = "Axis Aligned Box Shape|Box Configuration|Dimensions"
editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", aabb_component_id, box_dimensions_path, math.Vector3(1.0, 1.0, 1.0))
# Check that a callback is received.
region_changed_call_result = helper.wait_for_condition(lambda: material_region_changed_called == True, 2.0)
Report.result(MacroMaterialTests.material_changed_call_on_aabb_change, region_changed_call_result)
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(TerrainMacroMaterialComponent_MacroMaterialActivates)
@@ -0,0 +1,214 @@
"""
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
"""
class VegetationTests:
vegetation_on_gradient_1 = (
"Vegetation detected at correct position on Gradient1",
"Vegetation not detected at correct position on Gradient1"
)
vegetation_on_gradient_2 = (
"Vegetation detected at correct position on Gradient2",
"Vegetation not detected at correct position on Gradient2"
)
unfiltered_vegetation_count_correct = (
"Unfiltered vegetation spawn count correct",
"Unfiltered vegetation spawn count incorrect"
)
testTag2_excluded_vegetation_count_correct = (
"TestTag2 filtered vegetation count correct",
"TestTag2 filtered vegetation count incorrect"
)
testTag2_excluded_vegetation_z_correct = (
"TestTag2 filtered vegetation spawned in correct position",
"TestTag2 filtered vegetation failed to spawn in correct position"
)
testTag3_excluded_vegetation_count_correct = (
"TestTag3 filtered vegetation count correct",
"TestTag3 filtered vegetation count incorrect"
)
testTag3_excluded_vegetation_z_correct = (
"TestTag3 filtered vegetation spawned in correct position",
"TestTag3 filtered vegetation failed to spawn in correct position"
)
cleared_exclusion_vegetation_count_correct = (
"Cleared filter vegetation count correct",
"Cleared filter vegetation count incorrect"
)
def TerrainSystem_VegetationSpawnsOnTerrainSurfaces():
"""
Summary:
Load an empty level,
Create two entities with constant gradient components with different values.
Create two entities with TerrainLayerSpawners
Create an entity to spawn vegetation
Ensure that vegetation spawns at the correct heights
Add a VegetationSurfaceMaskFilter and ensure it responds correctly to surface changes.
:return: None
"""
import os
import sys
import math as sys_math
import azlmbr.legacy.general as general
import azlmbr.bus as bus
import azlmbr.math as math
import azlmbr.areasystem as areasystem
import azlmbr.editor as editor
import azlmbr.vegetation as vegetation
import azlmbr.terrain as terrain
import azlmbr.entity as EntityId
import azlmbr.surface_data as surface_data
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
def create_entity_at(entity_name, components_to_add, x, y, z):
entity = hydra.Entity(entity_name)
entity.create_entity(math.Vector3(x, y, z), components_to_add)
return entity
def FindHighestAndLowestZValuesInArea(aabb):
vegetation_items = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', aabb)
lowest_z = min([item.position.z for item in vegetation_items])
highest_z = max([item.position.z for item in vegetation_items])
return highest_z, lowest_z
helper.init_idle()
# Open an empty level.
helper.open_level("Physics", "Base")
helper.wait_for_condition(lambda: general.get_current_level_name() == "Base", 2.0)
general.idle_wait_frames(1)
box_height = 20.0
box_y_position = 10.0
box_dimensions = math.Vector3(20.0, 20.0, box_height)
# Add Terrain Rendering
hydra.add_level_component("Terrain World")
hydra.add_level_component("Terrain World Renderer")
# Create two terrain entities at adjoining positions
terrain_entity_1 = create_entity_at("Terrain1", ["Terrain Layer Spawner", "Axis Aligned Box Shape", "Terrain Height Gradient List", "Terrain Surface Gradient List"], 0.0, box_y_position, box_height/2.0)
terrain_entity_1.get_set_test(1, "Axis Aligned Box Shape|Box Configuration|Dimensions", box_dimensions)
terrain_entity_2 = create_entity_at("Terrain2", ["Terrain Layer Spawner", "Axis Aligned Box Shape", "Terrain Height Gradient List", "Terrain Surface Gradient List"], 20.0, box_y_position, box_height/2.0)
terrain_entity_2.get_set_test(1, "Axis Aligned Box Shape|Box Configuration|Dimensions", box_dimensions)
# Create two gradient entities.
gradient_value_1 = 0.25
gradient_value_2 = 0.5
gradient_entity_1 = create_entity_at("Gradient1", ["Constant Gradient"], 0.0, 0.0, 0.0)
gradient_entity_1.get_set_test(0, "Configuration|Value", gradient_value_1)
gradient_entity_2 = create_entity_at("Gradient2", ["Constant Gradient"], 0.0, 0.0, 0.0)
gradient_entity_2.get_set_test(0, "Configuration|Value", gradient_value_2)
mapping = terrain.TerrainSurfaceGradientMapping()
mapping.gradientEntityId = gradient_entity_1.id
pte = hydra.get_property_tree(terrain_entity_1.components[3])
pte.add_container_item("Configuration|Gradient to Surface Mappings", 0, mapping)
mapping = terrain.TerrainSurfaceGradientMapping()
mapping.gradientEntityId = gradient_entity_2.id
pte = hydra.get_property_tree(terrain_entity_2.components[3])
pte.add_container_item("Configuration|Gradient to Surface Mappings", 0, mapping)
# create a vegetation entity that overlaps both terrain entities.
vegetation_entity = create_entity_at("Vegetation", ["Vegetation Layer Spawner", "Axis Aligned Box Shape", "Vegetation Asset List", "Vegetation Surface Mask Filter"], 10.0, box_y_position, box_height/2.0)
vegetation_entity.get_set_test(1, "Axis Aligned Box Shape|Box Configuration|Dimensions", box_dimensions)
# Set the vegetation area to a PrefabInstanceSpawner with a specific prefab asset selected.
prefab_spawner = vegetation.PrefabInstanceSpawner()
prefab_spawner.SetPrefabAssetPath(os.path.join("Prefabs", "PinkFlower.spawnable"))
descriptor = hydra.get_component_property_value(vegetation_entity.components[2], 'Configuration|Embedded Assets|[0]')
descriptor.spawner = prefab_spawner
vegetation_entity.get_set_test(2, "Configuration|Embedded Assets|[0]", descriptor)
# Assign gradients to layer spawners.
terrain_entity_1.get_set_test(2, "Configuration|Gradient Entities", [gradient_entity_1.id])
terrain_entity_2.get_set_test(2, "Configuration|Gradient Entities", [gradient_entity_2.id])
# Move view so that the entities are visible.
general.set_current_view_position(17.0, -66.0, 41.0)
general.set_current_view_rotation(-15, 0, 0)
# Expected item counts under conditions to be tested.
# By default, vegetation spawns at a density of 20 items per 16 meters,
# so in a 20m square, there should be around 25 ^ 2 items depending on whether area edges are included.
# In this case there are 26 ^ 2 items.
expected_surface_tag_excluded_item_count = 338
expected_no_exclusions_item_count = 676
# Wait for the vegetation to spawn
helper.wait_for_condition(lambda: vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id) == expected_no_exclusions_item_count, 5.0)
# Check the spawn count is correct.
item_count = vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id)
Report.result(VegetationTests.unfiltered_vegetation_count_correct, item_count == expected_no_exclusions_item_count)
test_aabb = math.Aabb_CreateFromMinMax(math.Vector3(-10.0, -10.0, 0.0), math.Vector3(30.0, 10.0, box_height))
# Find the z positions of the items with the lowest and highest x values, this will avoid the overlap area where z values are blended between the surface heights.
highest_z, lowest_z = FindHighestAndLowestZValuesInArea(test_aabb)
# Check that the z values are as expected.
Report.result(VegetationTests.vegetation_on_gradient_1, sys_math.isclose(lowest_z, box_height * gradient_value_1, abs_tol=0.01))
Report.result(VegetationTests.vegetation_on_gradient_2, sys_math.isclose(highest_z, box_height * gradient_value_2, abs_tol=0.01))
# Assign SurfaceTags to the SurfaceGradientLists
terrain_entity_1.get_set_test(3, "Configuration|Gradient to Surface Mappings|[0]|Surface Tag", surface_data.SurfaceTag("test_tag2"))
terrain_entity_2.get_set_test(3, "Configuration|Gradient to Surface Mappings|[0]|Surface Tag", surface_data.SurfaceTag("test_tag3"))
# Give the VegetationSurfaceFilter an exclusion list, set it to exclude test_tag2 which should remove all the lower items which are in terrain_entity_1.
vegetation_entity.get_set_test(3, "Configuration|Exclusion|Surface Tags", [surface_data.SurfaceTag()])
vegetation_entity.get_set_test(3, "Configuration|Exclusion|Surface Tags|[0]", surface_data.SurfaceTag("test_tag2"))
# Wait for the vegetation to respawn and check z values.
helper.wait_for_condition(lambda: vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id) == expected_surface_tag_excluded_item_count, 5.0)
item_count = vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id)
Report.result(VegetationTests.testTag2_excluded_vegetation_count_correct, item_count == expected_surface_tag_excluded_item_count)
highest_z, lowest_z = FindHighestAndLowestZValuesInArea(test_aabb)
Report.result(VegetationTests.testTag2_excluded_vegetation_z_correct, lowest_z > box_height * gradient_value_1)
# Clear the filter and ensure vegetation respawns.
vegetation_entity.get_set_test(3, "Configuration|Exclusion|Surface Tags|[0]", surface_data.SurfaceTag("invalid"))
helper.wait_for_condition(lambda: vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id) == expected_no_exclusions_item_count, 5.0)
item_count = vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id)
Report.result(VegetationTests.cleared_exclusion_vegetation_count_correct, item_count == expected_no_exclusions_item_count)
# Exclude test_tag3 to exclude the higher items in terrain_entity_2 and recheck.
vegetation_entity.get_set_test(3, "Configuration|Exclusion|Surface Tags|[0]", surface_data.SurfaceTag("test_tag3"))
helper.wait_for_condition(lambda: vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id) == expected_surface_tag_excluded_item_count, 5.0)
item_count = vegetation.VegetationSpawnerRequestBus(bus.Event, "GetAreaProductCount", vegetation_entity.id)
Report.result(VegetationTests.testTag3_excluded_vegetation_count_correct, item_count == expected_surface_tag_excluded_item_count)
highest_z, lowest_z = FindHighestAndLowestZValuesInArea(test_aabb)
Report.result(VegetationTests.testTag3_excluded_vegetation_z_correct, highest_z < box_height * gradient_value_2)
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(TerrainSystem_VegetationSpawnsOnTerrainSurfaces)
@@ -0,0 +1,168 @@
"""
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():
level_components_added = ("Level components added correctly", "Failed to create level components")
create_terrain_spawner_entity = ("Terrain_spawner_entity created successfully", "Failed to create terrain_spawner_entity")
create_height_provider_entity = ("Height_provider_entity created successfully", "Failed to create height_provider_entity")
bounds_max_changed = ("Terrain World Bounds Max changed successfully", "Failed to change Terrain World Bounds Max")
bounds_min_changed = ("Terrain World Bounds Min changed successfully", "Failed to change Terrain World Bounds Min")
height_query_changed = ("Terrain World Height Query Resolution changed successfully", "Failed to change Height Query Resolution")
box_dimensions_changed = ("Aabb dimensions changed successfully", "Failed to change Aabb dimensions")
shape_changed = ("Shape changed successfully", "Failed Shape change")
frequency_changed = ("Frequency changed successfully", "Failed Frequency change")
entity_added = ("Entity added successfully", "Failed Entity add")
terrain_exists = ("Terrain exists at the provided point", "Terrain does not exist at the provided point")
terrain_does_not_exist = ("Terrain does not exist at the provided point", "Terrain exists at the provided point")
values_not_the_same = ("The tested values are not the same", "The tested values are the same")
no_errors_and_warnings_found = ("No errors and warnings found", "Found errors and warnings")
#fmt: on
def Terrain_World_ConfigurationWorks():
"""
Summary:
Test the Terrain World configuration changes when parameters are changed in the component
Test Steps:
Expected Behavior:
The Editor is stable there are no warnings or errors.
Test Steps:
1) Start the Tracer to catch any errors and warnings
2) Load the base level
3) Load the level components
4) Create 2 test entities, one parent at 512.0, 512.0, 50.0 and one child at the default position and add the required components
5) Set the base Terrain World values
6) Change the Axis Aligned Box Shape dimensions
7) Set the Shape Reference to terrain_spawner_entity
8) Set the FastNoise Gradient frequency to 0.01
9) Set the Gradient List to height_provider_entity
10) Disable and Enable the Terrain Gradient List so that it is recognised
11) Check terrain exists at a known position in the world
12) Check terrain does not exist at a known position outside the world
13) Check height value is the expected one when query resolution is changed
"""
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import TestHelper as helper, Report
from editor_python_test_tools.utils import Report, Tracer
import editor_python_test_tools.hydra_editor_utils as hydra
import azlmbr.math as azmath
import azlmbr.legacy.general as general
import azlmbr.bus as bus
import azlmbr.editor as editor
import azlmbr.terrain as terrain
import math
SET_BOX_X_SIZE = 2048.0
SET_BOX_Y_SIZE = 2048.0
SET_BOX_Z_SIZE = 100.0
CLAMP = 1
helper.init_idle()
# 1) Start the Tracer to catch any errors and warnings
with Tracer() as section_tracer:
# 2) Load the level
helper.open_level("", "Base")
helper.wait_for_condition(lambda: general.get_current_level_name() == "Base", 2.0)
# 3) Load the level components
terrain_world_component = hydra.add_level_component("Terrain World")
terrain_world_renderer = hydra.add_level_component("Terrain World Renderer")
Report.critical_result(Tests.level_components_added,
terrain_world_component is not None and terrain_world_renderer is not None)
# 4) Create 2 test entities, one parent at 512.0, 512.0, 50.0 and one child at the default position and add the required components
entity1_components_to_add = ["Axis Aligned Box Shape", "Terrain Layer Spawner", "Terrain Height Gradient List", "Terrain Physics Heightfield Collider"]
entity2_components_to_add = ["Shape Reference", "Gradient Transform Modifier", "FastNoise Gradient"]
terrain_spawner_entity = hydra.Entity("TerrainEntity")
terrain_spawner_entity.create_entity(azmath.Vector3(512.0, 512.0, 50.0), entity1_components_to_add)
Report.result(Tests.create_terrain_spawner_entity, terrain_spawner_entity.id.IsValid())
height_provider_entity = hydra.Entity("HeightProviderEntity")
height_provider_entity.create_entity(azmath.Vector3(0.0, 0.0, 0.0), entity2_components_to_add,terrain_spawner_entity.id)
Report.result(Tests.create_height_provider_entity, height_provider_entity.id.IsValid())
# Give everything a chance to finish initializing.
general.idle_wait_frames(1)
# 5) Set the base Terrain World values
world_bounds_max = azmath.Vector3(1100.0, 1100.0, 1100.0)
world_bounds_min = azmath.Vector3(10.0, 10.0, 10.0)
height_query_resolution = azmath.Vector2(1.0, 1.0)
hydra.set_component_property_value(terrain_world_component, "Configuration|World Bounds (Max)", world_bounds_max)
hydra.set_component_property_value(terrain_world_component, "Configuration|World Bounds (Min)", world_bounds_min)
hydra.set_component_property_value(terrain_world_component, "Configuration|Height Query Resolution (m)", height_query_resolution)
world_max = hydra.get_component_property_value(terrain_world_component, "Configuration|World Bounds (Max)")
world_min = hydra.get_component_property_value(terrain_world_component, "Configuration|World Bounds (Min)")
world_query = hydra.get_component_property_value(terrain_world_component, "Configuration|Height Query Resolution (m)")
Report.result(Tests.bounds_max_changed, world_max == world_bounds_max)
Report.result(Tests.bounds_min_changed, world_min == world_bounds_min)
Report.result(Tests.height_query_changed, world_query == height_query_resolution)
# 6) Change the Axis Aligned Box Shape dimensions
box_dimensions = azmath.Vector3(SET_BOX_X_SIZE, SET_BOX_Y_SIZE, SET_BOX_Z_SIZE)
terrain_spawner_entity.get_set_test(0, "Axis Aligned Box Shape|Box Configuration|Dimensions", box_dimensions)
box_shape_dimensions = hydra.get_component_property_value(terrain_spawner_entity.components[0], "Axis Aligned Box Shape|Box Configuration|Dimensions")
Report.result(Tests.box_dimensions_changed, box_dimensions == box_shape_dimensions)
# 7) Set the Shape Reference to terrain_spawner_entity
height_provider_entity.get_set_test(0, "Configuration|Shape Entity Id", terrain_spawner_entity.id)
entityId = hydra.get_component_property_value(height_provider_entity.components[0], "Configuration|Shape Entity Id")
Report.result(Tests.shape_changed, entityId == terrain_spawner_entity.id)
# 8) Set the FastNoise Gradient frequency to 0.01
frequency = 0.01
height_provider_entity.get_set_test(2, "Configuration|Frequency", frequency)
frequencyVal = hydra.get_component_property_value(height_provider_entity.components[2], "Configuration|Frequency")
Report.result(Tests.frequency_changed, math.isclose(frequency, frequencyVal, abs_tol = 0.00001))
# 9) Set the Gradient List to height_provider_entity
propertyTree = hydra.get_property_tree(terrain_spawner_entity.components[2])
propertyTree.add_container_item("Configuration|Gradient Entities", 0, height_provider_entity.id)
checkID = propertyTree.get_container_item("Configuration|Gradient Entities", 0)
Report.result(Tests.entity_added, checkID.GetValue() == height_provider_entity.id)
general.idle_wait_frames(1)
# 10) Disable and Enable the Terrain Gradient List so that it is recognised, EnableComponents performs both actions.
editor.EditorComponentAPIBus(bus.Broadcast, 'EnableComponents', [terrain_spawner_entity.components[2]])
# 11) Check terrain exists at a known position in the world
terrainExists = not terrain.TerrainDataRequestBus(bus.Broadcast, 'GetIsHoleFromFloats', 10.0, 10.0, CLAMP)
Report.result(Tests.terrain_exists, terrainExists)
terrainExists = not terrain.TerrainDataRequestBus(bus.Broadcast, 'GetIsHoleFromFloats', 1100.0, 1100.0, CLAMP)
Report.result(Tests.terrain_exists, terrainExists)
# 12) Check terrain does not exist at a known position outside the world
terrainDoesNotExist = terrain.TerrainDataRequestBus(bus.Broadcast, 'GetIsHoleFromFloats', 1101.0, 1101.0, CLAMP)
Report.result(Tests.terrain_does_not_exist, terrainDoesNotExist)
terrainDoesNotExist = terrain.TerrainDataRequestBus(bus.Broadcast, 'GetIsHoleFromFloats', 9.0, 9.0, CLAMP)
Report.result(Tests.terrain_does_not_exist, terrainDoesNotExist)
# 13) Check height value is the expected one when query resolution is changed
testpoint = terrain.TerrainDataRequestBus(bus.Broadcast, 'GetHeightFromFloats', 10.5, 10.5, CLAMP)
height_query_resolution = azmath.Vector2(0.5, 0.5)
hydra.set_component_property_value(terrain_world_component, "Configuration|Height Query Resolution (m)", height_query_resolution)
general.idle_wait_frames(1)
testpoint2 = terrain.TerrainDataRequestBus(bus.Broadcast, 'GetHeightFromFloats', 10.5, 10.5, CLAMP)
Report.result(Tests.values_not_the_same, not math.isclose(testpoint, testpoint2, abs_tol = 0.000000001))
helper.wait_for_condition(lambda: section_tracer.has_errors or section_tracer.has_asserts, 1.0)
for error_info in section_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in section_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(Terrain_World_ConfigurationWorks)
@@ -27,3 +27,15 @@ class TestAutomation(EditorTestSuite):
class test_Terrain_SupportsPhysics(EditorSharedTest):
from .EditorScripts import Terrain_SupportsPhysics as test_module
class test_TerrainHeightGradientList_AddRemoveGradientWorks(EditorSharedTest):
from .EditorScripts import TerrainHeightGradientList_AddRemoveGradientWorks as test_module
class test_TerrainSystem_VegetationSpawnsOnTerrainSurfaces(EditorSharedTest):
from .EditorScripts import TerrainSystem_VegetationSpawnsOnTerrainSurfaces as test_module
class test_TerrainMacroMaterialComponent_MacroMaterialActivates(EditorSharedTest):
from .EditorScripts import TerrainMacroMaterialComponent_MacroMaterialActivates as test_module
class test_TerrainWorld_ConfigurationWorks(EditorSharedTest):
from .EditorScripts import Terrain_World_ConfigurationWorks as test_module
@@ -3,8 +3,6 @@ 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
Fixture for clearing out 'MoveOutput' folders from \dev and \dev\PROJECT
"""
# Import builtin libraries
@@ -108,7 +108,7 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
"""
helper = bundler_batch_helper
seed_list = os.path.join(workspace.paths.engine_root(), "Assets", "Engine", "SeedAssetList.seed") # Engine seed list
asset = r"levels\testdependencieslevel\level.pak"
asset = r"levels\testdependencieslevel\testdependencieslevel.spawnable"
# Create Asset list
helper.call_assetLists(
@@ -191,7 +191,7 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
"""
helper = bundler_batch_helper
seed_list = os.path.join(workspace.paths.engine_root(), "Assets", "Engine", "SeedAssetList.seed") # Engine seed list
asset = r"levels\testdependencieslevel\level.pak"
asset = r"levels\testdependencieslevel\testdependencieslevel.spawnable"
# Useful bundle locations / names (2 for comparing contents)
# fmt:off
@@ -924,7 +924,7 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
# Create a seed file
helper.call_seeds(
seedListFile=helper["seed_list_file"],
addSeed=r"levels\testdependencieslevel\level.pak",
addSeed=r"levels\testdependencieslevel\testdependencieslevel.spawnable",
platform="pc",
)
@@ -947,9 +947,9 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
# Specifying platform but not "add" or "remove" should fail
result, _ = helper.call_assetLists(
assetListFile=helper["asset_info_file_request"],
allowOverwrites="",
seedListFile=helper["seed_list_file"],
platform="pc",
allowOverwrites="",
)
assert result, "Overwriting with override threw an error"
@@ -982,7 +982,7 @@ class TestsAssetBundlerBatch_WindowsAndMac(object):
request.addfinalizer(lambda: fs.delete([bundle_result_path], True, False))
bundles_folder = os.path.join(workspace.paths.project(), "Bundles")
level_pak = r"levels\testdependencieslevel\level.pak"
level_pak = r"levels\testdependencieslevel\testdependencieslevel.spawnable"
bundle_request_path = os.path.join(bundles_folder, "bundle.pak")
bundle_result_path = os.path.join(bundles_folder,
helper.platform_file_name("bundle.pak", workspace.asset_processor_platform))
@@ -8,6 +8,8 @@ General Asset Processor Batch Tests
"""
# Import builtin libraries
from os import listdir
import pytest
import logging
import os
@@ -724,3 +726,11 @@ class TestsAssetProcessorBatch_AllPlatforms(object):
assert error_line_found, "The error could not be found in the newest run of the AP Batch log."
@pytest.mark.assetpipeline
def test_AssetProcessor_Log_On_Failure(self, asset_processor, ap_setup_fixture, workspace):
asset_processor.prepare_test_environment(ap_setup_fixture["tests_dir"], "test_AP_Logs")
result, output = asset_processor.batch_process(expect_failure=True, capture_output=True)
assert result == False, f'AssetProcessorBatch should have failed because there is a bad asset, output was {output}'
jobLogs = listdir(workspace.paths.ap_job_logs() + "/test_AP_Logs")
assert not len(jobLogs) == 0, 'No job logs where output during failure.'
@@ -169,17 +169,17 @@ class TestsAssetRelocator_WindowsAndMac(object):
@pytest.mark.test_case_id("C21968388")
@pytest.mark.assetpipeline
def test_WindowsMacPlatforms_MoveCorruptedSliceFile_MoveSuccess(self, request, workspace, ap_setup_fixture,
def test_WindowsMacPlatforms_MoveCorruptedPrefabFile_MoveSuccess(self, request, workspace, ap_setup_fixture,
asset_processor):
"""
Asset with UUID/AssetId reference in non-standard format is
successfully scanned and relocated to the MoveOutput folder.
This test uses a pre-corrupted .slice file.
This test uses a pre-corrupted .prefab file.
Test Steps:
1. Create temporary testing environment with a corrupted slice
2. Attempt to move the corrupted slice
3. Verify that corrupted slice was moved successfully
1. Create temporary testing environment with a corrupted prefab
2. Attempt to move the corrupted prefab
3. Verify that corrupted prefab was moved successfully
"""
env = ap_setup_fixture
@@ -187,7 +187,7 @@ class TestsAssetRelocator_WindowsAndMac(object):
asset_folder = "C21968388"
source_dir, _ = asset_processor.prepare_test_environment(env["tests_dir"], asset_folder)
filename = "DependencyScannerAsset.slice"
filename = "DependencyScannerAsset.prefab"
file_path = os.path.join(source_dir, filename)
dst_rel_path = os.path.join("MoveOutput", filename)
dst_full_path = os.path.join(source_dir, dst_rel_path)
@@ -0,0 +1,908 @@
{
"ContainerEntity": {
"Id": "ContainerEntity",
"Name": "DependencyScannerAsset",
"Components": {
"Component_[10182366347512475253]": {
"$type": "EditorPrefabComponent",
"Id": 10182366347512475253
},
"Component_[12917798267488243668]": {
"$type": "EditorPendingCompositionComponent",
"Id": 12917798267488243668
},
"Component_[3261249813163778338]": {
"$type": "EditorOnlyEntityComponent",
"Id": 3261249813163778338
},
"Component_[3837204912784440039]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 3837204912784440039
},
"Component_[4272963378099646759]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 4272963378099646759,
"Parent Entity": ""
},
"Component_[4848458548047175816]": {
"$type": "EditorVisibilityComponent",
"Id": 4848458548047175816
},
"Component_[5787060997243919943]": {
"$type": "EditorInspectorComponent",
"Id": 5787060997243919943
},
"Component_[7804170251266531779]": {
"$type": "EditorLockComponent",
"Id": 7804170251266531779
},
"Component_[7874177159288365422]": {
"$type": "EditorEntitySortComponent",
"Id": 7874177159288365422
},
"Component_[8018146290632383969]": {
"$type": "EditorEntityIconComponent",
"Id": 8018146290632383969
},
"Component_[8452360690590857075]": {
"$type": "SelectionComponent",
"Id": 8452360690590857075
}
}
},
"Entities": {
"Entity_[274004659287]": {
"Id": "Entity_[274004659287]",
"Name": "DependencyScannerAsset",
"Components": {
"Component_[10849460799799271301]": {
"$type": "EditorPendingCompositionComponent",
"Id": 10849460799799271301
},
"Component_[11098142762746045658]": {
"$type": "SelectionComponent",
"Id": 11098142762746045658
},
"Component_[11154538629717040387]": {
"$type": "EditorEntitySortComponent",
"Id": 11154538629717040387,
"Child Entity Order": [
"Entity_[305822185575]",
"Entity_[278299626583]",
"Entity_[282594593879]",
"Entity_[286889561175]",
"Entity_[291184528471]"
]
},
"Component_[1365196255752273753]": {
"$type": "EditorOnlyEntityComponent",
"Id": 1365196255752273753
},
"Component_[280906579560376421]": {
"$type": "EditorLockComponent",
"Id": 280906579560376421
},
"Component_[4629965429001113748]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 4629965429001113748
},
"Component_[4876910656129741263]": {
"$type": "EditorEntityIconComponent",
"Id": 4876910656129741263
},
"Component_[5763306492614623496]": {
"$type": "EditorInspectorComponent",
"Id": 5763306492614623496,
"ComponentOrderEntryArray": [
{
"ComponentId": 7514977506847036117
}
]
},
"Component_[7327709568605458460]": {
"$type": "EditorVisibilityComponent",
"Id": 7327709568605458460
},
"Component_[7514977506847036117]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 7514977506847036117,
"Parent Entity": "ContainerEntity"
}
}
},
"Entity_[278299626583]": {
"Id": "Entity_[278299626583]",
"Name": "AssetIDMatch",
"Components": {
"Component_[10285740519857855186]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 10285740519857855186
},
"Component_[11273731016303624898]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 11273731016303624898,
"Parent Entity": "Entity_[274004659287]"
},
"Component_[1136790983026972010]": {
"$type": "EditorVisibilityComponent",
"Id": 1136790983026972010
},
"Component_[12777313618328131055]": {
"$type": "EditorPendingCompositionComponent",
"Id": 12777313618328131055
},
"Component_[13256044902558773795]": {
"$type": "EditorLockComponent",
"Id": 13256044902558773795
},
"Component_[15834551022302435776]": {
"$type": "EditorInspectorComponent",
"Id": 15834551022302435776,
"ComponentOrderEntryArray": [
{
"ComponentId": 11273731016303624898
},
{
"ComponentId": 9671522714018290727,
"SortIndex": 1
}
]
},
"Component_[16345420368214930095]": {
"$type": "SelectionComponent",
"Id": 16345420368214930095
},
"Component_[5309075942188429052]": {
"$type": "EditorEntitySortComponent",
"Id": 5309075942188429052
},
"Component_[8639731896786645938]": {
"$type": "EditorEntityIconComponent",
"Id": 8639731896786645938
},
"Component_[9844585173698551415]": {
"$type": "EditorOnlyEntityComponent",
"Id": 9844585173698551415
}
}
},
"Entity_[282594593879]": {
"Id": "Entity_[282594593879]",
"Name": "UUIDMatch",
"Components": {
"Component_[10379494986254888760]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 10379494986254888760
},
"Component_[10932830014545295552]": {
"$type": "SelectionComponent",
"Id": 10932830014545295552
},
"Component_[16077882919902242532]": {
"$type": "EditorEntitySortComponent",
"Id": 16077882919902242532
},
"Component_[2150375322459274584]": {
"$type": "EditorPendingCompositionComponent",
"Id": 2150375322459274584
},
"Component_[2645455411436465820]": {
"$type": "EditorEntityIconComponent",
"Id": 2645455411436465820
},
"Component_[5422214869037468733]": {
"$type": "EditorOnlyEntityComponent",
"Id": 5422214869037468733
},
"Component_[7238126895911071330]": {
"$type": "EditorInspectorComponent",
"Id": 7238126895911071330,
"ComponentOrderEntryArray": [
{
"ComponentId": 8407607000804893064
},
{
"ComponentId": 12952323341649885242,
"SortIndex": 1
}
]
},
"Component_[7981670269715131988]": {
"$type": "EditorVisibilityComponent",
"Id": 7981670269715131988
},
"Component_[8407607000804893064]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 8407607000804893064,
"Parent Entity": "Entity_[274004659287]"
},
"Component_[8567641786004090803]": {
"$type": "EditorLockComponent",
"Id": 8567641786004090803
}
}
},
"Entity_[286889561175]": {
"Id": "Entity_[286889561175]",
"Name": "RelativeProductMatch",
"Components": {
"Component_[10180645282669228972]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 10180645282669228972,
"Parent Entity": "Entity_[274004659287]"
},
"Component_[10200807690182688147]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 10200807690182688147
},
"Component_[11014661873645081316]": {
"$type": "EditorInspectorComponent",
"Id": 11014661873645081316,
"ComponentOrderEntryArray": [
{
"ComponentId": 10180645282669228972
},
{
"ComponentId": 12869852248016369650,
"SortIndex": 1
}
]
},
"Component_[12869852248016369650]": {
"$type": "{77CDE991-EC1A-B7C1-B112-7456ABAC81A1} EditorSpawnerComponent",
"Id": 12869852248016369650,
"Slice": {
"assetId": {
"guid": "{29F14025-3BD2-5CA9-A9DE-B8B349268C2F}",
"subId": 2
},
"assetHint": "slices/bullet.dynamicslice"
}
},
"Component_[15136448544716183259]": {
"$type": "EditorOnlyEntityComponent",
"Id": 15136448544716183259
},
"Component_[15966001894874626764]": {
"$type": "SelectionComponent",
"Id": 15966001894874626764
},
"Component_[16167982631516160155]": {
"$type": "EditorEntityIconComponent",
"Id": 16167982631516160155
},
"Component_[16672905198052847867]": {
"$type": "EditorEntitySortComponent",
"Id": 16672905198052847867
},
"Component_[4506946122562404190]": {
"$type": "EditorPendingCompositionComponent",
"Id": 4506946122562404190
},
"Component_[6836304267269231429]": {
"$type": "EditorLockComponent",
"Id": 6836304267269231429
},
"Component_[8756593519140349183]": {
"$type": "EditorVisibilityComponent",
"Id": 8756593519140349183
}
}
},
"Entity_[291184528471]": {
"Id": "Entity_[291184528471]",
"Name": "RelativeSourceMatch",
"Components": {
"Component_[11694027325905361034]": {
"$type": "EditorEntitySortComponent",
"Id": 11694027325905361034
},
"Component_[13891029613307790064]": {
"$type": "EditorEntityIconComponent",
"Id": 13891029613307790064
},
"Component_[15933511034411930900]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 15933511034411930900,
"Parent Entity": "Entity_[274004659287]"
},
"Component_[17540827492846961803]": {
"$type": "EditorLockComponent",
"Id": 17540827492846961803
},
"Component_[2850297705939373458]": {
"$type": "SelectionComponent",
"Id": 2850297705939373458
},
"Component_[4809103331004345812]": {
"$type": "EditorPendingCompositionComponent",
"Id": 4809103331004345812
},
"Component_[5654779331777839943]": {
"$type": "EditorInspectorComponent",
"Id": 5654779331777839943,
"ComponentOrderEntryArray": [
{
"ComponentId": 15933511034411930900
},
{
"ComponentId": 10284025539900054207,
"SortIndex": 1
}
]
},
"Component_[6097019179005900386]": {
"$type": "EditorVisibilityComponent",
"Id": 6097019179005900386
},
"Component_[7748387730313625157]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 7748387730313625157
},
"Component_[9822265453841229082]": {
"$type": "EditorOnlyEntityComponent",
"Id": 9822265453841229082
}
}
},
"Entity_[297232250983]": {
"Id": "Entity_[297232250983]",
"Name": "1151F14D38A65579888ABE3139882E68:[0]",
"Components": {
"Component_[11936148741777754959]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 11936148741777754959
},
"Component_[12610073699988743015]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 12610073699988743015,
"Parent Entity": "Entity_[305822185575]"
},
"Component_[1603205169722765279]": {
"$type": "EditorLockComponent",
"Id": 1603205169722765279
},
"Component_[17691206348057560715]": {
"$type": "EditorPendingCompositionComponent",
"Id": 17691206348057560715
},
"Component_[2711821203680330048]": {
"$type": "SelectionComponent",
"Id": 2711821203680330048
},
"Component_[3887480309311474860]": {
"$type": "EditorVisibilityComponent",
"Id": 3887480309311474860
},
"Component_[5853968883842282450]": {
"$type": "EditorEntityIconComponent",
"Id": 5853968883842282450
},
"Component_[7679080692843343453]": {
"$type": "EditorCommentComponent",
"Id": 7679080692843343453,
"Configuration": "Asset ID that matches an existing dependency of this asset (am_grass1.mtl)"
},
"Component_[8024752235278898687]": {
"$type": "EditorOnlyEntityComponent",
"Id": 8024752235278898687
},
"Component_[8373296084678231042]": {
"$type": "EditorEntitySortComponent",
"Id": 8373296084678231042
},
"Component_[9782967158965587831]": {
"$type": "EditorInspectorComponent",
"Id": 9782967158965587831,
"ComponentOrderEntryArray": [
{
"ComponentId": 12610073699988743015
},
{
"ComponentId": 7679080692843343453,
"SortIndex": 1
}
]
}
}
},
"Entity_[301527218279]": {
"Id": "Entity_[301527218279]",
"Name": "Slices/bullet.dynamicslice",
"Components": {
"Component_[15613078542630153866]": {
"$type": "EditorInspectorComponent",
"Id": 15613078542630153866,
"ComponentOrderEntryArray": [
{
"ComponentId": 2483032678718995164
},
{
"ComponentId": 9734604379060902193,
"SortIndex": 1
}
]
},
"Component_[16098942854900817264]": {
"$type": "SelectionComponent",
"Id": 16098942854900817264
},
"Component_[16720139961856477500]": {
"$type": "EditorEntitySortComponent",
"Id": 16720139961856477500
},
"Component_[2483032678718995164]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 2483032678718995164,
"Parent Entity": "Entity_[305822185575]"
},
"Component_[2502984783426127018]": {
"$type": "EditorLockComponent",
"Id": 2502984783426127018
},
"Component_[46714013890147210]": {
"$type": "EditorOnlyEntityComponent",
"Id": 46714013890147210
},
"Component_[4907474530744780429]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 4907474530744780429
},
"Component_[5420332829198300813]": {
"$type": "EditorPendingCompositionComponent",
"Id": 5420332829198300813
},
"Component_[7683578164681693579]": {
"$type": "EditorEntityIconComponent",
"Id": 7683578164681693579
},
"Component_[8312115250363172310]": {
"$type": "EditorVisibilityComponent",
"Id": 8312115250363172310
},
"Component_[9734604379060902193]": {
"$type": "EditorCommentComponent",
"Id": 9734604379060902193,
"Configuration": "Relative product path that matches an existing dependency of this asset"
}
}
},
"Entity_[305822185575]": {
"Id": "Entity_[305822185575]",
"Name": "AssetReferences",
"Components": {
"Component_[13422135993444528172]": {
"$type": "EditorPendingCompositionComponent",
"Id": 13422135993444528172
},
"Component_[13988015667379021413]": {
"$type": "EditorLockComponent",
"Id": 13988015667379021413
},
"Component_[14885956487876614434]": {
"$type": "EditorVisibilityComponent",
"Id": 14885956487876614434
},
"Component_[15550715415947731915]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 15550715415947731915
},
"Component_[2576266145980379805]": {
"$type": "EditorInspectorComponent",
"Id": 2576266145980379805,
"ComponentOrderEntryArray": [
{
"ComponentId": 3176911836967955668
},
{
"ComponentId": 836721549453007197,
"SortIndex": 1
}
]
},
"Component_[3176911836967955668]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 3176911836967955668,
"Parent Entity": "Entity_[274004659287]"
},
"Component_[5613459137294642234]": {
"$type": "SelectionComponent",
"Id": 5613459137294642234
},
"Component_[6400873582148097152]": {
"$type": "EditorEntityIconComponent",
"Id": 6400873582148097152
},
"Component_[684670817803453913]": {
"$type": "EditorEntitySortComponent",
"Id": 684670817803453913,
"Child Entity Order": [
"Entity_[323002054759]",
"Entity_[327297022055]",
"Entity_[301527218279]",
"Entity_[318707087463]",
"Entity_[297232250983]",
"Entity_[314412120167]",
"Entity_[331591989351]",
"Entity_[310117152871]"
]
},
"Component_[8118206464926826097]": {
"$type": "EditorOnlyEntityComponent",
"Id": 8118206464926826097
},
"Component_[836721549453007197]": {
"$type": "EditorCommentComponent",
"Id": 836721549453007197,
"Configuration": "Entity names are used to trigger the missing dependency scanner. Comments are stripped from dynamic slices."
}
}
},
"Entity_[310117152871]": {
"Id": "Entity_[310117152871]",
"Name": "Materials/FakeMaterial.mtl",
"Components": {
"Component_[10593857511582714674]": {
"$type": "EditorInspectorComponent",
"Id": 10593857511582714674,
"ComponentOrderEntryArray": [
{
"ComponentId": 11797216659359478300
},
{
"ComponentId": 13816702107134233983,
"SortIndex": 1
}
]
},
"Component_[11797216659359478300]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 11797216659359478300,
"Parent Entity": "Entity_[305822185575]"
},
"Component_[13816702107134233983]": {
"$type": "EditorCommentComponent",
"Id": 13816702107134233983,
"Configuration": "Invalid path that does not match an existing dependency of this asset"
},
"Component_[14868583012186337705]": {
"$type": "EditorOnlyEntityComponent",
"Id": 14868583012186337705
},
"Component_[14965348027145283648]": {
"$type": "EditorEntitySortComponent",
"Id": 14965348027145283648
},
"Component_[15075774238648121688]": {
"$type": "EditorEntityIconComponent",
"Id": 15075774238648121688
},
"Component_[16157883709857447266]": {
"$type": "EditorLockComponent",
"Id": 16157883709857447266
},
"Component_[17712080510249108208]": {
"$type": "EditorVisibilityComponent",
"Id": 17712080510249108208
},
"Component_[2247408514677946398]": {
"$type": "SelectionComponent",
"Id": 2247408514677946398
},
"Component_[5565369976544134481]": {
"$type": "EditorPendingCompositionComponent",
"Id": 5565369976544134481
},
"Component_[6044814215558788086]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 6044814215558788086
}
}
},
"Entity_[314412120167]": {
"Id": "Entity_[314412120167]",
"Name": "88888888-4444-4444-4444-CCCCCCCCCCCC",
"Components": {
"Component_[10072177500579430176]": {
"$type": "EditorLockComponent",
"Id": 10072177500579430176
},
"Component_[10853215476279564671]": {
"$type": "EditorCommentComponent",
"Id": 10853215476279564671,
"Configuration": "UUID that does not exist"
},
"Component_[13413154971272749631]": {
"$type": "SelectionComponent",
"Id": 13413154971272749631
},
"Component_[15316173756367163440]": {
"$type": "EditorInspectorComponent",
"Id": 15316173756367163440,
"ComponentOrderEntryArray": [
{
"ComponentId": 3266728630359207653
},
{
"ComponentId": 10853215476279564671,
"SortIndex": 1
}
]
},
"Component_[15809307959802829291]": {
"$type": "EditorEntitySortComponent",
"Id": 15809307959802829291
},
"Component_[17649652752752487081]": {
"$type": "EditorEntityIconComponent",
"Id": 17649652752752487081
},
"Component_[2130036493438440377]": {
"$type": "EditorPendingCompositionComponent",
"Id": 2130036493438440377
},
"Component_[3266728630359207653]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 3266728630359207653,
"Parent Entity": "Entity_[305822185575]"
},
"Component_[5892125564582966187]": {
"$type": "EditorVisibilityComponent",
"Id": 5892125564582966187
},
"Component_[597602776660257245]": {
"$type": "EditorOnlyEntityComponent",
"Id": 597602776660257245
},
"Component_[8238652007701465495]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 8238652007701465495
}
}
},
"Entity_[318707087463]": {
"Id": "Entity_[318707087463]",
"Name": "BBA1A5494C73578894BF0692CDA5FC33",
"Components": {
"Component_[10222455787643359341]": {
"$type": "EditorPendingCompositionComponent",
"Id": 10222455787643359341
},
"Component_[11487845392038268864]": {
"$type": "SelectionComponent",
"Id": 11487845392038268864
},
"Component_[12135534290310046764]": {
"$type": "EditorVisibilityComponent",
"Id": 12135534290310046764
},
"Component_[14412623226519978498]": {
"$type": "EditorOnlyEntityComponent",
"Id": 14412623226519978498
},
"Component_[14516371382857751872]": {
"$type": "EditorEntityIconComponent",
"Id": 14516371382857751872
},
"Component_[16011611743122468576]": {
"$type": "EditorInspectorComponent",
"Id": 16011611743122468576,
"ComponentOrderEntryArray": [
{
"ComponentId": 4157328932578509254
},
{
"ComponentId": 8524796860605854850,
"SortIndex": 1
}
]
},
"Component_[3813931698067937301]": {
"$type": "EditorEntitySortComponent",
"Id": 3813931698067937301
},
"Component_[4157328932578509254]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 4157328932578509254,
"Parent Entity": "Entity_[305822185575]"
},
"Component_[8524796860605854850]": {
"$type": "EditorCommentComponent",
"Id": 8524796860605854850,
"Configuration": "UUID that matches an existing dependency of this asset (lumbertank_body.cgf)"
},
"Component_[8660819596448699427]": {
"$type": "EditorLockComponent",
"Id": 8660819596448699427
},
"Component_[8768262795169819026]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 8768262795169819026
}
}
},
"Entity_[323002054759]": {
"Id": "Entity_[323002054759]",
"Name": "Materials/am_rockground.mtl",
"Components": {
"Component_[13459503224133892836]": {
"$type": "EditorEntityIconComponent",
"Id": 13459503224133892836
},
"Component_[1346698328271204385]": {
"$type": "EditorVisibilityComponent",
"Id": 1346698328271204385
},
"Component_[13662830241397426219]": {
"$type": "SelectionComponent",
"Id": 13662830241397426219
},
"Component_[14169735046939083706]": {
"$type": "EditorInspectorComponent",
"Id": 14169735046939083706,
"ComponentOrderEntryArray": [
{
"ComponentId": 833157791612452820
},
{
"ComponentId": 3573928838741352115,
"SortIndex": 1
}
]
},
"Component_[16049700338512950477]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 16049700338512950477
},
"Component_[16191253524853449302]": {
"$type": "EditorOnlyEntityComponent",
"Id": 16191253524853449302
},
"Component_[1737139665005484521]": {
"$type": "EditorPendingCompositionComponent",
"Id": 1737139665005484521
},
"Component_[17562284119637289685]": {
"$type": "EditorEntitySortComponent",
"Id": 17562284119637289685
},
"Component_[3573928838741352115]": {
"$type": "EditorCommentComponent",
"Id": 3573928838741352115,
"Configuration": "Relative source path that matches an existing dependency of this asset"
},
"Component_[485401015869338526]": {
"$type": "EditorLockComponent",
"Id": 485401015869338526
},
"Component_[833157791612452820]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 833157791612452820,
"Parent Entity": "Entity_[305822185575]"
}
}
},
"Entity_[327297022055]": {
"Id": "Entity_[327297022055]",
"Name": "Config/Game.xml",
"Components": {
"Component_[11848260632907964142]": {
"$type": "EditorInspectorComponent",
"Id": 11848260632907964142,
"ComponentOrderEntryArray": [
{
"ComponentId": 497869813123895830
},
{
"ComponentId": 5248857300320701553,
"SortIndex": 1
}
]
},
"Component_[12842864953492512672]": {
"$type": "EditorEntitySortComponent",
"Id": 12842864953492512672
},
"Component_[16656501539883791157]": {
"$type": "EditorLockComponent",
"Id": 16656501539883791157
},
"Component_[17365661125603122123]": {
"$type": "EditorEntityIconComponent",
"Id": 17365661125603122123
},
"Component_[2967487135389707052]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 2967487135389707052
},
"Component_[3356294263684362888]": {
"$type": "EditorOnlyEntityComponent",
"Id": 3356294263684362888
},
"Component_[497869813123895830]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 497869813123895830,
"Parent Entity": "Entity_[305822185575]"
},
"Component_[5248857300320701553]": {
"$type": "EditorCommentComponent",
"Id": 5248857300320701553,
"Configuration": "Valid path that does not match an existing dependency of this asset. Should report as a missing dependency"
},
"Component_[746309483212393367]": {
"$type": "SelectionComponent",
"Id": 746309483212393367
},
"Component_[8319831469290771470]": {
"$type": "EditorPendingCompositionComponent",
"Id": 8319831469290771470
},
"Component_[9369067377618608622]": {
"$type": "EditorVisibilityComponent",
"Id": 9369067377618608622
}
}
},
"Entity_[331591989351]": {
"Id": "Entity_[331591989351]",
"Name": "1151F14D38A65579888ABE3139882E68:[333]",
"Components": {
"Component_[104857639379046106]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 104857639379046106,
"Parent Entity": "Entity_[305822185575]"
},
"Component_[1061601983221247493]": {
"$type": "EditorLockComponent",
"Id": 1061601983221247493
},
"Component_[11028443253330664986]": {
"$type": "EditorVisibilityComponent",
"Id": 11028443253330664986
},
"Component_[13806275118632081006]": {
"$type": "EditorEntitySortComponent",
"Id": 13806275118632081006
},
"Component_[13922573109551604801]": {
"$type": "EditorEntityIconComponent",
"Id": 13922573109551604801
},
"Component_[17027032709917108335]": {
"$type": "EditorDisabledCompositionComponent",
"Id": 17027032709917108335
},
"Component_[17030988165269698825]": {
"$type": "EditorOnlyEntityComponent",
"Id": 17030988165269698825
},
"Component_[2294579021665535860]": {
"$type": "EditorPendingCompositionComponent",
"Id": 2294579021665535860
},
"Component_[5863078697041048226]": {
"$type": "EditorInspectorComponent",
"Id": 5863078697041048226,
"ComponentOrderEntryArray": [
{
"ComponentId": 104857639379046106
},
{
"ComponentId": 9466290982672370664,
"SortIndex": 1
}
]
},
"Component_[7608263859116142496]": {
"$type": "SelectionComponent",
"Id": 7608263859116142496
},
"Component_[9466290982672370664]": {
"$type": "EditorCommentComponent",
"Id": 9466290982672370664,
"Configuration": "Asset ID that does not exist (am_grass1.mtl UUID, no matching product ID)"
}
}
}
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c6b33c6137d6bd8c696f180c30a23089c95c1af398a630b4b13e080bec3254d
size 18220
@@ -23,12 +23,11 @@ from ..ap_fixtures.timeout_option_fixture import timeout_option_fixture as timeo
@pytest.mark.SUITE_periodic
@pytest.mark.parametrize('launcher_platform', ['windows_editor'])
@pytest.mark.parametrize('project', ['AutomatedTesting'])
@pytest.mark.parametrize('level', ['auto_test'])
@pytest.mark.parametrize('level', ['TestDependenciesLevel'])
class TestBundleMode(object):
def test_bundle_mode_with_levels_mounts_bundles_correctly(self, request, editor, level, launcher_platform,
asset_processor, workspace, bundler_batch_helper):
level_pak = os.path.join("levels", level, "level.pak")
level_pak = os.path.join("levels", level, "TestDependenciesLevel.spawnable")
bundles_folder = os.path.join(workspace.paths.project(), "Bundles")
bundle_request_path = os.path.join(bundles_folder, "bundle.pak")
bundle_result_path = os.path.join(bundles_folder,
@@ -23,7 +23,7 @@ def output_test_data(scene):
# Just write something to the file, but the filename is the main information
# used for the test.
f.write(f"scene.sourceFilename: {scene.sourceFilename}\n")
return True
return ''
mySceneJobHandler = None
@@ -8,10 +8,12 @@ SPDX-License-Identifier: Apache-2.0 OR MIT
import os
import logging
import subprocess
import sys
import pytest
import time
from os import path
import ly_test_tools.environment.file_system as file_system
import ly_test_tools.environment.process_utils as process_utils
import ly_test_tools.environment.waiter as waiter
@@ -98,7 +100,9 @@ class TestAutomationBase:
if autotest_mode:
pycmd += ["-autotest_mode"]
if enable_prefab_system:
pycmd += ["--regset=/Amazon/Preferences/EnablePrefabSystem=true"]
pycmd += [
"--regset=/Amazon/Preferences/EnablePrefabSystem=true",
f"--regset-file={path.join(workspace.paths.engine_root(), 'Registry', 'prefab.test.setreg')}"]
else:
pycmd += ["--regset=/Amazon/Preferences/EnablePrefabSystem=false"]
@@ -128,7 +132,8 @@ class TestAutomationBase:
errors.append(TestRunError("FAILED TEST", error_str))
if return_code and return_code != TestAutomationBase.TEST_FAIL_RETCODE: # Crashed
crash_info = "-- No crash log available --"
crash_log = os.path.join(workspace.paths.project_log(), 'error.log')
crash_log = workspace.paths.crash_log()
try:
waiter.wait_for(lambda: os.path.exists(crash_log), timeout=TestAutomationBase.WAIT_FOR_CRASH_LOG)
except AssertionError:
@@ -177,7 +182,7 @@ class TestAutomationBase:
@staticmethod
def _kill_ly_processes(include_asset_processor=True):
LY_PROCESSES = [
'Editor', 'Profiler', 'RemoteConsole',
'Editor', 'Profiler', 'RemoteConsole', 'AutomatedTesting.ServerLauncher', 'o3de'
]
AP_PROCESSES = [
'AssetProcessor', 'AssetProcessorBatch', 'AssetBuilder', 'CrySCompileServer',
@@ -9,10 +9,10 @@
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_FOUNDATION_TEST_SUPPORTED)
ly_add_pytest(
NAME AutomatedTesting::EditorTests_Main_Optimized
NAME AutomatedTesting::EditorTests_Main
TEST_SUITE main
TEST_SERIAL
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main_Optimized.py
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main.py
PYTEST_MARKS "not REQUIRES_gpu"
RUNTIME_DEPENDENCIES
Legacy::Editor
@@ -27,7 +27,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_
TEST_SUITE main
TEST_SERIAL
TEST_REQUIRES gpu
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main_Optimized.py
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main.py
PYTEST_MARKS "REQUIRES_gpu"
RUNTIME_DEPENDENCIES
Legacy::Editor
@@ -37,17 +37,4 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_
Editor
)
ly_add_pytest(
NAME AutomatedTesting::EditorTests_Sandbox_Optimized
TEST_SUITE sandbox
TEST_SERIAL
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Sandbox_Optimized.py
RUNTIME_DEPENDENCIES
Legacy::Editor
AZ::AssetProcessor
AutomatedTesting.Assets
COMPONENT
Editor
)
endif()
@@ -59,8 +59,8 @@ def AssetBrowser_SearchFiltering():
import azlmbr.legacy.general as general
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
def verify_files_appeared(model, allowed_asset_extensions, parent_index=QtCore.QModelIndex()):
indexes = [parent_index]
@@ -80,8 +80,7 @@ def AssetBrowser_SearchFiltering():
return True
# 1) Open an existing simple level
helper.init_idle()
helper.open_level("Physics", "Base")
hydra.open_base_level()
# 2) Open Asset Browser (if not opened already)
editor_window = pyside_utils.get_editor_main_window()
@@ -98,7 +97,13 @@ def AssetBrowser_SearchFiltering():
# 3) Type the name of an asset in the search bar and make sure it is filtered to and selectable
asset_browser = editor_window.findChild(QtWidgets.QDockWidget, "Asset Browser")
search_bar = asset_browser.findChild(QtWidgets.QLineEdit, "textSearch")
search_bar.setText("cedar.fbx")
# Add a small pause when typing in the search bar in order to check that the entries are updated properly
search_bar.setText("Cedar.f")
general.idle_wait(0.5)
search_bar.setText("Cedar.fbx")
general.idle_wait(0.5)
asset_browser_tree = asset_browser.findChild(QtWidgets.QTreeView, "m_assetBrowserTreeViewWidget")
asset_browser_table = asset_browser.findChild(QtWidgets.QTreeView, "m_assetBrowserTableViewWidget")
found = await pyside_utils.wait_for_condition(lambda: pyside_utils.find_child_by_pattern(asset_browser_table, "cedar.fbx"), 5.0)
@@ -53,6 +53,7 @@ def AssetBrowser_TreeNavigation():
import azlmbr.legacy.general as general
import editor_python_test_tools.pyside_utils as pyside_utils
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
@@ -69,8 +70,7 @@ def AssetBrowser_TreeNavigation():
file_path = ("AutomatedTesting", "Assets", "ImageGradients", "image_grad_test_gsi.png")
# 1) Open an existing simple level
helper.init_idle()
helper.open_level("Physics", "Base")
hydra.open_base_level()
# 2) Open Asset Browser (if not opened already)
editor_window = pyside_utils.get_editor_main_window()
@@ -215,8 +215,7 @@ def AssetPicker_UI_UX():
QtTest.QTest.keyClick(tree, Qt.Key_Enter, Qt.NoModifier)
# 1) Open an existing simple level
helper.init_idle()
helper.open_level("Physics", "Base")
hydra.open_base_level()
# 2) Create entity and add Mesh component
entity_position = math.Vector3(125.0, 136.0, 32.0)
@@ -56,19 +56,17 @@ def BasicEditorWorkflows_ExistingLevel_EntityComponentCRUD():
06. delete parent entity
"""
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.utils import Report
from editor_python_test_tools.editor_entity_utils import EditorEntity
import azlmbr.bus as bus
import azlmbr.editor as editor
import azlmbr.entity as entity
import azlmbr.legacy.general as general
import azlmbr.object
# 01. load an existing level
test_level = 'Simple'
general.open_level_no_prompt(test_level)
Report.result(Tests.load_level, general.get_current_level_name() == test_level)
hydra.open_base_level()
# 02. create parent entity and set name
# Delete any exiting entity and Create a new Entity at the root level
@@ -31,7 +31,7 @@ class Tests:
"Component removed from entity successfully",
"Failed to remove component from entity"
)
level_saved_and_exported = (
saved_and_exported = (
"Level saved and exported successfully",
"Failed to save/export level"
)
@@ -52,8 +52,7 @@ def BasicEditorWorkflows_LevelEntityComponentCRUD():
- A new entity can be created
- Entity hierarchy can be adjusted
- Components can be added/removed/updated
- Level can be saved
- Level can be exported
- Level can be saved/exported
Note:
- This test file must be called from the O3DE Editor command terminal
@@ -70,7 +69,7 @@ def BasicEditorWorkflows_LevelEntityComponentCRUD():
import azlmbr.editor as editor
import azlmbr.entity as entity
import azlmbr.math as math
import azlmbr.paths
import azlmbr.paths as paths
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.utils import Report
@@ -84,7 +83,7 @@ def BasicEditorWorkflows_LevelEntityComponentCRUD():
return None
# 1) Create a new level
level = "tmp_level"
lvl_name = "tmp_level"
editor_window = pyside_utils.get_editor_main_window()
new_level_action = pyside_utils.get_action_for_menu_path(editor_window, "File", "New Level")
pyside_utils.trigger_action_async(new_level_action)
@@ -95,23 +94,24 @@ def BasicEditorWorkflows_LevelEntityComponentCRUD():
Report.info("New Level dialog opened")
grp_box = new_level_dlg.findChild(QtWidgets.QGroupBox, "STATIC_GROUP1")
level_name = grp_box.findChild(QtWidgets.QLineEdit, "LEVEL")
level_name.setText(level)
level_name.setText(lvl_name)
button_box = new_level_dlg.findChild(QtWidgets.QDialogButtonBox, "buttonBox")
button_box.button(QtWidgets.QDialogButtonBox.Ok).click()
# Verify new level was created successfully
level_create_success = await pyside_utils.wait_for_condition(lambda: editor.EditorToolsApplicationRequestBus(
bus.Broadcast, "GetCurrentLevelName") == level, 5.0)
bus.Broadcast, "GetCurrentLevelName") == lvl_name, 5.0)
Report.critical_result(Tests.level_created, level_create_success)
# 2) Delete existing entities, and create and manipulate new entities via Entity Inspector
search_filter = azlmbr.entity.SearchFilter()
all_entities = entity.SearchBus(azlmbr.bus.Broadcast, "SearchEntities", search_filter)
editor.ToolsApplicationRequestBus(bus.Broadcast, "DeleteEntities", all_entities)
entity_outliner_widget = editor_window.findChild(QtWidgets.QWidget, "OutlinerWidgetUI")
entity_outliner_widget = editor_window.findChild(QtWidgets.QWidget, "EntityOutlinerWidgetUI")
outliner_object_list = entity_outliner_widget.findChild(QtWidgets.QWidget, "m_objectList_Contents")
outliner_tree = outliner_object_list.findChild(QtWidgets.QWidget, "m_objectTree")
await pyside_utils.trigger_context_menu_entry(outliner_tree, "Create entity")
outliner_viewport = outliner_tree.findChild(QtWidgets.QWidget, "qt_scrollarea_viewport")
await pyside_utils.trigger_context_menu_entry(outliner_viewport, "Create entity")
# Find the new entity
parent_entity_id = find_entity_by_name("Entity1")
@@ -153,14 +153,10 @@ def BasicEditorWorkflows_LevelEntityComponentCRUD():
save_level_action = pyside_utils.get_action_for_menu_path(editor_window, "File", "Save")
pyside_utils.trigger_action_async(save_level_action)
# 5) Export the level
export_action = pyside_utils.get_action_for_menu_path(editor_window, "Game", "Export to Engine")
pyside_utils.trigger_action_async(export_action)
level_pak_file = os.path.join(
"AutomatedTesting", "Levels", level, "level.pak"
)
export_success = await pyside_utils.wait_for_condition(lambda: os.path.exists(level_pak_file), 5.0)
Report.result(Tests.level_saved_and_exported, export_success)
# 5) Verify the save/export of the level
level_prefab_path = os.path.join(paths.products, "levels", lvl_name, f"{lvl_name}.spawnable")
success = await pyside_utils.wait_for_condition(lambda: os.path.exists(level_prefab_path), 5.0)
Report.result(Tests.saved_and_exported, success)
run_test()
@@ -63,7 +63,7 @@ def ComponentCRUD_Add_Delete_Components():
:return: None
"""
from PySide2 import QtWidgets, QtTest, QtCore
from PySide2 import QtWidgets, QtTest
from PySide2.QtCore import Qt
import azlmbr.legacy.general as general
@@ -74,7 +74,6 @@ def ComponentCRUD_Add_Delete_Components():
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
async def add_component(component_name):
pyside_utils.click_button_async(add_comp_btn)
@@ -88,8 +87,7 @@ def ComponentCRUD_Add_Delete_Components():
QtTest.QTest.keyClick(tree, Qt.Key_Enter, Qt.NoModifier)
# 1) Open an existing simple level
helper.init_idle()
helper.open_level("Physics", "Base")
hydra.open_base_level()
# 2) Create entity
entity_position = math.Vector3(125.0, 136.0, 32.0)
@@ -62,12 +62,11 @@ def Docking_BasicDockedTools():
import azlmbr.editor as editor
import azlmbr.entity as entity
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
# Open an existing simple level
helper.init_idle()
helper.open_level("Physics", "Base")
hydra.open_base_level()
# Make sure the Entity Outliner, Entity Inspector and Console tools are open
general.open_pane("Entity Outliner (PREVIEW)")
@@ -80,7 +79,7 @@ def Docking_BasicDockedTools():
editor.EditorEntityAPIBus(bus.Event, 'SetName', entity_id, entity_original_name)
editor_window = pyside_utils.get_editor_main_window()
entity_outliner = editor_window.findChild(QtWidgets.QDockWidget, "Entity Outliner (PREVIEW)")
entity_outliner = editor_window.findChild(QtWidgets.QDockWidget, "Entity Outliner")
# 1) Open the tools and dock them together in a floating tabbed widget.
# We drag/drop it over the viewport since it doesn't allow docking, so this will undock it
@@ -88,15 +87,15 @@ def Docking_BasicDockedTools():
pyside_utils.drag_and_drop(entity_outliner, render_overlay)
# We need to grab a new reference to the Entity Outliner QDockWidget because when it gets moved
# to the floating window, its parent changes so the wrapped intance we had becomes invalid
entity_outliner = editor_window.findChild(QtWidgets.QDockWidget, "Entity Outliner (PREVIEW)")
# to the floating window, its parent changes so the wrapped instance we had becomes invalid
entity_outliner = editor_window.findChild(QtWidgets.QDockWidget, "Entity Outliner")
# Dock the Entity Inspector tabbed with the floating Entity Outliner
entity_inspector = editor_window.findChild(QtWidgets.QDockWidget, "Entity Inspector")
pyside_utils.drag_and_drop(entity_inspector, entity_outliner)
# We need to grab a new reference to the Entity Inspector QDockWidget because when it gets moved
# to the floating window, its parent changes so the wrapped intance we had becomes invalid
# to the floating window, its parent changes so the wrapped instance we had becomes invalid
entity_inspector = editor_window.findChild(QtWidgets.QDockWidget, "Entity Inspector")
# Dock the Console tabbed with the floating Entity Inspector
@@ -106,7 +105,7 @@ def Docking_BasicDockedTools():
# Check to ensure all the tools are parented to the same QStackedWidget
def check_all_panes_tabbed():
entity_inspector = editor_window.findChild(QtWidgets.QDockWidget, "Entity Inspector")
entity_outliner = editor_window.findChild(QtWidgets.QDockWidget, "Entity Outliner (PREVIEW)")
entity_outliner = editor_window.findChild(QtWidgets.QDockWidget, "Entity Outliner")
console = editor_window.findChild(QtWidgets.QDockWidget, "Console")
entity_inspector_parent = entity_inspector.parentWidget()
entity_outliner_parent = entity_outliner.parentWidget()
@@ -122,7 +121,7 @@ def Docking_BasicDockedTools():
# 2.1,2) Select an Entity in the Entity Outliner.
entity_inspector = editor_window.findChild(QtWidgets.QDockWidget, "Entity Inspector")
entity_outliner = editor_window.findChild(QtWidgets.QDockWidget, "Entity Outliner (PREVIEW)")
entity_outliner = editor_window.findChild(QtWidgets.QDockWidget, "Entity Outliner")
console = editor_window.findChild(QtWidgets.QDockWidget, "Console")
object_tree = entity_outliner.findChild(QtWidgets.QTreeView, "m_objectTree")
test_entity_index = pyside_utils.find_child_by_pattern(object_tree, entity_original_name)
@@ -30,11 +30,13 @@ def EntityOutliner_EntityOrdering():
5) Add another new entity, ensure the rest of the order is unchanged
"""
import editor_python_test_tools.pyside_utils as pyside_utils
from PySide2 import QtCore
import azlmbr.legacy.general as general
import editor_python_test_tools.hydra_editor_utils as hydra
import editor_python_test_tools.pyside_utils as pyside_utils
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
from PySide2 import QtCore, QtWidgets, QtGui, QtTest
# Grab the Editor, Entity Outliner, and Outliner Model
editor_window = pyside_utils.get_editor_main_window()
@@ -110,8 +112,7 @@ def EntityOutliner_EntityOrdering():
expected_order = []
# 1) Open the empty Prefab Base level
helper.init_idle()
helper.open_level("Prefab", "Base")
hydra.open_base_level()
# 2) Add 5 entities to the outliner
ENTITIES_TO_ADD = 5
@@ -69,8 +69,8 @@ def InputBindings_Add_Remove_Input_Events():
import azlmbr.legacy.general as general
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
def open_asset_editor():
general.open_pane("Asset Editor")
@@ -81,8 +81,7 @@ def InputBindings_Add_Remove_Input_Events():
return not general.is_pane_visible("Asset Editor")
# 1) Open an existing simple level
helper.init_idle()
helper.open_level("Physics", "Base")
hydra.open_base_level()
# 2) Open Asset Editor
Report.result(Tests.asset_editor_opened, open_asset_editor())
@@ -26,9 +26,9 @@ def Menus_EditMenuOptions_Work():
:return: None
"""
import editor_python_test_tools.hydra_editor_utils as hydra
import editor_python_test_tools.pyside_utils as pyside_utils
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
edit_menu_options = [
("Undo",),
@@ -40,30 +40,32 @@ def Menus_EditMenuOptions_Work():
("Toggle Pivot Location",),
("Reset Entity Transform",),
("Reset Manipulator",),
("Reset Transform (Local)",),
("Reset Transform (World)",),
("Hide Selection",),
("Show All",),
("Modify", "Snap", "Snap angle"),
("Lock Selection",),
("Unlock All Entities",),
("Modify", "Snap", "Angle snapping"),
("Modify", "Snap", "Grid snapping"),
("Modify", "Transform Mode", "Move"),
("Modify", "Transform Mode", "Rotate"),
("Modify", "Transform Mode", "Scale"),
("Editor Settings", "Global Preferences"),
("Editor Settings", "Editor Settings Manager"),
("Editor Settings", "Keyboard Customization", "Customize Keyboard"),
("Editor Settings", "Keyboard Customization", "Export Keyboard Settings"),
("Editor Settings", "Keyboard Customization", "Import Keyboard Settings"),
# The following menu options are temporarily disabled due to https://github.com/o3de/o3de/issues/6746
#("Editor Settings", "Keyboard Customization", "Export Keyboard Settings"),
#("Editor Settings", "Keyboard Customization", "Import Keyboard Settings"),
]
# 1) Open an existing simple level
helper.init_idle()
helper.open_level("Physics", "Base")
hydra.open_base_level()
# 2) Interact with Edit Menu options
editor_window = pyside_utils.get_editor_main_window()
for option in edit_menu_options:
try:
action = pyside_utils.get_action_for_menu_path(editor_window, "Edit", *option)
Report.info(f"Triggering {action.iconText()}")
action.trigger()
action_triggered = True
except Exception as e:
@@ -26,35 +26,34 @@ def Menus_FileMenuOptions_Work():
:return: None
"""
import editor_python_test_tools.hydra_editor_utils as hydra
import editor_python_test_tools.pyside_utils as pyside_utils
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
file_menu_options = [
("New Level",),
("Open Level",),
("Import",),
#("Open Level",), Temporarily disabled due to https://github.com/o3de/o3de/issues/6605
#("Import",), Temporarily disabled due to https://github.com/o3de/o3de/issues/6746
("Save",),
("Save As",),
#("Save As",), Temporarily disabled due to https://github.com/o3de/o3de/issues/6605
("Save Level Statistics",),
("Edit Project Settings",),
("Edit Platform Settings",),
#("Edit Platform Settings",), Temporarily disabled due to https://github.com/o3de/o3de/issues/6604
("New Project",),
("Open Project",),
("Show Log File",),
("Resave All Slices",),
("Exit",),
]
# 1) Open an existing simple level
helper.init_idle()
helper.open_level("Physics", "Base")
hydra.open_base_level()
# 2) Interact with File Menu options
editor_window = pyside_utils.get_editor_main_window()
for option in file_menu_options:
try:
action = pyside_utils.get_action_for_menu_path(editor_window, "File", *option)
Report.info(f"Triggering {action.iconText()}")
action.trigger()
action_triggered = True
except Exception as e:
@@ -26,32 +26,35 @@ def Menus_ViewMenuOptions_Work():
:return: None
"""
import editor_python_test_tools.hydra_editor_utils as hydra
import editor_python_test_tools.pyside_utils as pyside_utils
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
view_menu_options = [
("Center on Selection",),
("Show Quick Access Bar",),
("Layouts", "Component Entity Layout",),
("Layouts", "Save Layout",),
("Viewport", "Configure Layout"),
("Viewport", "Go to Position"),
("Viewport", "Center on Selection"),
("Viewport", "Go to Location"),
("Viewport", "Remember Location"),
("Viewport", "Switch Camera"),
("Viewport", "Show/Hide Helpers"),
("Viewport", "Show Helpers"),
("Viewport", "Show Icons"),
("Refresh Style",),
]
# 1) Open an existing simple level
helper.init_idle()
helper.open_level("Physics", "Base")
hydra.open_base_level()
# 2) Interact with View Menu options
editor_window = pyside_utils.get_editor_main_window()
for option in view_menu_options:
try:
action = pyside_utils.get_action_for_menu_path(editor_window, "View", *option)
Report.info(f"Triggering {action.iconText()}")
action.trigger()
action_triggered = True
except Exception as e:
@@ -7,48 +7,77 @@ SPDX-License-Identifier: Apache-2.0 OR MIT
import os
import pytest
import sys
import ly_test_tools.environment.file_system as file_system
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../automatedtesting_shared')
from base import TestAutomationBase
@pytest.fixture
def remove_test_level(request, workspace, project):
file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", "tmp_level")], True, True)
def teardown():
file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", "tmp_level")], True, True)
request.addfinalizer(teardown)
from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, EditorParallelTest, EditorTestSuite
@pytest.mark.SUITE_main
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("project", ["AutomatedTesting"])
class TestAutomation(TestAutomationBase):
class TestAutomationNoAutoTestMode(EditorTestSuite):
def test_BasicEditorWorkflows_LevelEntityComponentCRUD(self, request, workspace, editor, launcher_platform,
remove_test_level):
# Disable -autotest_mode and -BatchMode. Tests cannot run in -BatchMode due to UI interactions, and these tests
# interact with modal dialogs
global_extra_cmdline_args = []
class test_AssetPicker_UI_UX(EditorSharedTest):
from .EditorScripts import AssetPicker_UI_UX as test_module
class test_BasicEditorWorkflows_ExistingLevel_EntityComponentCRUD(EditorSharedTest):
from .EditorScripts import BasicEditorWorkflows_ExistingLevel_EntityComponentCRUD as test_module
class test_BasicEditorWorkflows_LevelEntityComponentCRUD(EditorSingleTest):
# Custom teardown to remove level created during test
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")],
True, True)
from .EditorScripts import BasicEditorWorkflows_LevelEntityComponentCRUD as test_module
self._run_test(request, workspace, editor, test_module, batch_mode=False, autotest_mode=False, enable_prefab_system=False)
@pytest.mark.REQUIRES_gpu
def test_BasicEditorWorkflows_GPU_LevelEntityComponentCRUD(self, request, workspace, editor, launcher_platform,
remove_test_level):
from .EditorScripts import BasicEditorWorkflows_LevelEntityComponentCRUD as test_module
self._run_test(request, workspace, editor, test_module, batch_mode=False, autotest_mode=False,
use_null_renderer=False, enable_prefab_system=False)
class test_BasicEditorWorkflows_GPU_LevelEntityComponentCRUD(EditorSingleTest):
# Disable null renderer
use_null_renderer = False
def test_EntityOutlienr_EntityOrdering(self, request, workspace, editor, launcher_platform):
# Custom teardown to remove level created during test
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")],
True, True)
from .EditorScripts import BasicEditorWorkflows_LevelEntityComponentCRUD as test_module
class test_InputBindings_Add_Remove_Input_Events(EditorSharedTest):
from .EditorScripts import InputBindings_Add_Remove_Input_Events as test_module
@pytest.mark.SUITE_main
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("project", ["AutomatedTesting"])
class TestAutomationAutoTestMode(EditorTestSuite):
# Enable only -autotest_mode for these tests. Tests cannot run in -BatchMode due to UI interactions
global_extra_cmdline_args = ["-autotest_mode"]
class test_AssetBrowser_SearchFiltering(EditorSharedTest):
from .EditorScripts import AssetBrowser_SearchFiltering as test_module
class test_AssetBrowser_TreeNavigation(EditorSharedTest):
from .EditorScripts import AssetBrowser_TreeNavigation as test_module
class test_ComponentCRUD_Add_Delete_Components(EditorSharedTest):
from .EditorScripts import ComponentCRUD_Add_Delete_Components as test_module
@pytest.mark.skip("Passes locally/fails on Jenkins. https://github.com/o3de/o3de/issues/6747")
class test_Docking_BasicDockedTools(EditorSharedTest):
from .EditorScripts import Docking_BasicDockedTools as test_module
class test_EntityOutliner_EntityOrdering(EditorSharedTest):
from .EditorScripts import EntityOutliner_EntityOrdering as test_module
self._run_test(
request,
workspace,
editor,
test_module,
batch_mode=False,
autotest_mode=True,
)
class test_Menus_EditMenuOptions_Work(EditorSharedTest):
from .EditorScripts import Menus_EditMenuOptions as test_module
class test_Menus_FileMenuOptions_Work(EditorSharedTest):
from .EditorScripts import Menus_FileMenuOptions as test_module
class test_Menus_ViewMenuOptions_Work(EditorSharedTest):
from .EditorScripts import Menus_ViewMenuOptions as test_module
@@ -1,79 +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
"""
import os
import pytest
import ly_test_tools.environment.file_system as file_system
from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, EditorParallelTest, EditorTestSuite
@pytest.mark.xfail(reason="Optimized tests are experimental, we will enable xfail and monitor them temporarily.")
@pytest.mark.SUITE_main
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("project", ["AutomatedTesting"])
class TestAutomationNoAutoTestMode(EditorTestSuite):
# Disable -autotest_mode and -BatchMode. Tests cannot run in -BatchMode due to UI interactions, and these tests
# interact with modal dialogs
global_extra_cmdline_args = []
enable_prefab_system = False
class test_BasicEditorWorkflows_LevelEntityComponentCRUD(EditorSingleTest):
# Custom teardown to remove slice asset created during test
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")],
True, True)
from .EditorScripts import BasicEditorWorkflows_LevelEntityComponentCRUD as test_module
@pytest.mark.REQUIRES_gpu
class test_BasicEditorWorkflows_GPU_LevelEntityComponentCRUD(EditorSingleTest):
# Disable null renderer
use_null_renderer = False
# Custom teardown to remove slice asset created during test
def teardown(self, request, workspace, editor, editor_test_results, launcher_platform):
file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")],
True, True)
from .EditorScripts import BasicEditorWorkflows_LevelEntityComponentCRUD as test_module
class test_InputBindings_Add_Remove_Input_Events(EditorSharedTest):
from .EditorScripts import InputBindings_Add_Remove_Input_Events as test_module
class test_AssetPicker_UI_UX(EditorSharedTest):
from .EditorScripts import AssetPicker_UI_UX as test_module
@pytest.mark.SUITE_main
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("project", ["AutomatedTesting"])
class TestAutomationAutoTestMode(EditorTestSuite):
# Enable only -autotest_mode for these tests. Tests cannot run in -BatchMode due to UI interactions
global_extra_cmdline_args = ["-autotest_mode"]
enable_prefab_system = False
class test_AssetBrowser_TreeNavigation(EditorSharedTest):
from .EditorScripts import AssetBrowser_TreeNavigation as test_module
class test_AssetBrowser_SearchFiltering(EditorSharedTest):
from .EditorScripts import AssetBrowser_SearchFiltering as test_module
class test_ComponentCRUD_Add_Delete_Components(EditorSharedTest):
from .EditorScripts import ComponentCRUD_Add_Delete_Components as test_module
class test_Menus_ViewMenuOptions_Work(EditorSharedTest):
from .EditorScripts import Menus_ViewMenuOptions as test_module
@pytest.mark.skip(reason="Times out due to dialogs failing to dismiss: LYN-4208")
class test_Menus_FileMenuOptions_Work(EditorSharedTest):
from .EditorScripts import Menus_FileMenuOptions as test_module
class test_BasicEditorWorkflows_ExistingLevel_EntityComponentCRUD(EditorSharedTest):
from .EditorScripts import BasicEditorWorkflows_ExistingLevel_EntityComponentCRUD as test_module
@@ -1,60 +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
"""
import os
import pytest
import sys
import ly_test_tools.environment.file_system as file_system
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../automatedtesting_shared')
from base import TestAutomationBase
@pytest.fixture
def remove_test_level(request, workspace, project):
file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", "tmp_level")], True, True)
def teardown():
file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", "tmp_level")], True, True)
request.addfinalizer(teardown)
@pytest.mark.SUITE_periodic
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("project", ["AutomatedTesting"])
class TestAutomation(TestAutomationBase):
def test_AssetBrowser_TreeNavigation(self, request, workspace, editor, launcher_platform):
from .EditorScripts import AssetBrowser_TreeNavigation as test_module
self._run_test(request, workspace, editor, test_module, batch_mode=False, enable_prefab_system=False)
def test_AssetBrowser_SearchFiltering(self, request, workspace, editor, launcher_platform):
from .EditorScripts import AssetBrowser_SearchFiltering as test_module
self._run_test(request, workspace, editor, test_module, batch_mode=False, enable_prefab_system=False)
def test_AssetPicker_UI_UX(self, request, workspace, editor, launcher_platform):
from .EditorScripts import AssetPicker_UI_UX as test_module
self._run_test(request, workspace, editor, test_module, autotest_mode=False, batch_mode=False, enable_prefab_system=False)
def test_ComponentCRUD_Add_Delete_Components(self, request, workspace, editor, launcher_platform):
from .EditorScripts import ComponentCRUD_Add_Delete_Components as test_module
self._run_test(request, workspace, editor, test_module, batch_mode=False, enable_prefab_system=False)
def test_InputBindings_Add_Remove_Input_Events(self, request, workspace, editor, launcher_platform):
from .EditorScripts import InputBindings_Add_Remove_Input_Events as test_module
self._run_test(request, workspace, editor, test_module, batch_mode=False, autotest_mode=False, enable_prefab_system=False)
def test_Menus_ViewMenuOptions_Work(self, request, workspace, editor, launcher_platform):
from .EditorScripts import Menus_ViewMenuOptions as test_module
self._run_test(request, workspace, editor, test_module, batch_mode=False, enable_prefab_system=False)
@pytest.mark.skip(reason="Times out due to dialogs failing to dismiss: LYN-4208")
def test_Menus_FileMenuOptions_Work(self, request, workspace, editor, launcher_platform):
from .EditorScripts import Menus_FileMenuOptions as test_module
self._run_test(request, workspace, editor, test_module, batch_mode=False, enable_prefab_system=False)
@@ -1,27 +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
"""
import os
import pytest
import sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../automatedtesting_shared')
from base import TestAutomationBase
@pytest.mark.SUITE_sandbox
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("project", ["AutomatedTesting"])
class TestAutomation(TestAutomationBase):
def test_Menus_EditMenuOptions_Work(self, request, workspace, editor, launcher_platform):
from .EditorScripts import Menus_EditMenuOptions as test_module
self._run_test(request, workspace, editor, test_module, batch_mode=False, enable_prefab_system=False)
def test_Docking_BasicDockedTools(self, request, workspace, editor, launcher_platform):
from .EditorScripts import Docking_BasicDockedTools as test_module
self._run_test(request, workspace, editor, test_module, batch_mode=False, enable_prefab_system=False)
@@ -1,28 +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
"""
import os
import pytest
from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, EditorParallelTest, EditorTestSuite
@pytest.mark.SUITE_sandbox
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("project", ["AutomatedTesting"])
class TestAutomationAutoTestMode(EditorTestSuite):
# Enable only -autotest_mode for these tests. Tests cannot run in -BatchMode due to UI interactions
global_extra_cmdline_args = ["-autotest_mode"]
enable_prefab_system = False
class test_Docking_BasicDockedTools(EditorSharedTest):
from .EditorScripts import Docking_BasicDockedTools as test_module
class test_Menus_EditMenuOptions_Work(EditorSharedTest):
from .EditorScripts import Menus_EditMenuOptions as test_module
@@ -51,19 +51,22 @@ def AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude():
import os
import azlmbr.asset as asset
import azlmbr.editor as editor
import azlmbr.legacy.general as general
import azlmbr.bus as bus
import azlmbr.math as math
import azlmbr.prefab as prefab
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.prefab_utils import Prefab
from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
# 1) Open an existing simple level
helper.init_idle()
helper.open_level("Physics", "Base")
helper.open_level("", "Base")
# Set view of planting area for visual debugging
general.set_current_view_position(512.0, 500.0, 38.0)
@@ -71,8 +74,11 @@ def AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude():
# 2) Create a new entity with required vegetation area components
center_point = math.Vector3(512.0, 512.0, 32.0)
asset_path = os.path.join("Slices", "PinkFlower.dynamicslice")
spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 32.0, 32.0, 32.0, asset_path)
flower_asset_path = os.path.join("assets", "objects", "foliage", "grass_flower_pink.azmodel")
flower_prefab = dynveg.create_temp_mesh_prefab(flower_asset_path, "PinkFlower")[0]
spawner_entity = dynveg.create_prefab_vegetation_area("Instance Spawner", center_point, 32.0, 32.0, 32.0, flower_prefab)
# Add a Vegetation Altitude Filter
spawner_entity.add_component("Vegetation Altitude Filter")
@@ -32,7 +32,9 @@ def AltitudeFilter_FilterStageToggle():
import os
import azlmbr.legacy.general as general
import azlmbr.bus as bus
import azlmbr.math as math
import azlmbr.prefab as prefab
import editor_python_test_tools.hydra_editor_utils as hydra
from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg
@@ -44,13 +46,16 @@ def AltitudeFilter_FilterStageToggle():
# Open an existing simple level
helper.init_idle()
helper.open_level("Physics", "Base")
helper.open_level("", "Base")
general.set_current_view_position(512.0, 480.0, 38.0)
# Create basic vegetation entity
position = math.Vector3(512.0, 512.0, 32.0)
asset_path = os.path.join("Slices", "PinkFlower.dynamicslice")
vegetation = dynveg.create_vegetation_area("vegetation", position, 16.0, 16.0, 16.0, asset_path)
flower_asset_path = os.path.join("assets", "objects", "foliage", "grass_flower_pink.azmodel")
flower_prefab = dynveg.create_temp_mesh_prefab(flower_asset_path, "PinkFlower")[0]
vegetation = dynveg.create_prefab_vegetation_area("vegetation", position, 16.0, 16.0, 16.0, flower_prefab)
# Add a Vegetation Altitude Filter to the vegetation area entity
vegetation.add_component("Vegetation Altitude Filter")
@@ -57,7 +57,7 @@ def AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude():
# 1) Open an existing simple level
helper.init_idle()
helper.open_level("Physics", "Base")
helper.open_level("", "Base")
# Set view of planting area for visual debugging
general.set_current_view_position(512.0, 500.0, 38.0)
@@ -65,8 +65,11 @@ def AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude():
# 2) Create a new entity with required vegetation area components
center_point = math.Vector3(512.0, 512.0, 32.0)
asset_path = os.path.join("Slices", "PinkFlower.dynamicslice")
spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 16.0, 16.0, 16.0, asset_path)
flower_asset_path = os.path.join("assets", "objects", "foliage", "grass_flower_pink.azmodel")
flower_prefab = dynveg.create_temp_mesh_prefab(flower_asset_path, "PinkFlower")[0]
spawner_entity = dynveg.create_prefab_vegetation_area("Instance Spawner", center_point, 16.0, 16.0, 16.0, flower_prefab)
# Add a Vegetation Altitude Filter
spawner_entity.add_component("Vegetation Altitude Filter")
@@ -111,7 +111,7 @@ def AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea():
# 4) Create a spawner using a Vegetation Asset List Combiner component and a Weight Selector, and disallow
# spawning empty assets
spawner_entity = dynveg.create_vegetation_area("Spawner Entity", center_point, 16.0, 16.0, 16.0, None)
spawner_entity = dynveg.create_dynamic_slice_vegetation_area("Spawner Entity", center_point, 16.0, 16.0, 16.0, None)
spawner_entity.remove_component("Vegetation Asset List")
spawner_entity.add_component("Vegetation Asset List Combiner")
spawner_entity.add_component("Vegetation Asset Weight Selector")

Some files were not shown because too many files have changed in this diff Show More