#!/usr/bin/env python3 # Copyright (c) 2021 Egor Tensin # 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()