aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/tools/ctest-driver.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/ctest-driver.py')
-rwxr-xr-xtools/ctest-driver.py166
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()