base.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. """
  2. All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
  3. its licensors.
  4. For complete copyright and license terms please see the LICENSE at the root of this
  5. distribution (the "License"). All use of this software is governed by the License,
  6. or, if provided, by the license below or the license accompanying this file. Do not
  7. remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. """
  10. import os
  11. import logging
  12. import subprocess
  13. import pytest
  14. import time
  15. import ly_test_tools.environment.file_system as file_system
  16. import ly_test_tools.environment.process_utils as process_utils
  17. import ly_test_tools.environment.waiter as waiter
  18. from ly_test_tools.lumberyard.asset_processor import AssetProcessor
  19. from ly_test_tools.launchers.exceptions import WaitTimeoutError
  20. from ly_test_tools.log.log_monitor import LogMonitor, LogMonitorException
  21. class TestRunError():
  22. def __init__(self, title, content):
  23. self.title = title
  24. self.content = content
  25. class TestAutomationBase:
  26. MAX_TIMEOUT = 180 # 3 minutes max for a test to run
  27. WAIT_FOR_CRASH_LOG = 20 # Seconds for waiting for a crash log
  28. TEST_FAIL_RETCODE = 0xF # Return code for test failure
  29. test_times = {}
  30. asset_processor = None
  31. def setup_class(cls):
  32. cls.test_times = {}
  33. cls.editor_times = {}
  34. cls.asset_processor = None
  35. def teardown_class(cls):
  36. logger = logging.getLogger(__name__)
  37. # Report times
  38. time_info_str = "Individual test times (Full test time, Editor test time):\n"
  39. for testcase_name, t in cls.test_times.items():
  40. editor_t = cls.editor_times[testcase_name]
  41. time_info_str += f"{testcase_name}: (Full:{t} sec, Editor:{editor_t} sec)\n"
  42. logger.info(time_info_str)
  43. # Kill all ly processes
  44. cls.asset_processor.teardown()
  45. cls._kill_ly_processes()
  46. def _run_test(self, request, workspace, editor, testcase_module, extra_cmdline_args=[]):
  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.__class__.asset_processor = AssetProcessor(workspace)
  55. self.asset_processor.backup_ap_settings()
  56. self._kill_ly_processes(include_asset_processor=False)
  57. self.asset_processor.start()
  58. self.asset_processor.wait_for_idle()
  59. def teardown():
  60. if os.path.exists(workspace.paths.editor_log()):
  61. workspace.artifact_manager.save_artifact(workspace.paths.editor_log())
  62. try:
  63. file_system.restore_backup(workspace.paths.editor_log(), workspace.paths.project_log())
  64. except FileNotFoundError as e:
  65. self.logger.debug(f"File restoration failed, editor log could not be found.\nError: {e}")
  66. editor.kill()
  67. request.addfinalizer(teardown)
  68. if os.path.exists(workspace.paths.editor_log()):
  69. self.logger.debug("Creating backup for existing editor log before test run.")
  70. file_system.create_backup(workspace.paths.editor_log(), workspace.paths.project_log())
  71. ############
  72. # Run test #
  73. editor_starttime = time.time()
  74. self.logger.debug("Running automated test")
  75. testcase_module_filepath = self._get_testcase_module_filepath(testcase_module)
  76. pycmd = ["--runpythontest", testcase_module_filepath, "-BatchMode", "-autotest_mode", "-NullRenderer"] + extra_cmdline_args
  77. editor.args.extend(pycmd) # args are added to the WinLauncher start command
  78. editor.start(backupFiles = False, launch_ap = False)
  79. try:
  80. editor.wait(TestAutomationBase.MAX_TIMEOUT)
  81. except WaitTimeoutError:
  82. errors.append(TestRunError("TIMEOUT", "Editor did not close after {TestAutomationBase.MAX_TIMEOUT} seconds, verify the test is ending and the application didn't freeze"))
  83. editor.kill()
  84. output = editor.get_output()
  85. self.logger.debug("Test output:\n" + output)
  86. return_code = editor.get_returncode()
  87. self.editor_times[testcase_name] = time.time() - editor_starttime
  88. ###################
  89. # Validate result #
  90. if return_code != 0:
  91. if output:
  92. error_str = "Test failed, output:\n" + output.replace("\n", "\n ")
  93. else:
  94. error_str = "Test failed, no output available..\n"
  95. errors.append(TestRunError("FAILED TEST", error_str))
  96. if return_code != TestAutomationBase.TEST_FAIL_RETCODE: # Crashed
  97. crash_info = "-- No crash log available --"
  98. error_log = os.path.join(workspace.paths.project_log(), 'error.log')
  99. try:
  100. waiter.wait_for(lambda: os.path.exists(error_log), timeout=TestAutomationBase.WAIT_FOR_CRASH_LOG)
  101. except AssertionError:
  102. pass
  103. try:
  104. with open(error_log) as f:
  105. crash_info = f.read()
  106. except Exception as ex:
  107. crash_info += f"\n{str(ex)}"
  108. return_code_str = f"0x{return_code:0X}" if isinstance(return_code, int) else "None"
  109. error_str = f"Editor.exe crashed, return code: {return_code_str}\n\nCrash log:\n{crash_info}"
  110. errors.append(TestRunError("CRASH", error_str))
  111. self.test_times[testcase_name] = time.time() - test_starttime
  112. ###################
  113. # Error reporting #
  114. if errors:
  115. error_str = "Error list:\n"
  116. longest_title = max([len(e.title) for e in errors])
  117. longest_title += (longest_title % 2) # make it even spaces
  118. longest_title = max(30, longest_title) # at least 30 -
  119. header_decoration = "-".center(longest_title, "-") + "\n"
  120. for e in errors:
  121. error_str += header_decoration
  122. error_str += f" {e.title} ".center(longest_title, "-") + "\n"
  123. error_str += header_decoration
  124. for line in e.content.split("\n"):
  125. error_str += f" {line}\n"
  126. error_str += header_decoration
  127. error_str += "Editor log:\n"
  128. try:
  129. with open(workspace.paths.editor_log()) as f:
  130. log_basename = os.path.basename(workspace.paths.editor_log())
  131. for line in f.readlines():
  132. error_str += f"|{log_basename}| {line}"
  133. except Exception as ex:
  134. error_str += "-- No log available --"
  135. pytest.fail(error_str)
  136. @staticmethod
  137. def _kill_ly_processes(include_asset_processor=True):
  138. LY_PROCESSES = [
  139. 'Editor', 'Profiler', 'RemoteConsole',
  140. ]
  141. AP_PROCESSES = [
  142. 'AssetProcessor', 'AssetProcessorBatch', 'AssetBuilder', 'CrySCompileServer',
  143. 'rc' # Resource Compiler
  144. ]
  145. if include_asset_processor:
  146. process_utils.kill_processes_named(LY_PROCESSES+AP_PROCESSES, ignore_extensions=True)
  147. else:
  148. process_utils.kill_processes_named(LY_PROCESSES, ignore_extensions=True)
  149. @staticmethod
  150. def _get_testcase_module_filepath(testcase_module):
  151. # type: (Module) -> str
  152. """
  153. return the full path of the test module
  154. :param testcase_module: The testcase python module being tested
  155. :return str: The full path to the testcase module
  156. """
  157. return os.path.splitext(testcase_module.__file__)[0] + ".py"