• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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