launch_android_test.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. #
  8. import argparse
  9. import logging
  10. import os
  11. import pathlib
  12. import queue
  13. import re
  14. import sys
  15. import threading
  16. import time
  17. # Resolve the common python module
  18. ROOT_DEV_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..'))
  19. if ROOT_DEV_PATH not in sys.path:
  20. sys.path.append(ROOT_DEV_PATH)
  21. from cmake.Tools import common
  22. from cmake.Tools.Platform.Android import android_support
  23. # The name of the unit test target
  24. TEST_PROJECT = 'AzTestRunner'
  25. TEST_ACTIVITY = 'AzTestRunnerActivity'
  26. # Prepare a regex that will strip out the timestamp and PID information from adb's logcat to just show 'LMBR' tagged logs
  27. REGEX_LOGCAT_LINE = re.compile(r'([\d-]+)\s+([\d:\.]+)\s+(\d+)\s+(\d+)\s+(I)\s+(LMBR)([\s]+)(:\s)(.*)')
  28. # The startup delay will allow the test runner to pause so that the we have a chance to query for the PID of the test launcher
  29. TEST_RUNNER_STARTUP_DELAY = 1
  30. LOGCAT_BUFFER_SIZE_MB = 32
  31. LOGCAT_READ_QUEUE = queue.Queue()
  32. def validate_android_test_build_dir(build_dir, configuration):
  33. """
  34. Validate an android test build folder
  35. :param build_dir: The build directory where the android test project was generated.
  36. :param configuration: The configuration of the test build
  37. :return: tuple of (Path of build_dir, path of the build dir native path (where the native binaries are built for the configuration), and the android AdbTool wrapper
  38. """
  39. build_path = pathlib.Path(build_dir) if os.path.isabs(build_dir) else pathlib.Path(ROOT_DEV_PATH) / build_dir
  40. if not build_path.is_dir():
  41. raise common.LmbrCmdError(f"Invalid android build directory '{str(build_path)}'")
  42. # Get the platform settings to validate the test game name (must match TEST_PROJECT)
  43. platform_settings = common.PlatformSettings(build_path)
  44. if not platform_settings.projects:
  45. raise common.LmbrCmdError("Missing required platform settings object from build directory.")
  46. is_unit_test_str = getattr(platform_settings, 'is_unit_test', 'False')
  47. is_unit_test = is_unit_test_str.lower() in ('t', 'true', '1')
  48. if not is_unit_test:
  49. raise common.LmbrCmdError("Invalid android build folder for tests.")
  50. # Construct and validate the path to the native binaries that are built for the APK based on the input confiugration
  51. build_configuration_path = build_path / 'app' / 'cmake' / configuration / 'arm64-v8a'
  52. if not build_configuration_path.is_dir():
  53. raise common.LmbrCmdError(f"Invalid android build configuration '{configuration}': Make sure that the APK has been built with this configuration successfully")
  54. # Validate the android SDK path that was registered in the platform settings
  55. android_sdk_path = getattr(platform_settings, 'android_sdk_path', None)
  56. if not android_sdk_path:
  57. raise common.LmbrCmdError(f"Android SDK Path {android_sdk_path} is missing in the platform settings for {build_dir}.")
  58. if not os.path.isdir(android_sdk_path):
  59. raise common.LmbrCmdError(f"Android SDK Path {android_sdk_path} in the platform settings for {build_dir} is not valid.")
  60. return build_path, build_configuration_path, android_sdk_path
  61. def launch_test_on_device(adb_tool, test_module, timeout_secs, test_filter):
  62. """
  63. Launch an test module on the connect android device
  64. :param adb_tool: The ADB Tool to exec the adb commands necessary to run a test
  65. :param test_module: The name of the test module to run
  66. :param timeout_secs: Timeout for a test run
  67. :return: True if the tests passed, False if not
  68. """
  69. # Clear user data before each test
  70. adb_tool.exec(['shell', 'pm', 'clear', 'org.o3de.tests'])
  71. # Increase the log buffer to prevent 'end of file' error from logcat
  72. adb_tool.exec(['shell', 'logcat', '-G', f'{LOGCAT_BUFFER_SIZE_MB}M'])
  73. # Start the test activity
  74. exec_args = ['shell', 'am', 'start',
  75. '-n', f'org.o3de.tests/.{TEST_ACTIVITY}',
  76. '--es', test_module, 'AzRunUnitTests',
  77. '--es', 'startdelay', str(TEST_RUNNER_STARTUP_DELAY)]
  78. if test_filter:
  79. exec_args.extend([
  80. '--es', 'gtest_filter', test_filter
  81. ])
  82. ret, result_output, result_error = adb_tool.exec(exec_args, capture_stdout=True)
  83. if ret != 0:
  84. raise common.LmbrCmdError(f"Unable to launch test runner activity: {result_error or result_output}")
  85. tests_passed = False
  86. result_pid = None
  87. try:
  88. # Make multiple attempts to get the PID of the test process
  89. max_pid_retries = 5
  90. while max_pid_retries > 0:
  91. ret, result_pid, _ = adb_tool.exec(['shell', 'pidof', '-s', 'org.o3de.tests'], capture_stdout=True)
  92. if ret == 0:
  93. break
  94. time.sleep(1)
  95. max_pid_retries -= 1
  96. if not result_pid:
  97. raise common.LmbrCmdError("Unable to get process id for the Test Runner launcher")
  98. result_pid = result_pid.strip()
  99. # Start the adb logcat process for the result PID and filter the stdout
  100. logcat_proc = adb_tool.popen(['shell', 'logcat', f'--pid={result_pid}', '-s', 'LMBR'])
  101. start_time = time.time()
  102. while logcat_proc.poll() is None:
  103. # Break out of the loop if we triggered the test timeout condition (timeout_secs)
  104. elapsed_time = time.time() - start_time
  105. if elapsed_time > timeout_secs > 0:
  106. logging.error("Test Runner Timeout")
  107. break
  108. # Break out of the loop in case the process dies unexpectly
  109. ret, _, _ = adb_tool.exec(['shell', 'pidof', '-s', 'org.o3de.tests'], capture_stdout=True)
  110. if ret != 0:
  111. break
  112. # Use a regex to strip out timestamp/PID logcat line and filter only the 'LMBR' tagged log events
  113. line = logcat_proc.stdout.readline()
  114. result = REGEX_LOGCAT_LINE.match(line) if line else None
  115. if result:
  116. lmbr_log_line = result.group(9)
  117. print(lmbr_log_line)
  118. if '[FAILURE]' in lmbr_log_line:
  119. break
  120. if '[SUCCESS]' in lmbr_log_line:
  121. tests_passed = True
  122. break
  123. finally:
  124. if result_pid:
  125. adb_tool.exec(['shell', 'logcat', f'--pid={result_pid}', '-c'])
  126. # Kill the test launcher process
  127. adb_tool.exec(['shell', 'am', 'force-stop', 'org.o3de.tests'])
  128. time.sleep(2)
  129. return tests_passed
  130. def launch_android_test(build_dir, configuration, target_dev_serial, test_target, timeout_secs, test_filter):
  131. """
  132. Launch the unit test android apk with specific test target(s)
  133. :param build_dir: The cmake build directory to base the launch values on
  134. :param configuration: The configuration to base the launch values on
  135. :param target_dev_serial: The target device serial number to launch the test on. If none, launch on all connected devices
  136. :param test_target: The name of the target module to invoke the test on. If 'all' is specified, then iterate through all of the test modules and launch them individually
  137. :param timeout_secs: Timeout value for individual test runs
  138. :return: True if the test run(s) were successful, false if not
  139. """
  140. # Validate the build dir and configuration
  141. build_path, build_configuration_path, android_sdk_path = validate_android_test_build_dir(build_dir=build_dir,
  142. configuration=configuration)
  143. test_targets = common.get_validated_test_modules(test_modules=test_target, build_dir_path=build_configuration_path)
  144. # Track the long text length for formatting/alignment for the final report
  145. max_module_text_len = max([len(module) for module in test_targets])
  146. module_column_width = max_module_text_len + 8
  147. adb_tool = android_support.AdbTool(android_sdk_path)
  148. adb_tool.connect(target_dev_serial)
  149. start_time = time.time()
  150. final_report_map = {}
  151. test_run_complete_event = threading.Event()
  152. successful_run = True
  153. for test_target in test_targets:
  154. logging.info(f"Launching test for module {test_target}")
  155. result = launch_test_on_device(adb_tool, test_target, timeout_secs, test_filter)
  156. if result:
  157. logging.info(f"Tests for module {test_target} Passed")
  158. final_report_map[test_target] = 'PASSED'
  159. else:
  160. logging.info(f"Tests for module {test_target} Failed")
  161. final_report_map[test_target] = 'FAILED'
  162. successful_run = False
  163. time.sleep(1)
  164. end_time = time.time()
  165. elapsed_time = end_time - start_time
  166. hours = elapsed_time // 3600
  167. elapsed_time = elapsed_time - 3600 * hours
  168. minutes = elapsed_time // 60
  169. seconds = elapsed_time - 60 * minutes
  170. logging.info(f"Total Time : {int(hours)}h {int(minutes)}m {int(seconds)}s")
  171. logging.info(f"Test Modules: {len(test_targets)}")
  172. logging.info('----------------------------------------------------')
  173. for test_module, test_result in final_report_map.items():
  174. module_text_len = len(test_module)
  175. logging.info(f"{test_module}{' '* (module_column_width-module_text_len)} : {test_result}")
  176. test_run_complete_event.set()
  177. adb_tool.disconnect()
  178. return successful_run
  179. def main(args):
  180. parser = argparse.ArgumentParser(description="Launch a test module on a target dev kit.")
  181. parser.add_argument('-b', '--build-dir',
  182. help=f'The relative build directory to deploy from.',
  183. required=True)
  184. parser.add_argument('-c', '--configuration',
  185. help='The build configuration from the build directory for the source deployment files',
  186. default='profile')
  187. parser.add_argument('test_module',
  188. nargs='*',
  189. help="The test module(s) to launch on the target device. Defaults to all registered test modules",
  190. default=[])
  191. parser.add_argument('--device-serial',
  192. help='The optional device serial to target the launch on. Defaults to the all devices connected.',
  193. default=None)
  194. parser.add_argument('--timeout',
  195. help='The timeout in secs for each test module to prevent deadlocked tests',
  196. type=int,
  197. default=-1)
  198. parser.add_argument('--test-filter',
  199. help="Option gtest filter to pass along to the unit test launcher",
  200. default=None)
  201. parser.add_argument('--debug',
  202. help='Enable debug logging',
  203. action='store_true')
  204. parsed_args = parser.parse_args(args)
  205. logging.basicConfig(format='%(levelname)s: %(message)s',
  206. level=logging.DEBUG if parsed_args.debug else logging.INFO)
  207. result = launch_android_test(build_dir=parsed_args.build_dir,
  208. configuration=parsed_args.configuration,
  209. target_dev_serial=parsed_args.device_serial,
  210. test_target=parsed_args.test_module,
  211. timeout_secs=int(parsed_args.timeout),
  212. test_filter=parsed_args.test_filter)
  213. return 0 if result else 1
  214. if __name__ == '__main__':
  215. try:
  216. result_code = main(sys.argv[1:])
  217. exit(result_code)
  218. except common.LmbrCmdError as err:
  219. logging.error(str(err))
  220. exit(err.code)