launch_ios_test.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  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 glob
  10. import json
  11. import logging
  12. import os
  13. import pathlib
  14. import plistlib
  15. import re
  16. import sys
  17. import time
  18. TEST_TARGET_NAME = 'TestLauncherTarget'
  19. TEST_STARTED_STRING = 'TEST STARTED'
  20. TEST_SUCCESS_STRING = 'TEST SUCCEEDED'
  21. TEST_FAILURE_STRING = 'TEST FAILED'
  22. TEST_RUN_SEARCH_PATTERN=re.compile(r'^\[\s*([a-zA-Z0-9]*\s*)\]\s*([a-zA-Z0-9\.\s]*)(\(.*\))')
  23. # Resolve the common python module
  24. ROOT_DEV_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..'))
  25. if ROOT_DEV_PATH not in sys.path:
  26. sys.path.append(ROOT_DEV_PATH)
  27. from cmake.Tools import common
  28. def launch_ios_test(build_dir, target_dev_name, test_target, timeout_secs, test_filter, xctestrun_file, test_report_json_file):
  29. build_path = pathlib.Path(build_dir) if os.path.isabs(build_dir) else pathlib.Path(ROOT_DEV_PATH) / build_dir
  30. if not build_path.is_dir():
  31. raise common.LmbrCmdError(f"Invalid build directory '{str(build_path)}'")
  32. if xctestrun_file:
  33. xctestrun_file_path = build_path / xctestrun_file
  34. if not os.path.exists(xctestrun_file_path):
  35. raise common.LmbrCmdError(f"'{str(xctestrun_file_path)}' not found in '{str(build_path)}'.")
  36. else:
  37. test_run_file = xctestrun_file_path
  38. else:
  39. # By default, xcodebuild will place the xctestrun file at the root of the build directory. There is only one file.
  40. # The xctestrun filename has the format <scheme_name>_iphoneos<sdk_version>-<arch>.xctestrun
  41. # Our scheme name is always "AzTestRunner" and the only iOS architecture we support is arm64.
  42. # The SDK version is the only variable. But, Xcode only allows the installation of the latest SDK(based on Xcode version).
  43. glob_pattern = str(build_path) + '/AzTestRunner_iphoneos*-arm64.xctestrun'
  44. test_run_files = glob.glob(glob_pattern)
  45. if not test_run_files:
  46. raise common.LmbrCmdError(f"No xctestrun file found in '{str(build_path)}'. Run build_ios_test.py first.")
  47. test_run_file = test_run_files[0]
  48. test_targets = common.get_validated_test_modules(test_modules=test_target, build_dir_path=build_path)
  49. test_run_contents = []
  50. test_case_successes = []
  51. test_case_fails = []
  52. crashed_test_modules = []
  53. test_case_count = 0
  54. with open(test_run_file, 'rb') as fp:
  55. test_run_contents = plistlib.load(fp)
  56. xcode_build = common.CommandLineExec('/usr/bin/xcodebuild')
  57. for target in test_targets:
  58. with open(test_run_file, 'wb') as fp:
  59. fp.truncate(0)
  60. command_line_arguments = [target, 'AzRunUnitTests']
  61. if test_filter:
  62. command_line_arguments.extend([f'--gtest_filter={test_filter}'])
  63. test_run_contents[TEST_TARGET_NAME]['CommandLineArguments'] = command_line_arguments
  64. with open(test_run_file, 'wb') as fp:
  65. plistlib.dump(test_run_contents, fp, sort_keys=False)
  66. xcode_args = ['test-without-building', '-xctestrun', test_run_file, '-destination', f'platform=iOS,name={target_dev_name}', '-allowProvisioningUpdates', '-allowProvisioningDeviceRegistration']
  67. if timeout_secs < 0:
  68. xcode_args.extend(['-test-timeouts-enabled', 'NO'])
  69. else:
  70. xcode_args.extend(['-test-timeouts-enabled', 'YES'])
  71. xcode_args.extend(['-maximum-test-execution-time-allowance', f'{timeout_secs}'])
  72. xcode_out = xcode_build.popen(xcode_args, cwd=build_path, shell=False)
  73. # Log XCTest's output to debug.
  74. # Use test start and end markers to separate XCTest output from AzTestRunner's output
  75. test_success = False
  76. test_output = False
  77. while xcode_out.poll() is None:
  78. line = xcode_out.stdout.readline()
  79. if line.startswith('** TEST EXECUTE FAILED **'):
  80. # The test run crashed, so we need to track the failed test module
  81. crashed_test_modules.append(target)
  82. else:
  83. matched = TEST_RUN_SEARCH_PATTERN.search(line)
  84. if matched:
  85. test_case_action = matched.group(1).strip()
  86. test_case_name = matched.group(2).strip()
  87. test_case_elapsed = matched.group(3).strip() if len(matched.groups()) > 2 else ''
  88. if test_case_action == 'OK':
  89. test_case_successes.append(f'{test_case_name} {test_case_elapsed}')
  90. test_case_count += 1
  91. elif test_case_action == 'FAILED' and 'listed below:' not in test_case_name:
  92. test_case_fails.append(f'{test_case_name} {test_case_elapsed}')
  93. test_case_count += 1
  94. if TEST_STARTED_STRING in line:
  95. test_output = True
  96. if TEST_SUCCESS_STRING in line:
  97. test_success = True
  98. test_output = False
  99. elif TEST_FAILURE_STRING in line:
  100. test_output = False
  101. if test_output:
  102. print(line, end='')
  103. else:
  104. logging.debug(line)
  105. logging.info(f'{target} Succeeded') if test_success else print(f'{target} Failed')
  106. if test_report_json_file:
  107. test_report_json_path = pathlib.Path(test_report_json_file)
  108. test_timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.localtime())
  109. result_dict = {
  110. 'index': 'ly_platforms.test',
  111. 'payload': {
  112. 'Git Success': True,
  113. 'Build Success': True,
  114. 'Passed': test_case_successes,
  115. 'Failed': test_case_fails,
  116. 'CrashedModules': crashed_test_modules,
  117. 'Count': test_case_count
  118. },
  119. 'pipeline': 'filebeat',
  120. 'timestamp': test_timestamp
  121. }
  122. result_json = json.dumps(result_dict, indent=4)
  123. test_report_json_path.write_text(result_json, encoding='UTF-8', errors='ignore')
  124. logging.info(f'MARS report saved to {test_report_json_path}')
  125. logging.info(f"MARS report:\n{result_json}")
  126. def main(args):
  127. parser = argparse.ArgumentParser(description="Launch a test module on a target iOS device.")
  128. parser.add_argument('-b', '--build-dir',
  129. help='The relative build directory to deploy from.',
  130. required=True)
  131. parser.add_argument('test_module',
  132. nargs='*',
  133. help='The test module(s) to launch on the target device. Defaults to all registered test modules',
  134. default=[])
  135. parser.add_argument('--device-name',
  136. help='The name of the iOS device on which to launch.',
  137. required=True)
  138. parser.add_argument('--timeout',
  139. help='The timeout in secs for each test module to prevent deadlocked tests',
  140. type=int,
  141. default=-1)
  142. parser.add_argument('--test-filter',
  143. help='Optional gtest filter to pass along to the unit test launcher',
  144. default=None)
  145. parser.add_argument('--xctestrun-file',
  146. help='Optional parameter to specify custom xctestrun file (path relative to build directory)',
  147. default=None)
  148. parser.add_argument('--debug',
  149. help='Enable debug logging',
  150. action='store_true')
  151. parser.add_argument('--test-report-json',
  152. help='The optional path to the test report json file that will be generated for MARS reporting',
  153. default=None)
  154. parsed_args = parser.parse_args(args)
  155. logging.basicConfig(format='%(levelname)s: %(message)s',
  156. level=logging.DEBUG if parsed_args.debug else logging.INFO)
  157. result = launch_ios_test(build_dir=parsed_args.build_dir,
  158. target_dev_name=parsed_args.device_name,
  159. test_target=parsed_args.test_module,
  160. timeout_secs=int(parsed_args.timeout),
  161. test_filter=parsed_args.test_filter,
  162. xctestrun_file=parsed_args.xctestrun_file,
  163. test_report_json_file=parsed_args.test_report_json)
  164. return 0 if result else 1
  165. if __name__ == '__main__':
  166. try:
  167. result_code = main(sys.argv[1:])
  168. exit(result_code)
  169. except common.LmbrCmdError as err:
  170. logging.error(str(err))
  171. exit(err.code)