xref: /aosp_15_r20/external/cronet/testing/scripts/rust/rust_main_program.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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
5 compatible with the requirements of the `main_program` module.
6 """
7 
8 import argparse
9 import os
10 import re
11 import subprocess
12 import sys
13 
14 sys.path.append(os.path.dirname(os.path.abspath(__file__)))
15 import exe_util
16 import main_program
17 import test_results
18 
19 
20 def _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 
27 def _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 
35 def _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 
65 def _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 
103 def _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 
114 class _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 
167 def _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 
184 if __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