1# Copyright 2021 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"""This is a library for wrapping Rust test executables in a way that is 5compatible with the requirements of the `main_program` module. 6""" 7 8import argparse 9import os 10import re 11import subprocess 12import sys 13 14import exe_util 15import main_program 16import test_results 17 18 19def _format_test_name(test_executable_name, test_case_name): 20 assert '//' not in test_executable_name 21 assert '/' not in test_case_name 22 test_case_name = '/'.join(test_case_name.split('::')) 23 return '{}//{}'.format(test_executable_name, test_case_name) 24 25 26def _parse_test_name(test_name): 27 assert '//' in test_name 28 assert '::' not in test_name 29 test_executable_name, test_case_name = test_name.split('//', 1) 30 test_case_name = '::'.join(test_case_name.split('/')) 31 return test_executable_name, test_case_name 32 33 34def _scrape_test_list(output, test_executable_name): 35 """Scrapes stdout from running a Rust test executable with 36 --list and --format=terse. 37 38 Args: 39 output: A string with the full stdout of a Rust test executable. 40 test_executable_name: A string. Used as a prefix in "full" test names 41 in the returned results. 42 43 Returns: 44 A list of strings - a list of all test names. 45 """ 46 TEST_SUFFIX = ': test' 47 BENCHMARK_SUFFIX = ': benchmark' 48 test_case_names = [] 49 for line in output.splitlines(): 50 if line.endswith(TEST_SUFFIX): 51 test_case_names.append(line[:-len(TEST_SUFFIX)]) 52 elif line.endswith(BENCHMARK_SUFFIX): 53 continue 54 else: 55 raise ValueError( 56 'Unexpected format of a list of tests: {}'.format(output)) 57 test_names = [ 58 _format_test_name(test_executable_name, test_case_name) 59 for test_case_name in test_case_names 60 ] 61 return test_names 62 63 64def _scrape_test_results(output, test_executable_name, 65 list_of_expected_test_case_names): 66 """Scrapes stdout from running a Rust test executable with 67 --test --format=pretty. 68 69 Args: 70 output: A string with the full stdout of a Rust test executable. 71 test_executable_name: A string. Used as a prefix in "full" test names 72 in the returned TestResult objects. 73 list_of_expected_test_case_names: A list of strings - expected test case 74 names (from the perspective of a single executable / with no prefix). 75 Returns: 76 A list of test_results.TestResult objects. 77 """ 78 results = [] 79 regex = re.compile(r'^test ([:\w]+) \.\.\. (\w+)') 80 for line in output.splitlines(): 81 match = regex.match(line.strip()) 82 if not match: 83 continue 84 85 test_case_name = match.group(1) 86 if test_case_name not in list_of_expected_test_case_names: 87 continue 88 89 actual_test_result = match.group(2) 90 if actual_test_result == 'ok': 91 actual_test_result = 'PASS' 92 elif actual_test_result == 'FAILED': 93 actual_test_result = 'FAIL' 94 elif actual_test_result == 'ignored': 95 actual_test_result = 'SKIP' 96 97 test_name = _format_test_name(test_executable_name, test_case_name) 98 results.append(test_results.TestResult(test_name, actual_test_result)) 99 return results 100 101 102def _get_exe_specific_tests(expected_test_executable_name, list_of_test_names): 103 results = [] 104 for test_name in list_of_test_names: 105 actual_test_executable_name, test_case_name = _parse_test_name( 106 test_name) 107 if actual_test_executable_name != expected_test_executable_name: 108 continue 109 results.append(test_case_name) 110 return results 111 112 113class _TestExecutableWrapper: 114 def __init__(self, path_to_test_executable): 115 if not os.path.isfile(path_to_test_executable): 116 raise ValueError('No such file: ' + path_to_test_executable) 117 self._path_to_test_executable = path_to_test_executable 118 self._name_of_test_executable, _ = os.path.splitext( 119 os.path.basename(path_to_test_executable)) 120 121 def list_all_tests(self): 122 """Returns: 123 A list of strings - a list of all test names. 124 """ 125 args = [self._path_to_test_executable, '--list', '--format=terse'] 126 output = subprocess.check_output(args, text=True) 127 return _scrape_test_list(output, self._name_of_test_executable) 128 129 def run_tests(self, list_of_tests_to_run): 130 """Runs tests listed in `list_of_tests_to_run`. Ignores tests for other 131 test executables. 132 133 Args: 134 list_of_tests_to_run: A list of strings (a list of test names). 135 136 Returns: 137 A list of test_results.TestResult objects. 138 """ 139 list_of_tests_to_run = _get_exe_specific_tests( 140 self._name_of_test_executable, list_of_tests_to_run) 141 if not list_of_tests_to_run: 142 return [] 143 144 # TODO(lukasza): Avoid passing all test names on the cmdline (might 145 # require adding support to Rust test executables for reading cmdline 146 # args from a file). 147 # TODO(lukasza): Avoid scraping human-readable output (try using 148 # JSON output once it stabilizes; hopefully preserving human-readable 149 # output to the terminal). 150 args = [ 151 self._path_to_test_executable, '--test', '--format=pretty', 152 '--color=always', '--exact' 153 ] 154 args.extend(list_of_tests_to_run) 155 156 print('Running tests from {}...'.format(self._name_of_test_executable)) 157 output = exe_util.run_and_tee_output(args) 158 print('Running tests from {}... DONE.'.format( 159 self._name_of_test_executable)) 160 print() 161 162 return _scrape_test_results(output, self._name_of_test_executable, 163 list_of_tests_to_run) 164 165 166def _parse_args(args): 167 description = 'Wrapper for running Rust unit tests with support for ' \ 168 'Chromium test filters, sharding, and test output.' 169 parser = argparse.ArgumentParser(description=description) 170 main_program.add_cmdline_args(parser) 171 172 parser.add_argument('--rust-test-executable', 173 action='append', 174 dest='rust_test_executables', 175 default=[], 176 help=argparse.SUPPRESS, 177 metavar='FILEPATH', 178 required=True) 179 180 return parser.parse_args(args=args) 181 182 183if __name__ == '__main__': 184 parsed_args = _parse_args(sys.argv[1:]) 185 rust_tests_wrappers = [ 186 _TestExecutableWrapper(path) 187 for path in parsed_args.rust_test_executables 188 ] 189 main_program.main(rust_tests_wrappers, parsed_args, os.environ) 190