# Copyright 2024 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Provides a way to run multiple tests as a bundle. It forwards all the arguments to the run_test.py, but overrides the test runner it uses. Since this class runs in a higher level as the regular run_test.py, it would be more clear to not include it into the run_test.py and avoid cycle-dependency. Use of this test runner may break sharding.""" import argparse import logging import os import sys from subprocess import CompletedProcess from typing import List, NamedTuple, Optional from urllib.parse import urlparse import run_test from run_executable_test import ExecutableTestRunner from test_runner import TestRunner class TestCase(NamedTuple): """Defines a TestCase, it executes the package with optional arguments.""" # The test package in the format of fuchsia-pkg://...#meta/...cm. package: str # Optional arguments to pass to the test run. It can include multiple # arguments separated by any whitespaces. args: str = '' class _BundledTestRunner(TestRunner): """A TestRunner implementation to run multiple test cases. It always run all tests even some of them failed. The return code is the return code of the last non-zero test run.""" # private, use run_tests.get_test_runner function instead. # Keep the order of parameters consistent with ExecutableTestRunner. # pylint: disable=too-many-arguments # TODO(crbug.com/346806329): May consider using a structure to capture the # arguments. def __init__(self, out_dir: str, tests: List[TestCase], target_id: Optional[str], code_coverage_dir: Optional[str], logs_dir: Optional[str], package_deps: List[str], test_realm: Optional[str]): super().__init__( out_dir, [], [], target_id, _BundledTestRunner._merge_packages(tests, package_deps)) assert tests self._tests = tests self._code_coverage_dir = code_coverage_dir self._logs_dir = logs_dir self._test_realm = test_realm @staticmethod def _merge_packages(tests: List[TestCase], package_deps: List[str]) -> List[str]: packages = list(package_deps) # Include test packages if they have not been defined in the # package_deps. packages.extend( {urlparse(x.package).path.lstrip('/') + '.far' for x in tests} - {os.path.basename(x) for x in packages}) return packages def run_test(self) -> CompletedProcess: returncode = 0 for test in self._tests: assert test.package.endswith('.cm') test_runner = ExecutableTestRunner( self._out_dir, test.args.split(), test.package, self._target_id, self._code_coverage_dir, self._logs_dir, self._package_deps, self._test_realm) # It's a little bit wasteful to resolve all the packages once per # test package, but it's easier. result = test_runner.run_test().returncode logging.info('Result of test %s is %s', test, result) if result != 0: returncode = result return CompletedProcess(args='', returncode=returncode) def run_tests(tests: List[TestCase]) -> int: """Runs multiple tests. Args: tests: The definition of each test case. Note: All the packages in tests will always be included, and it's expected that the far files sharing the same name as the package in TestCase except for the suffix. E.g. test1.far is the far file of fuchsia-pkg://fuchsia.com/test1#meta/some.cm. Duplicated packages in either --packages or tests are allowed as long as they are targeting the same file; otherwise the test run would trigger an assertion failure. Far files in the --packages can be either absolute paths or relative paths starting from --out-dir.""" # The 'bundled-tests' is a place holder and has no specific meaning; the # run_test._get_test_runner is overridden. sys.argv.append('bundled-tests') def get_test_runner(runner_args: argparse.Namespace, *_) -> TestRunner: # test_args are not used, and each TestCase should have its own args. return _BundledTestRunner(runner_args.out_dir, tests, runner_args.target_id, runner_args.code_coverage_dir, runner_args.logs_dir, runner_args.packages, runner_args.test_realm) # pylint: disable=protected-access run_test._get_test_runner = get_test_runner return run_test.main()