base.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. """
  6. import os
  7. import logging
  8. import sys
  9. import pytest
  10. import time
  11. from os import path
  12. import ly_test_tools.environment.file_system as file_system
  13. import ly_test_tools.environment.process_utils as process_utils
  14. import ly_test_tools.environment.waiter as waiter
  15. from ly_test_tools.o3de.asset_processor import AssetProcessor
  16. from ly_test_tools.launchers.exceptions import WaitTimeoutError
  17. from ly_test_tools.log.log_monitor import LogMonitor, LogMonitorException
  18. from ly_test_tools.o3de.editor_test_utils import compile_test_case_name_from_request
  19. class TestRunError():
  20. def __init__(self, title, content):
  21. self.title = title
  22. self.content = content
  23. class TestAutomationBase:
  24. MAX_TIMEOUT = 180 # 3 minutes max for a test to run
  25. WAIT_FOR_CRASH_LOG = 20 # Seconds for waiting for a crash log
  26. TEST_FAIL_RETCODE = 0xF # Return code for test failure
  27. test_times = {}
  28. asset_processor = None
  29. def setup_class(cls):
  30. cls.test_times = {}
  31. cls.editor_times = {}
  32. cls.asset_processor = None
  33. def teardown_class(cls):
  34. logger = logging.getLogger(__name__)
  35. # Report times
  36. time_info_str = "Individual test times (Full test time, Editor test time):\n"
  37. for testcase_name, t in cls.test_times.items():
  38. editor_t = cls.editor_times[testcase_name]
  39. time_info_str += f"{testcase_name}: (Full:{t} sec, Editor:{editor_t} sec)\n"
  40. logger.info(time_info_str)
  41. if cls.asset_processor is not None:
  42. cls.asset_processor.teardown()
  43. # Kill all ly processes
  44. cls._kill_ly_processes(include_asset_processor=True)
  45. def _run_test(self, request, workspace, editor, testcase_module, extra_cmdline_args=[], batch_mode=True,
  46. autotest_mode=True, use_null_renderer=True):
  47. test_starttime = time.time()
  48. self.logger = logging.getLogger(__name__)
  49. errors = []
  50. testcase_name = os.path.basename(testcase_module.__file__)
  51. #########
  52. # Setup #
  53. if self.asset_processor is None:
  54. self._kill_ly_processes(include_asset_processor=True)
  55. self.__class__.asset_processor = AssetProcessor(workspace)
  56. self.asset_processor.backup_ap_settings()
  57. else:
  58. self._kill_ly_processes(include_asset_processor=False)
  59. if not self.asset_processor.process_exists():
  60. self.asset_processor.start()
  61. self.asset_processor.wait_for_idle()
  62. def teardown():
  63. if os.path.exists(workspace.paths.editor_log()):
  64. workspace.artifact_manager.save_artifact(workspace.paths.editor_log())
  65. try:
  66. file_system.restore_backup(workspace.paths.editor_log(), workspace.paths.project_log())
  67. except FileNotFoundError as e:
  68. self.logger.debug(f"File restoration failed, editor log could not be found.\nError: {e}")
  69. editor.stop()
  70. request.addfinalizer(teardown)
  71. if os.path.exists(workspace.paths.editor_log()):
  72. self.logger.debug("Creating backup for existing editor log before test run.")
  73. file_system.create_backup(workspace.paths.editor_log(), workspace.paths.project_log())
  74. ############
  75. # Run test #
  76. editor_starttime = time.time()
  77. self.logger.debug("Running automated test")
  78. testcase_module_filepath = self._get_testcase_module_filepath(testcase_module)
  79. compiled_test_case_name = compile_test_case_name_from_request(request)
  80. pycmd = ["--runpythontest", testcase_module_filepath, f"-pythontestcase={compiled_test_case_name}",
  81. "--regset=/Amazon/Preferences/EnablePrefabSystem=true",
  82. f"--regset-file={path.join(workspace.paths.engine_root(), 'Registry', 'prefab.test.setreg')}"]
  83. if use_null_renderer:
  84. pycmd += ["-rhi=null"]
  85. if batch_mode:
  86. pycmd += ["-BatchMode"]
  87. if autotest_mode:
  88. pycmd += ["-autotest_mode"]
  89. pycmd += extra_cmdline_args
  90. editor.args.extend(pycmd) # args are added to the WinLauncher start command
  91. editor.start(backupFiles = False, launch_ap = False)
  92. try:
  93. editor.wait(TestAutomationBase.MAX_TIMEOUT)
  94. except WaitTimeoutError:
  95. 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"))
  96. editor.stop()
  97. output = editor.get_output()
  98. self.logger.debug("Test output:\n" + output)
  99. return_code = editor.get_returncode()
  100. self.editor_times[testcase_name] = time.time() - editor_starttime
  101. ###################
  102. # Validate result #
  103. if return_code != 0:
  104. if output:
  105. error_str = "Test failed, output:\n" + output.replace("\n", "\n ")
  106. else:
  107. error_str = "Test failed, no output available..\n"
  108. errors.append(TestRunError("FAILED TEST", error_str))
  109. if return_code and return_code != TestAutomationBase.TEST_FAIL_RETCODE: # Crashed
  110. crash_info = "-- No crash log available --"
  111. crash_log = workspace.paths.crash_log()
  112. try:
  113. waiter.wait_for(lambda: os.path.exists(crash_log), timeout=TestAutomationBase.WAIT_FOR_CRASH_LOG)
  114. except AssertionError:
  115. pass
  116. try:
  117. with open(crash_log) as f:
  118. crash_info = f.read()
  119. except Exception as ex:
  120. crash_info += f"\n{str(ex)}"
  121. return_code_str = f"0x{return_code:0X}" if isinstance(return_code, int) else "None"
  122. error_str = f"Editor.exe crashed, return code: {return_code_str}\n\nCrash log:\n{crash_info}"
  123. errors.append(TestRunError("CRASH", error_str))
  124. self.test_times[testcase_name] = time.time() - test_starttime
  125. ###################
  126. # Error reporting #
  127. if errors:
  128. error_str = "Error list:\n"
  129. longest_title = max([len(e.title) for e in errors])
  130. longest_title += (longest_title % 2) # make it even spaces
  131. longest_title = max(30, longest_title) # at least 30 -
  132. header_decoration = "-".center(longest_title, "-") + "\n"
  133. for e in errors:
  134. error_str += header_decoration
  135. error_str += f" {e.title} ".center(longest_title, "-") + "\n"
  136. error_str += header_decoration
  137. for line in e.content.split("\n"):
  138. error_str += f" {line}\n"
  139. error_str += header_decoration
  140. error_str += "Editor log:\n"
  141. try:
  142. with open(workspace.paths.editor_log()) as f:
  143. log_basename = os.path.basename(workspace.paths.editor_log())
  144. for line in f.readlines():
  145. error_str += f"|{log_basename}| {line}"
  146. except Exception as ex:
  147. error_str += f"-- No log available ({ex})--"
  148. pytest.fail(error_str)
  149. @staticmethod
  150. def _kill_ly_processes(include_asset_processor=True):
  151. LY_PROCESSES = [
  152. 'Editor', 'Profiler', 'RemoteConsole', 'AutomatedTesting.ServerLauncher', 'o3de'
  153. ]
  154. AP_PROCESSES = [
  155. 'AssetProcessor', 'AssetProcessorBatch', 'AssetBuilder', 'CrySCompileServer',
  156. 'rc' # Resource Compiler
  157. ]
  158. if include_asset_processor:
  159. process_utils.kill_processes_named(LY_PROCESSES+AP_PROCESSES, ignore_extensions=True)
  160. else:
  161. process_utils.kill_processes_named(LY_PROCESSES, ignore_extensions=True)
  162. @staticmethod
  163. def _get_testcase_module_filepath(testcase_module):
  164. # type: (Module) -> str
  165. """
  166. return the full path of the test module
  167. :param testcase_module: The testcase python module being tested
  168. :return str: The full path to the testcase module
  169. """
  170. return os.path.splitext(testcase_module.__file__)[0] + ".py"