# Copyright 2021 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """This is a library for wrapping Rust test executables in a way that is compatible with the requirements of the `main_program` module. """ import argparse import os import re import subprocess import sys import exe_util import main_program import test_results def _format_test_name(test_executable_name, test_case_name): assert '//' not in test_executable_name assert '/' not in test_case_name test_case_name = '/'.join(test_case_name.split('::')) return '{}//{}'.format(test_executable_name, test_case_name) def _parse_test_name(test_name): assert '//' in test_name assert '::' not in test_name test_executable_name, test_case_name = test_name.split('//', 1) test_case_name = '::'.join(test_case_name.split('/')) return test_executable_name, test_case_name def _scrape_test_list(output, test_executable_name): """Scrapes stdout from running a Rust test executable with --list and --format=terse. Args: output: A string with the full stdout of a Rust test executable. test_executable_name: A string. Used as a prefix in "full" test names in the returned results. Returns: A list of strings - a list of all test names. """ TEST_SUFFIX = ': test' BENCHMARK_SUFFIX = ': benchmark' test_case_names = [] for line in output.splitlines(): if line.endswith(TEST_SUFFIX): test_case_names.append(line[:-len(TEST_SUFFIX)]) elif line.endswith(BENCHMARK_SUFFIX): continue else: raise ValueError( 'Unexpected format of a list of tests: {}'.format(output)) test_names = [ _format_test_name(test_executable_name, test_case_name) for test_case_name in test_case_names ] return test_names def _scrape_test_results(output, test_executable_name, list_of_expected_test_case_names): """Scrapes stdout from running a Rust test executable with --test --format=pretty. Args: output: A string with the full stdout of a Rust test executable. test_executable_name: A string. Used as a prefix in "full" test names in the returned TestResult objects. list_of_expected_test_case_names: A list of strings - expected test case names (from the perspective of a single executable / with no prefix). Returns: A list of test_results.TestResult objects. """ results = [] regex = re.compile(r'^test ([:\w]+) \.\.\. (\w+)') for line in output.splitlines(): match = regex.match(line.strip()) if not match: continue test_case_name = match.group(1) if test_case_name not in list_of_expected_test_case_names: continue actual_test_result = match.group(2) if actual_test_result == 'ok': actual_test_result = 'PASS' elif actual_test_result == 'FAILED': actual_test_result = 'FAIL' elif actual_test_result == 'ignored': actual_test_result = 'SKIP' test_name = _format_test_name(test_executable_name, test_case_name) results.append(test_results.TestResult(test_name, actual_test_result)) return results def _get_exe_specific_tests(expected_test_executable_name, list_of_test_names): results = [] for test_name in list_of_test_names: actual_test_executable_name, test_case_name = _parse_test_name( test_name) if actual_test_executable_name != expected_test_executable_name: continue results.append(test_case_name) return results class _TestExecutableWrapper: def __init__(self, path_to_test_executable): if not os.path.isfile(path_to_test_executable): raise ValueError('No such file: ' + path_to_test_executable) self._path_to_test_executable = path_to_test_executable self._name_of_test_executable, _ = os.path.splitext( os.path.basename(path_to_test_executable)) def list_all_tests(self): """Returns: A list of strings - a list of all test names. """ args = [self._path_to_test_executable, '--list', '--format=terse'] output = subprocess.check_output(args, text=True) return _scrape_test_list(output, self._name_of_test_executable) def run_tests(self, list_of_tests_to_run): """Runs tests listed in `list_of_tests_to_run`. Ignores tests for other test executables. Args: list_of_tests_to_run: A list of strings (a list of test names). Returns: A list of test_results.TestResult objects. """ list_of_tests_to_run = _get_exe_specific_tests( self._name_of_test_executable, list_of_tests_to_run) if not list_of_tests_to_run: return [] # TODO(lukasza): Avoid passing all test names on the cmdline (might # require adding support to Rust test executables for reading cmdline # args from a file). # TODO(lukasza): Avoid scraping human-readable output (try using # JSON output once it stabilizes; hopefully preserving human-readable # output to the terminal). args = [ self._path_to_test_executable, '--test', '--format=pretty', '--color=always', '--exact' ] args.extend(list_of_tests_to_run) print('Running tests from {}...'.format(self._name_of_test_executable)) output = exe_util.run_and_tee_output(args) print('Running tests from {}... DONE.'.format( self._name_of_test_executable)) print() return _scrape_test_results(output, self._name_of_test_executable, list_of_tests_to_run) def _parse_args(args): description = 'Wrapper for running Rust unit tests with support for ' \ 'Chromium test filters, sharding, and test output.' parser = argparse.ArgumentParser(description=description) main_program.add_cmdline_args(parser) parser.add_argument('--rust-test-executable', action='append', dest='rust_test_executables', default=[], help=argparse.SUPPRESS, metavar='FILEPATH', required=True) return parser.parse_args(args=args) if __name__ == '__main__': parsed_args = _parse_args(sys.argv[1:]) rust_tests_wrappers = [ _TestExecutableWrapper(path) for path in parsed_args.rust_test_executables ] main_program.main(rust_tests_wrappers, parsed_args, os.environ)