1# Copyright 2024 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4"""Provides a way to run multiple tests as a bundle. It forwards all the 5 arguments to the run_test.py, but overrides the test runner it uses. 6 7 Since this class runs in a higher level as the regular run_test.py, it would 8 be more clear to not include it into the run_test.py and avoid 9 cycle-dependency. 10 11 Use of this test runner may break sharding.""" 12 13import argparse 14import logging 15import os 16import sys 17 18from subprocess import CompletedProcess 19from typing import List, NamedTuple, Optional 20from urllib.parse import urlparse 21 22import run_test 23from run_executable_test import ExecutableTestRunner 24from test_runner import TestRunner 25 26 27class TestCase(NamedTuple): 28 """Defines a TestCase, it executes the package with optional arguments.""" 29 30 # The test package in the format of fuchsia-pkg://...#meta/...cm. 31 package: str 32 33 # Optional arguments to pass to the test run. It can include multiple 34 # arguments separated by any whitespaces. 35 args: str = '' 36 37 38class _BundledTestRunner(TestRunner): 39 """A TestRunner implementation to run multiple test cases. It always run all 40 tests even some of them failed. The return code is the return code of the 41 last non-zero test run.""" 42 43 # private, use run_tests.get_test_runner function instead. 44 # Keep the order of parameters consistent with ExecutableTestRunner. 45 # pylint: disable=too-many-arguments 46 # TODO(crbug.com/346806329): May consider using a structure to capture the 47 # arguments. 48 def __init__(self, out_dir: str, tests: List[TestCase], 49 target_id: Optional[str], code_coverage_dir: Optional[str], 50 logs_dir: Optional[str], package_deps: List[str], 51 test_realm: Optional[str]): 52 super().__init__( 53 out_dir, [], [], target_id, 54 _BundledTestRunner._merge_packages(tests, package_deps)) 55 assert tests 56 self._tests = tests 57 self._code_coverage_dir = code_coverage_dir 58 self._logs_dir = logs_dir 59 self._test_realm = test_realm 60 61 @staticmethod 62 def _merge_packages(tests: List[TestCase], 63 package_deps: List[str]) -> List[str]: 64 packages = list(package_deps) 65 # Include test packages if they have not been defined in the 66 # package_deps. 67 packages.extend( 68 {urlparse(x.package).path.lstrip('/') + '.far' 69 for x in tests} - {os.path.basename(x) 70 for x in packages}) 71 return packages 72 73 def run_test(self) -> CompletedProcess: 74 returncode = 0 75 for test in self._tests: 76 assert test.package.endswith('.cm') 77 test_runner = ExecutableTestRunner( 78 self._out_dir, test.args.split(), test.package, 79 self._target_id, self._code_coverage_dir, self._logs_dir, 80 self._package_deps, self._test_realm) 81 # It's a little bit wasteful to resolve all the packages once per 82 # test package, but it's easier. 83 result = test_runner.run_test().returncode 84 logging.info('Result of test %s is %s', test, result) 85 if result != 0: 86 returncode = result 87 return CompletedProcess(args='', returncode=returncode) 88 89 90def run_tests(tests: List[TestCase]) -> int: 91 """Runs multiple tests. 92 93 Args: 94 tests: The definition of each test case. 95 96 Note: 97 All the packages in tests will always be included, and it's expected 98 that the far files sharing the same name as the package in TestCase 99 except for the suffix. E.g. test1.far is the far file of 100 fuchsia-pkg://fuchsia.com/test1#meta/some.cm. 101 102 Duplicated packages in either --packages or tests are allowed as long 103 as they are targeting the same file; otherwise the test run would 104 trigger an assertion failure. 105 106 Far files in the --packages can be either absolute paths or relative 107 paths starting from --out-dir.""" 108 # The 'bundled-tests' is a place holder and has no specific meaning; the 109 # run_test._get_test_runner is overridden. 110 sys.argv.append('bundled-tests') 111 112 def get_test_runner(runner_args: argparse.Namespace, *_) -> TestRunner: 113 # test_args are not used, and each TestCase should have its own args. 114 return _BundledTestRunner(runner_args.out_dir, tests, 115 runner_args.target_id, 116 runner_args.code_coverage_dir, 117 runner_args.logs_dir, runner_args.packages, 118 runner_args.test_realm) 119 120 # pylint: disable=protected-access 121 run_test._get_test_runner = get_test_runner 122 return run_test.main() 123