123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- """
- 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 logging
- 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
- from ly_test_tools.o3de.asset_processor import AssetProcessor
- from ly_test_tools.launchers.exceptions import WaitTimeoutError
- from ly_test_tools.log.log_monitor import LogMonitor, LogMonitorException
- from ly_test_tools.o3de.editor_test_utils import compile_test_case_name_from_request
- class TestRunError():
- def __init__(self, title, content):
- self.title = title
- self.content = content
- class TestAutomationBase:
- MAX_TIMEOUT = 180 # 3 minutes max for a test to run
- WAIT_FOR_CRASH_LOG = 20 # Seconds for waiting for a crash log
- TEST_FAIL_RETCODE = 0xF # Return code for test failure
-
- test_times = {}
- asset_processor = None
-
- def setup_class(cls):
- cls.test_times = {}
- cls.editor_times = {}
- cls.asset_processor = None
-
- def teardown_class(cls):
- logger = logging.getLogger(__name__)
- # Report times
- time_info_str = "Individual test times (Full test time, Editor test time):\n"
- for testcase_name, t in cls.test_times.items():
- editor_t = cls.editor_times[testcase_name]
- time_info_str += f"{testcase_name}: (Full:{t} sec, Editor:{editor_t} sec)\n"
-
- logger.info(time_info_str)
- if cls.asset_processor is not None:
- cls.asset_processor.teardown()
- # Kill all ly processes
- cls._kill_ly_processes(include_asset_processor=True)
- def _run_test(self, request, workspace, editor, testcase_module, extra_cmdline_args=[], batch_mode=True,
- autotest_mode=True, use_null_renderer=True):
- test_starttime = time.time()
- self.logger = logging.getLogger(__name__)
- errors = []
- testcase_name = os.path.basename(testcase_module.__file__)
-
- #########
- # Setup #
- if self.asset_processor is None:
- self._kill_ly_processes(include_asset_processor=True)
- self.__class__.asset_processor = AssetProcessor(workspace)
- self.asset_processor.backup_ap_settings()
- else:
- self._kill_ly_processes(include_asset_processor=False)
- if not self.asset_processor.process_exists():
- self.asset_processor.start()
- self.asset_processor.wait_for_idle()
- def teardown():
- if os.path.exists(workspace.paths.editor_log()):
- workspace.artifact_manager.save_artifact(workspace.paths.editor_log())
- try:
- file_system.restore_backup(workspace.paths.editor_log(), workspace.paths.project_log())
- except FileNotFoundError as e:
- self.logger.debug(f"File restoration failed, editor log could not be found.\nError: {e}")
- editor.stop()
- request.addfinalizer(teardown)
- if os.path.exists(workspace.paths.editor_log()):
- self.logger.debug("Creating backup for existing editor log before test run.")
- file_system.create_backup(workspace.paths.editor_log(), workspace.paths.project_log())
-
- ############
- # Run test #
-
- editor_starttime = time.time()
- self.logger.debug("Running automated test")
- testcase_module_filepath = self._get_testcase_module_filepath(testcase_module)
- compiled_test_case_name = compile_test_case_name_from_request(request)
- pycmd = ["--runpythontest", testcase_module_filepath, f"-pythontestcase={compiled_test_case_name}",
- "--regset=/Amazon/Preferences/EnablePrefabSystem=true",
- f"--regset-file={path.join(workspace.paths.engine_root(), 'Registry', 'prefab.test.setreg')}"]
- if use_null_renderer:
- pycmd += ["-rhi=null"]
- if batch_mode:
- pycmd += ["-BatchMode"]
- if autotest_mode:
- pycmd += ["-autotest_mode"]
- pycmd += extra_cmdline_args
- editor.args.extend(pycmd) # args are added to the WinLauncher start command
- editor.start(backupFiles = False, launch_ap = False)
- try:
- editor.wait(TestAutomationBase.MAX_TIMEOUT)
- except WaitTimeoutError:
- errors.append(TestRunError("TIMEOUT", f"Editor did not close after {TestAutomationBase.MAX_TIMEOUT} seconds, verify the test is ending and the application didn't freeze"))
- editor.stop()
-
- output = editor.get_output()
- self.logger.debug("Test output:\n" + output)
- return_code = editor.get_returncode()
-
- self.editor_times[testcase_name] = time.time() - editor_starttime
-
- ###################
- # Validate result #
-
- if return_code != 0:
- if output:
- error_str = "Test failed, output:\n" + output.replace("\n", "\n ")
- else:
- error_str = "Test failed, no output available..\n"
- 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 = workspace.paths.crash_log()
- try:
- waiter.wait_for(lambda: os.path.exists(crash_log), timeout=TestAutomationBase.WAIT_FOR_CRASH_LOG)
- except AssertionError:
- pass
-
- try:
- with open(crash_log) as f:
- crash_info = f.read()
- except Exception as ex:
- crash_info += f"\n{str(ex)}"
- return_code_str = f"0x{return_code:0X}" if isinstance(return_code, int) else "None"
- error_str = f"Editor.exe crashed, return code: {return_code_str}\n\nCrash log:\n{crash_info}"
- errors.append(TestRunError("CRASH", error_str))
-
- self.test_times[testcase_name] = time.time() - test_starttime
-
- ###################
- # Error reporting #
-
- if errors:
- error_str = "Error list:\n"
- longest_title = max([len(e.title) for e in errors])
- longest_title += (longest_title % 2) # make it even spaces
- longest_title = max(30, longest_title) # at least 30 -
- header_decoration = "-".center(longest_title, "-") + "\n"
- for e in errors:
- error_str += header_decoration
- error_str += f" {e.title} ".center(longest_title, "-") + "\n"
- error_str += header_decoration
- for line in e.content.split("\n"):
- error_str += f" {line}\n"
-
- error_str += header_decoration
- error_str += "Editor log:\n"
- try:
- with open(workspace.paths.editor_log()) as f:
- log_basename = os.path.basename(workspace.paths.editor_log())
- for line in f.readlines():
- error_str += f"|{log_basename}| {line}"
- except Exception as ex:
- error_str += f"-- No log available ({ex})--"
- pytest.fail(error_str)
-
- @staticmethod
- def _kill_ly_processes(include_asset_processor=True):
- LY_PROCESSES = [
- 'Editor', 'Profiler', 'RemoteConsole', 'AutomatedTesting.ServerLauncher', 'o3de'
- ]
- AP_PROCESSES = [
- 'AssetProcessor', 'AssetProcessorBatch', 'AssetBuilder', 'CrySCompileServer',
- 'rc' # Resource Compiler
- ]
-
- if include_asset_processor:
- process_utils.kill_processes_named(LY_PROCESSES+AP_PROCESSES, ignore_extensions=True)
- else:
- process_utils.kill_processes_named(LY_PROCESSES, ignore_extensions=True)
-
- @staticmethod
- def _get_testcase_module_filepath(testcase_module):
- # type: (Module) -> str
- """
- return the full path of the test module
- :param testcase_module: The testcase python module being tested
- :return str: The full path to the testcase module
- """
- return os.path.splitext(testcase_module.__file__)[0] + ".py"
|