# 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
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.
def __init__(self, out_dir: str, target_id: str, package_deps: List[str],
tests: List[TestCase], logs_dir: str):
super().__init__(
out_dir, [], [], target_id,
_BundledTestRunner._merge_packages(tests, package_deps))
assert tests
self._tests = tests
self._logs_dir = logs_dir
@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, None,
self._logs_dir, [], None)
# It's a little bit wasteful to resolve all the packages once per
# test package, but it's easier.
# pylint: disable=protected-access
test_runner._package_deps = self._package_deps
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, runner_args.target_id,
runner_args.packages, tests,
runner_args.logs_dir)
# pylint: disable=protected-access
run_test._get_test_runner = get_test_runner
return run_test.main()