From 2564e8f8dce6fc65804500dee166f72a574477f9 Mon Sep 17 00:00:00 2001 From: smurly Date: Wed, 11 Aug 2021 12:15:31 -0700 Subject: [PATCH] MaterialEditor BasicTests added to AutomatedTesting for AR (#3022) * MaterialEditor BasicTests added to AutomatedTesting for AR Signed-off-by: Scott Murray * launch_and_validate_results adding a waiter.wait_for to the log monitor so the log file exists Signed-off-by: Scott Murray --- .../AssetProcessorGamePlatformConfig.setreg | 19 ++ .../hydra_test_utils.py | 15 +- .../hydra_AtomMaterialEditor_BasicTests.py | 183 ++++++++++++ .../atom_utils/material_editor_utils.py | 274 ++++++++++++++++++ .../atom_renderer/test_Atom_MainSuite.py | 63 ++++ 5 files changed, 552 insertions(+), 2 deletions(-) create mode 100644 AutomatedTesting/AssetProcessorGamePlatformConfig.setreg create mode 100644 AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_AtomMaterialEditor_BasicTests.py create mode 100644 AutomatedTesting/Gem/PythonTests/atom_renderer/atom_utils/material_editor_utils.py diff --git a/AutomatedTesting/AssetProcessorGamePlatformConfig.setreg b/AutomatedTesting/AssetProcessorGamePlatformConfig.setreg new file mode 100644 index 0000000000..5457b3f1ca --- /dev/null +++ b/AutomatedTesting/AssetProcessorGamePlatformConfig.setreg @@ -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 + } + } + } + } +} diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py index 3004b9ec7d..3d4d9ea419 100644 --- a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py @@ -29,7 +29,7 @@ def teardown_editor(editor): 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=[], - timeout=300): + timeout=300, log_file_name="Editor.log"): """ Runs the Editor with the specified script, and monitors for expected log lines. :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 cfg_args: Additional arguments for CFG, such as LevelName. :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) request.addfinalizer(lambda: teardown_editor(editor)) @@ -58,7 +59,17 @@ def launch_and_validate_results(request, test_directory, editor, editor_script, 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 log_monitor = ly_test_tools.log.log_monitor.LogMonitor(launcher=editor, log_file_path=editorlog_file) diff --git a/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_AtomMaterialEditor_BasicTests.py b/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_AtomMaterialEditor_BasicTests.py new file mode 100644 index 0000000000..b3c51ca912 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_AtomMaterialEditor_BasicTests.py @@ -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() diff --git a/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_utils/material_editor_utils.py b/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_utils/material_editor_utils.py new file mode 100644 index 0000000000..77d1285188 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_utils/material_editor_utils.py @@ -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) + ) diff --git a/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_MainSuite.py b/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_MainSuite.py index 39689816b8..be75801b63 100644 --- a/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_MainSuite.py +++ b/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_MainSuite.py @@ -11,6 +11,7 @@ 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 atom_renderer.atom_utils.atom_constants import LIGHT_TYPES @@ -242,3 +243,65 @@ class TestAtomEditorComponentsMain(object): null_renderer=True, 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", + )