# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""This is a library for wrapping Rust test executables in a way that is
compatible with the requirements of the `main_program` module.
"""
import argparse
import os
import re
import subprocess
import sys
import exe_util
import main_program
import test_results
def _format_test_name(test_executable_name, test_case_name):
assert '//' not in test_executable_name
assert '/' not in test_case_name
test_case_name = '/'.join(test_case_name.split('::'))
return '{}//{}'.format(test_executable_name, test_case_name)
def _parse_test_name(test_name):
assert '//' in test_name
assert '::' not in test_name
test_executable_name, test_case_name = test_name.split('//', 1)
test_case_name = '::'.join(test_case_name.split('/'))
return test_executable_name, test_case_name
def _scrape_test_list(output, test_executable_name):
"""Scrapes stdout from running a Rust test executable with
--list and --format=terse.
Args:
output: A string with the full stdout of a Rust test executable.
test_executable_name: A string. Used as a prefix in "full" test names
in the returned results.
Returns:
A list of strings - a list of all test names.
"""
TEST_SUFFIX = ': test'
BENCHMARK_SUFFIX = ': benchmark'
test_case_names = []
for line in output.splitlines():
if line.endswith(TEST_SUFFIX):
test_case_names.append(line[:-len(TEST_SUFFIX)])
elif line.endswith(BENCHMARK_SUFFIX):
continue
else:
raise ValueError(
'Unexpected format of a list of tests: {}'.format(output))
test_names = [
_format_test_name(test_executable_name, test_case_name)
for test_case_name in test_case_names
]
return test_names
def _scrape_test_results(output, test_executable_name,
list_of_expected_test_case_names):
"""Scrapes stdout from running a Rust test executable with
--test --format=pretty.
Args:
output: A string with the full stdout of a Rust test executable.
test_executable_name: A string. Used as a prefix in "full" test names
in the returned TestResult objects.
list_of_expected_test_case_names: A list of strings - expected test case
names (from the perspective of a single executable / with no prefix).
Returns:
A list of test_results.TestResult objects.
"""
results = []
regex = re.compile(r'^test ([:\w]+) \.\.\. (\w+)')
for line in output.splitlines():
match = regex.match(line.strip())
if not match:
continue
test_case_name = match.group(1)
if test_case_name not in list_of_expected_test_case_names:
continue
actual_test_result = match.group(2)
if actual_test_result == 'ok':
actual_test_result = 'PASS'
elif actual_test_result == 'FAILED':
actual_test_result = 'FAIL'
elif actual_test_result == 'ignored':
actual_test_result = 'SKIP'
test_name = _format_test_name(test_executable_name, test_case_name)
results.append(test_results.TestResult(test_name, actual_test_result))
return results
def _get_exe_specific_tests(expected_test_executable_name, list_of_test_names):
results = []
for test_name in list_of_test_names:
actual_test_executable_name, test_case_name = _parse_test_name(
test_name)
if actual_test_executable_name != expected_test_executable_name:
continue
results.append(test_case_name)
return results
class _TestExecutableWrapper:
def __init__(self, path_to_test_executable):
if not os.path.isfile(path_to_test_executable):
raise ValueError('No such file: ' + path_to_test_executable)
self._path_to_test_executable = path_to_test_executable
self._name_of_test_executable, _ = os.path.splitext(
os.path.basename(path_to_test_executable))
def list_all_tests(self):
"""Returns:
A list of strings - a list of all test names.
"""
args = [self._path_to_test_executable, '--list', '--format=terse']
output = subprocess.check_output(args, text=True)
return _scrape_test_list(output, self._name_of_test_executable)
def run_tests(self, list_of_tests_to_run):
"""Runs tests listed in `list_of_tests_to_run`. Ignores tests for other
test executables.
Args:
list_of_tests_to_run: A list of strings (a list of test names).
Returns:
A list of test_results.TestResult objects.
"""
list_of_tests_to_run = _get_exe_specific_tests(
self._name_of_test_executable, list_of_tests_to_run)
if not list_of_tests_to_run:
return []
# TODO(lukasza): Avoid passing all test names on the cmdline (might
# require adding support to Rust test executables for reading cmdline
# args from a file).
# TODO(lukasza): Avoid scraping human-readable output (try using
# JSON output once it stabilizes; hopefully preserving human-readable
# output to the terminal).
args = [
self._path_to_test_executable, '--test', '--format=pretty',
'--color=always', '--exact'
]
args.extend(list_of_tests_to_run)
print('Running tests from {}...'.format(self._name_of_test_executable))
output = exe_util.run_and_tee_output(args)
print('Running tests from {}... DONE.'.format(
self._name_of_test_executable))
print()
return _scrape_test_results(output, self._name_of_test_executable,
list_of_tests_to_run)
def _parse_args(args):
description = 'Wrapper for running Rust unit tests with support for ' \
'Chromium test filters, sharding, and test output.'
parser = argparse.ArgumentParser(description=description)
main_program.add_cmdline_args(parser)
parser.add_argument('--rust-test-executable',
action='append',
dest='rust_test_executables',
default=[],
help=argparse.SUPPRESS,
metavar='FILEPATH',
required=True)
return parser.parse_args(args=args)
if __name__ == '__main__':
parsed_args = _parse_args(sys.argv[1:])
rust_tests_wrappers = [
_TestExecutableWrapper(path)
for path in parsed_args.rust_test_executables
]
main_program.main(rust_tests_wrappers, parsed_args, os.environ)