| #!/usr/bin/env python3 |
| # Copyright 2025 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Builds and runs selected unit tests.""" |
| |
| import argparse |
| import json |
| import os |
| import plistlib |
| import re |
| import subprocess |
| import sys |
| from typing import Any, Dict, List, Optional, Tuple |
| |
| import shared_test_utils |
| from shared_test_utils import Colors, Simulator, print_header, print_command |
| |
| |
| |
| def _build_tests(out_dir: str, test_target: str) -> bool: |
| """Builds the unit test target. |
| |
| Args: |
| out_dir: The output directory for the build. |
| test_target: The target to build. |
| |
| Returns: |
| True if the build was successful, False otherwise. |
| """ |
| build_command = ['autoninja', '-C', out_dir, test_target] |
| print_header("--- Building Tests ---") |
| print_command(build_command) |
| try: |
| subprocess.check_call(build_command) |
| return True |
| except subprocess.CalledProcessError as e: |
| print(f"Build failed with exit code {e.returncode}") |
| return False |
| |
| |
| def _run_tests(out_dir: str, simulator_udid: str, |
| gtest_filter: Optional[str], test_target: str, |
| gtest_repeat: Optional[str] = None) -> int: |
| """Installs and runs the tests on the specified simulator. |
| |
| Args: |
| out_dir: The output directory for the build. |
| simulator_udid: The UDID of the simulator to use. |
| gtest_filter: The gtest filter to apply. |
| test_target: The target to run. |
| |
| Returns: |
| The exit code of the test runner (0 for success, non-zero for failure). |
| """ |
| app_path = os.path.join(out_dir, f'{test_target}.app') |
| info_plist_path = os.path.join(app_path, 'Info.plist') |
| with open(info_plist_path, 'rb') as f: |
| info_plist = plistlib.load(f) |
| bundle_id = info_plist['CFBundleIdentifier'] |
| |
| install_command = ['xcrun', 'simctl', 'install', simulator_udid, app_path] |
| print_header("--- Installing App ---") |
| print_command(install_command) |
| subprocess.check_call(install_command) |
| |
| launch_command = [ |
| 'xcrun', 'simctl', 'launch', '--console-pty', simulator_udid, bundle_id |
| ] |
| if gtest_filter: |
| launch_command.append('--gtest_filter=' + gtest_filter) |
| if gtest_repeat: |
| launch_command.append('--gtest_repeat=' + gtest_repeat) |
| print_header("--- Running Tests ---") |
| print_command(launch_command) |
| |
| process = subprocess.Popen(launch_command, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| text=True) |
| |
| test_failed = False |
| test_completed = False |
| while True: |
| line = process.stdout.readline() |
| if not line: |
| break |
| sys.stdout.write(line) |
| if '[ PASSED ]' in line or '[ FAILED ]' in line: |
| test_completed = True |
| if '[ FAILED ]' in line: |
| test_failed = True |
| |
| process.wait() |
| |
| # If the test completed and there were failures, return 1. |
| if test_completed and test_failed: |
| return 1 |
| |
| # If the test did not complete (i.e. crashed), return 1. |
| if not test_completed: |
| print(f'\n{Colors.FAIL}{Colors.BOLD}' |
| 'ERROR: Test runner did not complete. Assuming crash.' |
| f'{Colors.RESET}') |
| return 1 |
| |
| # If the test completed and there were no failures, return 0. |
| return 0 |
| |
| |
| def main() -> int: |
| """Main function for the script. |
| |
| Parses arguments, finds a simulator, builds, and runs the tests. |
| |
| Returns: |
| The exit code of the test run. |
| """ |
| print_header("=== Run Unit Tests ===") |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| '--out-dir', |
| default='out/Debug-iphonesimulator', |
| help='The output directory to use for the build (default: %(default)s).') |
| parser.add_argument('--gtest_filter', |
| help='The gtest_filter to use for running the tests.') |
| parser.add_argument('--gtest_repeat', |
| help='The gtest_repeat to use for running the tests.') |
| parser.add_argument('--device', help='The device type to use for the test.') |
| parser.add_argument('--os', |
| help='The OS version to use for the test (e.g., 17.5).') |
| parser.add_argument( |
| '--test-target', |
| default='ios_chrome_unittests', |
| help='The test target to run (default: %(default)s).') |
| args = parser.parse_args() |
| |
| simulator = shared_test_utils.find_and_boot_simulator( |
| args.device, args.os) |
| if not simulator: |
| return 1 |
| |
| if not _build_tests(args.out_dir, args.test_target): |
| return 1 |
| |
| return _run_tests(args.out_dir, simulator.udid, args.gtest_filter, |
| args.test_target, args.gtest_repeat) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |