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