Browse Source

MaterialEditor BasicTests added to AutomatedTesting for AR (#3022)

* MaterialEditor BasicTests added to AutomatedTesting for AR

Signed-off-by: Scott Murray <[email protected]>

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

Signed-off-by: Scott Murray <[email protected]>
smurly 4 years ago
parent
commit
2564e8f8dc

+ 19 - 0
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
+                }
+            }
+        }
+    }
+}

+ 13 - 2
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)

+ 183 - 0
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()

+ 274 - 0
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)
+    )

+ 63 - 0
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,
         )
+
+
[email protected]("project", ["AutomatedTesting"])
[email protected]("launcher_platform", ['windows_generic'])
[email protected]
+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",
+        )