diff options
Diffstat (limited to 'tools/ctest-driver.py')
-rwxr-xr-x | tools/ctest-driver.py | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/tools/ctest-driver.py b/tools/ctest-driver.py new file mode 100755 index 0000000..414e20c --- /dev/null +++ b/tools/ctest-driver.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2021 Egor Tensin <Egor.Tensin@gmail.com> +# This file is part of the "cmake-common" project. +# For details, see https://github.com/egor-tensin/cmake-common. +# Distributed under the MIT License. + +'''Wrap your actual test driver to use with CTest + +CTest suffers from at least two issues, in particular with regard to its +PASS_REGULAR_EXPRESSION feature: + +1. The regular expression syntax used by CMake is deficient. +2. The exit code of a test is ignored if one of the regexes matches. + +This script tries to fix them. +''' + +import argparse +import os +import re +import subprocess +import sys + + +SCRIPT_NAME = os.path.basename(__file__) + + +def dump(msg, **kwargs): + print(f'{SCRIPT_NAME}: {msg}', **kwargs) + + +def err(msg): + dump(msg, file=sys.stderr) + + +def read_file(path): + with open(path, mode='r') as fd: + return fd.read() + + +def run(cmd_line): + try: + result = subprocess.run(cmd_line, check=True, universal_newlines=True, + stderr=subprocess.STDOUT, + stdout=subprocess.PIPE) + assert result.returncode == 0 + return result.stdout + except subprocess.CalledProcessError as e: + sys.stdout.write(e.output) + sys.exit(e.returncode) + + +def run_new_window(cmd_line): + try: + result = subprocess.run(cmd_line, check=True, + creationflags=subprocess.CREATE_NEW_CONSOLE) + assert result.returncode == 0 + return None + except subprocess.CalledProcessError as e: + sys.exit(e.returncode) + + +def match(s, regex): + return re.search(regex, s, flags=re.MULTILINE) + + +def match_any(s, regexes): + return any([match(s, regex) for regex in regexes]) + + +def match_all(s, regexes): + return all([match(s, regex) for regex in regexes]) + + +def match_pass_regexes(output, regexes): + if not regexes: + return + if not match_all(output, regexes): + err("Couldn't match test program's output against all of the"\ + " regular expressions:") + for regex in regexes: + err(f'\t{regex}') + sys.exit(1) + + +def match_fail_regexes(output, regexes): + if not regexes: + return + if match_any(output, regexes): + err("Matched test program's output against some of the regular"\ + " expressions:") + for regex in regexes: + err(f'\t{regex}') + sys.exit(1) + + +def run_actual_test_driver(args): + cmd_line = [args.exe_path] + args.exe_args + run_func = run + if args.new_window: + run_func = run_new_window + output = run_func(cmd_line) + if args.new_window and (args.pass_regexes or args.fail_regexes): + err("Cannot launch child process in a new window and capture its output") + if output is not None: + sys.stdout.write(output) + match_pass_regexes(output, args.pass_regexes) + match_fail_regexes(output, args.fail_regexes) + + +def grep_file(args): + contents = read_file(args.path) + match_pass_regexes(contents, args.pass_regexes) + match_fail_regexes(contents, args.fail_regexes) + + +def parse_args(argv=None): + if argv is None: + argv = sys.argv[1:] + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + + subparsers = parser.add_subparsers(dest='command') + + parser_run = subparsers.add_parser('run', help='run an executable and check its output') + parser_run.add_argument('-p', '--pass-regex', nargs='*', + dest='pass_regexes', metavar='REGEX', + help='pass if all of these regexes match') + parser_run.add_argument('-f', '--fail-regex', nargs='*', + dest='fail_regexes', metavar='REGEX', + help='fail if any of these regexes matches') + parser_run.add_argument('-n', '--new-window', action='store_true', + help='launch child process in a new console window') + parser_run.add_argument('exe_path', metavar='PATH', + help='path to the test executable') + # nargs='*' here would discard additional '--'s. + parser_run.add_argument('exe_args', metavar='ARG', nargs=argparse.REMAINDER, + help='test executable arguments') + parser_run.set_defaults(func=run_actual_test_driver) + + parser_grep = subparsers.add_parser('grep', help='check file contents for matching patterns') + parser_grep.add_argument('-p', '--pass-regex', nargs='*', + dest='pass_regexes', metavar='REGEX', + help='pass if all of these regexes match') + parser_grep.add_argument('-f', '--fail-regex', nargs='*', + dest='fail_regexes', metavar='REGEX', + help='fail if any of these regexes matches') + parser_grep.add_argument('path', metavar='PATH', help='text file path') + parser_grep.set_defaults(func=grep_file) + + args = parser.parse_args(argv) + if args.command is None: + parser.error('please specify a subcommand to run') + return args + + +def main(argv=None): + args = parse_args(argv) + args.func(args) + + +if __name__ == '__main__': + main() |