|  | # 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) |