You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
12 KiB
Python
294 lines
12 KiB
Python
#
|
|
# 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 argparse
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
import queue
|
|
import re
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
# Resolve the common python module
|
|
ROOT_DEV_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..'))
|
|
if ROOT_DEV_PATH not in sys.path:
|
|
sys.path.append(ROOT_DEV_PATH)
|
|
|
|
from cmake.Tools import common
|
|
from cmake.Tools.Platform.Android import android_support
|
|
|
|
|
|
# The name of the unit test target
|
|
TEST_PROJECT = 'AzTestRunner'
|
|
|
|
TEST_ACTIVITY = 'AzTestRunnerActivity'
|
|
|
|
# Prepare a regex that will strip out the timestamp and PID information from adb's logcat to just show 'LMBR' tagged logs
|
|
REGEX_LOGCAT_LINE = re.compile(r'([\d-]+)\s+([\d:\.]+)\s+(\d+)\s+(\d+)\s+(I)\s+(LMBR)([\s]+)(:\s)(.*)')
|
|
|
|
# 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
|
|
TEST_RUNNER_STARTUP_DELAY = 1
|
|
|
|
LOGCAT_BUFFER_SIZE_MB = 32
|
|
|
|
LOGCAT_READ_QUEUE = queue.Queue()
|
|
|
|
|
|
def validate_android_test_build_dir(build_dir, configuration):
|
|
"""
|
|
Validate an android test build folder
|
|
|
|
:param build_dir: The build directory where the android test project was generated.
|
|
:param configuration: The configuration of the test build
|
|
: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
|
|
"""
|
|
build_path = pathlib.Path(build_dir) if os.path.isabs(build_dir) else pathlib.Path(ROOT_DEV_PATH) / build_dir
|
|
if not build_path.is_dir():
|
|
raise common.LmbrCmdError(f"Invalid android build directory '{str(build_path)}'")
|
|
|
|
# Get the platform settings to validate the test game name (must match TEST_PROJECT)
|
|
platform_settings = common.PlatformSettings(build_path)
|
|
if not platform_settings.projects:
|
|
raise common.LmbrCmdError("Missing required platform settings object from build directory.")
|
|
is_unit_test_str = getattr(platform_settings, 'is_unit_test', 'False')
|
|
is_unit_test = is_unit_test_str.lower() in ('t', 'true', '1')
|
|
if not is_unit_test:
|
|
raise common.LmbrCmdError("Invalid android build folder for tests.")
|
|
|
|
# Construct and validate the path to the native binaries that are built for the APK based on the input confiugration
|
|
build_configuration_path = build_path / 'app' / 'cmake' / configuration / 'arm64-v8a'
|
|
|
|
if not build_configuration_path.is_dir():
|
|
raise common.LmbrCmdError(f"Invalid android build configuration '{configuration}': Make sure that the APK has been built with this configuration successfully")
|
|
|
|
# Validate the android SDK path that was registered in the platform settings
|
|
android_sdk_path = getattr(platform_settings, 'android_sdk_path', None)
|
|
if not android_sdk_path:
|
|
raise common.LmbrCmdError(f"Android SDK Path {android_sdk_path} is missing in the platform settings for {build_dir}.")
|
|
if not os.path.isdir(android_sdk_path):
|
|
raise common.LmbrCmdError(f"Android SDK Path {android_sdk_path} in the platform settings for {build_dir} is not valid.")
|
|
|
|
return build_path, build_configuration_path, android_sdk_path
|
|
|
|
|
|
def launch_test_on_device(adb_tool, test_module, timeout_secs, test_filter):
|
|
"""
|
|
Launch an test module on the connect android device
|
|
|
|
:param adb_tool: The ADB Tool to exec the adb commands necessary to run a test
|
|
:param test_module: The name of the test module to run
|
|
:param timeout_secs: Timeout for a test run
|
|
:return: True if the tests passed, False if not
|
|
"""
|
|
|
|
# Clear user data before each test
|
|
adb_tool.exec(['shell', 'pm', 'clear', 'com.lumberyard.tests'])
|
|
|
|
# Increase the log buffer to prevent 'end of file' error from logcat
|
|
adb_tool.exec(['shell', 'logcat', '-G', f'{LOGCAT_BUFFER_SIZE_MB}M'])
|
|
|
|
# Start the test activity
|
|
exec_args = ['shell', 'am', 'start',
|
|
'-n', f'com.lumberyard.tests/.{TEST_ACTIVITY}',
|
|
'--es', test_module, 'AzRunUnitTests',
|
|
'--es', 'startdelay', str(TEST_RUNNER_STARTUP_DELAY)]
|
|
if test_filter:
|
|
exec_args.extend([
|
|
'--es', 'gtest_filter', test_filter
|
|
])
|
|
|
|
ret, result_output, result_error = adb_tool.exec(exec_args, capture_stdout=True)
|
|
|
|
if ret != 0:
|
|
raise common.LmbrCmdError(f"Unable to launch test runner activity: {result_error or result_output}")
|
|
|
|
tests_passed = False
|
|
result_pid = None
|
|
|
|
try:
|
|
|
|
# Make multiple attempts to get the PID of the test process
|
|
max_pid_retries = 5
|
|
while max_pid_retries > 0:
|
|
ret, result_pid, _ = adb_tool.exec(['shell', 'pidof', '-s', 'com.lumberyard.tests'], capture_stdout=True)
|
|
if ret == 0:
|
|
break
|
|
time.sleep(1)
|
|
max_pid_retries -= 1
|
|
if not result_pid:
|
|
raise common.LmbrCmdError("Unable to get process id for the Test Runner launcher")
|
|
result_pid = result_pid.strip()
|
|
|
|
# Start the adb logcat process for the result PID and filter the stdout
|
|
logcat_proc = adb_tool.popen(['shell', 'logcat', f'--pid={result_pid}', '-s', 'LMBR'])
|
|
start_time = time.time()
|
|
|
|
while logcat_proc.poll() is None:
|
|
# Break out of the loop if we triggered the test timeout condition (timeout_secs)
|
|
elapsed_time = time.time() - start_time
|
|
|
|
if elapsed_time > timeout_secs > 0:
|
|
logging.error("Test Runner Timeout")
|
|
break
|
|
|
|
# Break out of the loop in case the process dies unexpectly
|
|
ret, _, _ = adb_tool.exec(['shell', 'pidof', '-s', 'com.lumberyard.tests'], capture_stdout=True)
|
|
if ret != 0:
|
|
break
|
|
|
|
# Use a regex to strip out timestamp/PID logcat line and filter only the 'LMBR' tagged log events
|
|
line = logcat_proc.stdout.readline()
|
|
|
|
result = REGEX_LOGCAT_LINE.match(line) if line else None
|
|
|
|
if result:
|
|
lmbr_log_line = result.group(9)
|
|
print(lmbr_log_line)
|
|
if '[FAILURE]' in lmbr_log_line:
|
|
break
|
|
if '[SUCCESS]' in lmbr_log_line:
|
|
tests_passed = True
|
|
break
|
|
|
|
finally:
|
|
if result_pid:
|
|
adb_tool.exec(['shell', 'logcat', f'--pid={result_pid}', '-c'])
|
|
# Kill the test launcher process
|
|
adb_tool.exec(['shell', 'am', 'force-stop', 'com.lumberyard.tests'])
|
|
time.sleep(2)
|
|
|
|
return tests_passed
|
|
|
|
|
|
def launch_android_test(build_dir, configuration, target_dev_serial, test_target, timeout_secs, test_filter):
|
|
"""
|
|
Launch the unit test android apk with specific test target(s)
|
|
|
|
:param build_dir: The cmake build directory to base the launch values on
|
|
:param configuration: The configuration to base the launch values on
|
|
:param target_dev_serial: The target device serial number to launch the test on. If none, launch on all connected devices
|
|
: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
|
|
:param timeout_secs: Timeout value for individual test runs
|
|
|
|
:return: True if the test run(s) were successful, false if not
|
|
"""
|
|
|
|
# Validate the build dir and configuration
|
|
build_path, build_configuration_path, android_sdk_path = validate_android_test_build_dir(build_dir=build_dir,
|
|
configuration=configuration)
|
|
|
|
test_targets = common.get_validated_test_modules(test_modules=test_target, build_dir_path=build_configuration_path)
|
|
|
|
# Track the long text length for formatting/alignment for the final report
|
|
max_module_text_len = max([len(module) for module in test_targets])
|
|
module_column_width = max_module_text_len + 8
|
|
|
|
adb_tool = android_support.AdbTool(android_sdk_path)
|
|
adb_tool.connect(target_dev_serial)
|
|
|
|
start_time = time.time()
|
|
final_report_map = {}
|
|
test_run_complete_event = threading.Event()
|
|
successful_run = True
|
|
for test_target in test_targets:
|
|
|
|
logging.info(f"Launching test for module {test_target}")
|
|
|
|
result = launch_test_on_device(adb_tool, test_target, timeout_secs, test_filter)
|
|
if result:
|
|
logging.info(f"Tests for module {test_target} Passed")
|
|
final_report_map[test_target] = 'PASSED'
|
|
else:
|
|
logging.info(f"Tests for module {test_target} Failed")
|
|
final_report_map[test_target] = 'FAILED'
|
|
successful_run = False
|
|
|
|
time.sleep(1)
|
|
|
|
end_time = time.time()
|
|
elapsed_time = end_time - start_time
|
|
|
|
hours = elapsed_time // 3600
|
|
elapsed_time = elapsed_time - 3600 * hours
|
|
minutes = elapsed_time // 60
|
|
seconds = elapsed_time - 60 * minutes
|
|
|
|
logging.info(f"Total Time : {int(hours)}h {int(minutes)}m {int(seconds)}s")
|
|
logging.info(f"Test Modules: {len(test_targets)}")
|
|
logging.info('----------------------------------------------------')
|
|
for test_module, test_result in final_report_map.items():
|
|
module_text_len = len(test_module)
|
|
logging.info(f"{test_module}{' '* (module_column_width-module_text_len)} : {test_result}")
|
|
|
|
test_run_complete_event.set()
|
|
|
|
adb_tool.disconnect()
|
|
|
|
return successful_run
|
|
|
|
|
|
def main(args):
|
|
|
|
parser = argparse.ArgumentParser(description="Launch a test module on a target dev kit.")
|
|
|
|
parser.add_argument('-b', '--build-dir',
|
|
help=f'The relative build directory to deploy from.',
|
|
required=True)
|
|
|
|
parser.add_argument('-c', '--configuration',
|
|
help='The build configuration from the build directory for the source deployment files',
|
|
default='profile')
|
|
|
|
parser.add_argument('test_module',
|
|
nargs='*',
|
|
help="The test module(s) to launch on the target device. Defaults to all registered test modules",
|
|
default=[])
|
|
|
|
parser.add_argument('--device-serial',
|
|
help='The optional device serial to target the launch on. Defaults to the all devices connected.',
|
|
default=None)
|
|
|
|
parser.add_argument('--timeout',
|
|
help='The timeout in secs for each test module to prevent deadlocked tests',
|
|
type=int,
|
|
default=-1)
|
|
|
|
parser.add_argument('--test-filter',
|
|
help="Option gtest filter to pass along to the unit test launcher",
|
|
default=None)
|
|
|
|
parser.add_argument('--debug',
|
|
help='Enable debug logging',
|
|
action='store_true')
|
|
|
|
parsed_args = parser.parse_args(args)
|
|
|
|
logging.basicConfig(format='%(levelname)s: %(message)s',
|
|
level=logging.DEBUG if parsed_args.debug else logging.INFO)
|
|
|
|
result = launch_android_test(build_dir=parsed_args.build_dir,
|
|
configuration=parsed_args.configuration,
|
|
target_dev_serial=parsed_args.device_serial,
|
|
test_target=parsed_args.test_module,
|
|
timeout_secs=int(parsed_args.timeout),
|
|
test_filter=parsed_args.test_filter)
|
|
return 0 if result else 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
try:
|
|
result_code = main(sys.argv[1:])
|
|
exit(result_code)
|
|
|
|
except common.LmbrCmdError as err:
|
|
logging.error(str(err))
|
|
exit(err.code)
|