MaterialEditor BasicTests added to AutomatedTesting for AR (#3022)

* MaterialEditor BasicTests added to AutomatedTesting for AR

Signed-off-by: Scott Murray <scottmur@amazon.com>

* launch_and_validate_results adding a waiter.wait_for to the log monitor so the log file exists

Signed-off-by: Scott Murray <scottmur@amazon.com>
monroegm-disable-blank-issue-2
smurly 4 years ago committed by GitHub
parent 6d1bb8e439
commit 2564e8f8dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,19 @@
{
"Amazon": {
"AssetProcessor": {
"Settings": {
"RC cgf": {
"ignore": true
},
"RC fbx": {
"ignore": true
},
"ScanFolder AtomTestData": {
"watch": "@ENGINEROOT@/Gems/Atom/TestData",
"recursive": 1,
"order": 1000
}
}
}
}
}

@ -29,7 +29,7 @@ def teardown_editor(editor):
def launch_and_validate_results(request, test_directory, editor, editor_script, expected_lines, unexpected_lines=[], def launch_and_validate_results(request, test_directory, editor, editor_script, expected_lines, unexpected_lines=[],
halt_on_unexpected=False, run_python="--runpythontest", auto_test_mode=True, null_renderer=True, cfg_args=[], halt_on_unexpected=False, run_python="--runpythontest", auto_test_mode=True, null_renderer=True, cfg_args=[],
timeout=300): timeout=300, log_file_name="Editor.log"):
""" """
Runs the Editor with the specified script, and monitors for expected log lines. Runs the Editor with the specified script, and monitors for expected log lines.
:param request: Special fixture providing information of the requesting test function. :param request: Special fixture providing information of the requesting test function.
@ -44,6 +44,7 @@ def launch_and_validate_results(request, test_directory, editor, editor_script,
:param null_renderer: Specifies the test does not require the renderer. Defaults to True. :param null_renderer: Specifies the test does not require the renderer. Defaults to True.
:param cfg_args: Additional arguments for CFG, such as LevelName. :param cfg_args: Additional arguments for CFG, such as LevelName.
:param timeout: Length of time for test to run. Default is 60. :param timeout: Length of time for test to run. Default is 60.
:param log_file_name: Name of the log file created by the editor. Defaults to 'Editor.log'
""" """
test_case = os.path.join(test_directory, editor_script) test_case = os.path.join(test_directory, editor_script)
request.addfinalizer(lambda: teardown_editor(editor)) request.addfinalizer(lambda: teardown_editor(editor))
@ -58,7 +59,17 @@ def launch_and_validate_results(request, test_directory, editor, editor_script,
with editor.start(): with editor.start():
editorlog_file = os.path.join(editor.workspace.paths.project_log(), 'Editor.log') editorlog_file = os.path.join(editor.workspace.paths.project_log(), log_file_name)
# Log monitor requires the file to exist.
logger.debug(f"Waiting until log file <{editorlog_file}> exists...")
waiter.wait_for(
lambda: os.path.exists(editorlog_file),
timeout=60,
exc=f"Log file '{editorlog_file}' was never created by another process.",
interval=1,
)
logger.debug(f"Done! log file <{editorlog_file}> exists.")
# Initialize the log monitor and set time to wait for log creation # Initialize the log monitor and set time to wait for log creation
log_monitor = ly_test_tools.log.log_monitor.LogMonitor(launcher=editor, log_file_path=editorlog_file) log_monitor = ly_test_tools.log.log_monitor.LogMonitor(launcher=editor, log_file_path=editorlog_file)

@ -0,0 +1,183 @@
"""
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 azlmbr.materialeditor will fail with a ModuleNotFound error when using this script with Editor.exe
This is because azlmbr.materialeditor only binds to MaterialEditor.exe and not Editor.exe
You need to launch this script with MaterialEditor.exe in order for azlmbr.materialeditor to appear.
"""
import os
import sys
import time
import azlmbr.math as math
import azlmbr.paths
sys.path.append(os.path.join(azlmbr.paths.devassets, "Gem", "PythonTests"))
import atom_renderer.atom_utils.material_editor_utils as material_editor
NEW_MATERIAL = "test_material.material"
NEW_MATERIAL_1 = "test_material_1.material"
NEW_MATERIAL_2 = "test_material_2.material"
TEST_MATERIAL_1 = "001_DefaultWhite.material"
TEST_MATERIAL_2 = "002_BaseColorLerp.material"
TEST_MATERIAL_3 = "003_MetalMatte.material"
TEST_DATA_PATH = os.path.join(
azlmbr.paths.devroot, "Gems", "Atom", "TestData", "TestData", "Materials", "StandardPbrTestCases"
)
MATERIAL_TYPE_PATH = os.path.join(
azlmbr.paths.devroot, "Gems", "Atom", "Feature", "Common", "Assets",
"Materials", "Types", "StandardPBR.materialtype",
)
def run():
"""
Summary:
Material Editor basic tests including the below
1. Opening an Existing Asset
2. Creating a New Asset
3. Closing Selected Material
4. Closing All Materials
5. Closing all but Selected Material
6. Saving Material
7. Saving as a New Material
8. Saving as a Child Material
9. Saving all Open Materials
Expected Result:
All the above functions work as expected in Material Editor.
:return: None
"""
# 1) Test Case: Opening an Existing Asset
document_id = material_editor.open_material(MATERIAL_TYPE_PATH)
print(f"Material opened: {material_editor.is_open(document_id)}")
# Verify if the test material exists initially
target_path = os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Materials", NEW_MATERIAL)
print(f"Test asset doesn't exist initially: {not os.path.exists(target_path)}")
# 2) Test Case: Creating a New Material Using Existing One
material_editor.save_document_as_child(document_id, target_path)
material_editor.wait_for_condition(lambda: os.path.exists(target_path), 2.0)
print(f"New asset created: {os.path.exists(target_path)}")
# Verify if the newly created document is open
new_document_id = material_editor.open_material(target_path)
material_editor.wait_for_condition(lambda: material_editor.is_open(new_document_id))
print(f"New Material opened: {material_editor.is_open(new_document_id)}")
# 3) Test Case: Closing Selected Material
print(f"Material closed: {material_editor.close_document(new_document_id)}")
# Open materials initially
document1_id, document2_id, document3_id = (
material_editor.open_material(os.path.join(TEST_DATA_PATH, material))
for material in [TEST_MATERIAL_1, TEST_MATERIAL_2, TEST_MATERIAL_3]
)
# 4) Test Case: Closing All Materials
print(f"All documents closed: {material_editor.close_all_documents()}")
# 5) Test Case: Closing all but Selected Material
document1_id, document2_id, document3_id = (
material_editor.open_material(os.path.join(TEST_DATA_PATH, material))
for material in [TEST_MATERIAL_1, TEST_MATERIAL_2, TEST_MATERIAL_3]
)
result = material_editor.close_all_except_selected(document1_id)
print(f"Close All Except Selected worked as expected: {result and material_editor.is_open(document1_id)}")
# 6) Test Case: Saving Material
document_id = material_editor.open_material(os.path.join(TEST_DATA_PATH, TEST_MATERIAL_1))
property_name = azlmbr.name.Name("baseColor.color")
initial_color = material_editor.get_property(document_id, property_name)
# Assign new color to the material file and save the actual material
expected_color = math.Color(0.25, 0.25, 0.25, 1.0)
material_editor.set_property(document_id, property_name, expected_color)
material_editor.save_document(document_id)
# 7) Test Case: Saving as a New Material
# Assign new color to the material file and save the document as copy
expected_color_1 = math.Color(0.5, 0.5, 0.5, 1.0)
material_editor.set_property(document_id, property_name, expected_color_1)
target_path_1 = os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Materials", NEW_MATERIAL_1)
material_editor.save_document_as_copy(document_id, target_path_1)
time.sleep(2.0)
# 8) Test Case: Saving as a Child Material
# Assign new color to the material file save the document as child
expected_color_2 = math.Color(0.75, 0.75, 0.75, 1.0)
material_editor.set_property(document_id, property_name, expected_color_2)
target_path_2 = os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Materials", NEW_MATERIAL_2)
material_editor.save_document_as_child(document_id, target_path_2)
time.sleep(2.0)
# Close/Reopen documents
material_editor.close_all_documents()
document_id = material_editor.open_material(os.path.join(TEST_DATA_PATH, TEST_MATERIAL_1))
document1_id = material_editor.open_material(target_path_1)
document2_id = material_editor.open_material(target_path_2)
# Verify if the changes are saved in the actual document
actual_color = material_editor.get_property(document_id, property_name)
print(f"Actual Document saved with changes: {material_editor.compare_colors(actual_color, expected_color)}")
# Verify if the changes are saved in the document saved as copy
actual_color = material_editor.get_property(document1_id, property_name)
result_copy = material_editor.compare_colors(actual_color, expected_color_1)
print(f"Document saved as copy is saved with changes: {result_copy}")
# Verify if the changes are saved in the document saved as child
actual_color = material_editor.get_property(document2_id, property_name)
result_child = material_editor.compare_colors(actual_color, expected_color_2)
print(f"Document saved as child is saved with changes: {result_child}")
# Revert back the changes in the actual document
material_editor.set_property(document_id, property_name, initial_color)
material_editor.save_document(document_id)
material_editor.close_all_documents()
# 9) Test Case: Saving all Open Materials
# Open first material and make change to the values
document1_id = material_editor.open_material(os.path.join(TEST_DATA_PATH, TEST_MATERIAL_1))
property1_name = azlmbr.name.Name("metallic.factor")
initial_metallic_factor = material_editor.get_property(document1_id, property1_name)
expected_metallic_factor = 0.444
material_editor.set_property(document1_id, property1_name, expected_metallic_factor)
# Open second material and make change to the values
document2_id = material_editor.open_material(os.path.join(TEST_DATA_PATH, TEST_MATERIAL_2))
property2_name = azlmbr.name.Name("baseColor.color")
initial_color = material_editor.get_property(document2_id, property2_name)
expected_color = math.Color(0.4156, 0.0196, 0.6862, 1.0)
material_editor.set_property(document2_id, property2_name, expected_color)
# Save all and close all documents
material_editor.save_all()
material_editor.close_all_documents()
# Reopen materials and verify values
document1_id = material_editor.open_material(os.path.join(TEST_DATA_PATH, TEST_MATERIAL_1))
result = material_editor.is_close(
material_editor.get_property(document1_id, property1_name), expected_metallic_factor, 0.00001
)
document2_id = material_editor.open_material(os.path.join(TEST_DATA_PATH, TEST_MATERIAL_2))
result = result and material_editor.compare_colors(
expected_color, material_editor.get_property(document2_id, property2_name))
print(f"Save All worked as expected: {result}")
# Revert the changes made
material_editor.set_property(document1_id, property1_name, initial_metallic_factor)
material_editor.set_property(document2_id, property2_name, initial_color)
material_editor.save_all()
material_editor.close_all_documents()
if __name__ == "__main__":
run()

@ -0,0 +1,274 @@
"""
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 azlmbr.materialeditor will fail with a ModuleNotFound error when using this script with Editor.exe
This is because azlmbr.materialeditor only binds to MaterialEditor.exe and not Editor.exe
You need to launch this script with MaterialEditor.exe in order for azlmbr.materialeditor to appear.
"""
import os
import sys
import time
import azlmbr.atom
import azlmbr.materialeditor as materialeditor
import azlmbr.bus as bus
import azlmbr.atomtools.general as general
def is_close(actual, expected, buffer=sys.float_info.min):
"""
:param actual: actual value
:param expected: expected value
:param buffer: acceptable variation from expected
:return: bool
"""
return abs(actual - expected) < buffer
def compare_colors(color1, color2, buffer=0.00001):
"""
Compares the red, green and blue properties of a color allowing a slight variance of buffer
:param color1: first color to compare
:param color2: second color
:param buffer: allowed variance in individual color value
:return: bool
"""
return (
is_close(color1.r, color2.r, buffer)
and is_close(color1.g, color2.g, buffer)
and is_close(color1.b, color2.b, buffer)
)
def open_material(file_path):
"""
:return: uuid of material document opened
"""
return materialeditor.MaterialDocumentSystemRequestBus(bus.Broadcast, "OpenDocument", file_path)
def is_open(document_id):
"""
:return: bool
"""
return materialeditor.MaterialDocumentRequestBus(bus.Event, "IsOpen", document_id)
def save_document(document_id):
"""
:return: bool success
"""
return materialeditor.MaterialDocumentSystemRequestBus(bus.Broadcast, "SaveDocument", document_id)
def save_document_as_copy(document_id, target_path):
"""
:return: bool success
"""
return materialeditor.MaterialDocumentSystemRequestBus(
bus.Broadcast, "SaveDocumentAsCopy", document_id, target_path
)
def save_document_as_child(document_id, target_path):
"""
:return: bool success
"""
return materialeditor.MaterialDocumentSystemRequestBus(
bus.Broadcast, "SaveDocumentAsChild", document_id, target_path
)
def save_all():
"""
:return: bool success
"""
return materialeditor.MaterialDocumentSystemRequestBus(bus.Broadcast, "SaveAllDocuments")
def close_document(document_id):
"""
:return: bool success
"""
return materialeditor.MaterialDocumentSystemRequestBus(bus.Broadcast, "CloseDocument", document_id)
def close_all_documents():
"""
:return: bool success
"""
return materialeditor.MaterialDocumentSystemRequestBus(bus.Broadcast, "CloseAllDocuments")
def close_all_except_selected(document_id):
"""
:return: bool success
"""
return materialeditor.MaterialDocumentSystemRequestBus(bus.Broadcast, "CloseAllDocumentsExcept", document_id)
def get_property(document_id, property_name):
"""
:return: property value or invalid value if the document is not open or the property_name can't be found
"""
return materialeditor.MaterialDocumentRequestBus(bus.Event, "GetPropertyValue", document_id, property_name)
def set_property(document_id, property_name, value):
materialeditor.MaterialDocumentRequestBus(bus.Event, "SetPropertyValue", document_id, property_name, value)
def is_pane_visible(pane_name):
"""
:return: bool
"""
return materialeditor.MaterialEditorWindowRequestBus(bus.Broadcast, "IsDockWidgetVisible", pane_name)
def set_pane_visibility(pane_name, value):
materialeditor.MaterialEditorWindowRequestBus(bus.Broadcast, "SetDockWidgetVisible", pane_name, value)
def select_lighting_config(config_name):
azlmbr.materialeditor.MaterialViewportRequestBus(azlmbr.bus.Broadcast, "SelectLightingPresetByName", config_name)
def set_grid_enable_disable(value):
azlmbr.materialeditor.MaterialViewportRequestBus(azlmbr.bus.Broadcast, "SetGridEnabled", value)
def get_grid_enable_disable():
"""
:return: bool
"""
return azlmbr.materialeditor.MaterialViewportRequestBus(azlmbr.bus.Broadcast, "GetGridEnabled")
def set_shadowcatcher_enable_disable(value):
azlmbr.materialeditor.MaterialViewportRequestBus(azlmbr.bus.Broadcast, "SetShadowCatcherEnabled", value)
def get_shadowcatcher_enable_disable():
"""
:return: bool
"""
return azlmbr.materialeditor.MaterialViewportRequestBus(azlmbr.bus.Broadcast, "GetShadowCatcherEnabled")
def select_model_config(configname):
azlmbr.materialeditor.MaterialViewportRequestBus(azlmbr.bus.Broadcast, "SelectModelPresetByName", configname)
def wait_for_condition(function, timeout_in_seconds=1.0):
# type: (function, float) -> bool
"""
Function to run until it returns True or timeout is reached
the function can have no parameters and
waiting idle__wait_* is handled here not in the function
:param function: a function that returns a boolean indicating a desired condition is achieved
:param timeout_in_seconds: when reached, function execution is abandoned and False is returned
"""
with Timeout(timeout_in_seconds) as t:
while True:
try:
general.idle_wait_frames(1)
except Exception:
print("WARNING: Couldn't wait for frame")
if t.timed_out:
return False
ret = function()
if not isinstance(ret, bool):
raise TypeError("return value for wait_for_condition function must be a bool")
if ret:
return True
class Timeout:
# type: (float) -> None
"""
contextual timeout
:param seconds: float seconds to allow before timed_out is True
"""
def __init__(self, seconds):
self.seconds = seconds
def __enter__(self):
self.die_after = time.time() + self.seconds
return self
def __exit__(self, type, value, traceback):
pass
@property
def timed_out(self):
return time.time() > self.die_after
screenshotsFolder = os.path.join(azlmbr.paths.devroot, "AtomTest", "Cache" "pc", "Screenshots")
class ScreenshotHelper:
"""
A helper to capture screenshots and wait for them.
"""
def __init__(self, idle_wait_frames_callback):
super().__init__()
self.done = False
self.capturedScreenshot = False
self.max_frames_to_wait = 60
self.idle_wait_frames_callback = idle_wait_frames_callback
def capture_screenshot_blocking(self, filename):
"""
Capture a screenshot and block the execution until the screenshot has been written to the disk.
"""
self.handler = azlmbr.atom.FrameCaptureNotificationBusHandler()
self.handler.connect()
self.handler.add_callback("OnCaptureFinished", self.on_screenshot_captured)
self.done = False
self.capturedScreenshot = False
success = azlmbr.atom.FrameCaptureRequestBus(azlmbr.bus.Broadcast, "CaptureScreenshot", filename)
if success:
self.wait_until_screenshot()
print("Screenshot taken.")
else:
print("screenshot failed")
return self.capturedScreenshot
def on_screenshot_captured(self, parameters):
# the parameters come in as a tuple
if parameters[0]:
print("screenshot saved: {}".format(parameters[1]))
self.capturedScreenshot = True
else:
print("screenshot failed: {}".format(parameters[1]))
self.done = True
self.handler.disconnect()
def wait_until_screenshot(self):
frames_waited = 0
while self.done == False:
self.idle_wait_frames_callback(1)
if frames_waited > self.max_frames_to_wait:
print("timeout while waiting for the screenshot to be written")
self.handler.disconnect()
break
else:
frames_waited = frames_waited + 1
print("(waited {} frames)".format(frames_waited))
def capture_screenshot(file_path):
return ScreenshotHelper(azlmbr.atomtools.general.idle_wait_frames).capture_screenshot_blocking(
os.path.join(file_path)
)

@ -11,6 +11,7 @@ import os
import pytest import pytest
import ly_test_tools.environment.file_system as file_system
import editor_python_test_tools.hydra_test_utils as hydra import editor_python_test_tools.hydra_test_utils as hydra
from atom_renderer.atom_utils.atom_constants import LIGHT_TYPES from atom_renderer.atom_utils.atom_constants import LIGHT_TYPES
@ -242,3 +243,65 @@ class TestAtomEditorComponentsMain(object):
null_renderer=True, null_renderer=True,
cfg_args=cfg_args, cfg_args=cfg_args,
) )
@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()
def teardown():
# Cleanup our newly created materials
delete_files()
request.addfinalizer(teardown)
@pytest.mark.parametrize("exe_file_name", ["MaterialEditor"])
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 = [
# "Trace::Assert",
# "Trace::Error",
"Traceback (most recent call last):"
]
hydra.launch_and_validate_results(
request,
TEST_DIRECTORY,
generic_launcher,
"hydra_AtomMaterialEditor_BasicTests.py",
run_python="--runpython",
timeout=80,
expected_lines=expected_lines,
unexpected_lines=unexpected_lines,
halt_on_unexpected=True,
log_file_name="MaterialEditor.log",
)

Loading…
Cancel
Save