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