From 9a1e2ae270f2d7dff2aeff86b93e33bf9f901b9c Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Mon, 5 Oct 2026 12:45:32 +0500 Subject: tools/ctest-driver.py -> project/ctest_driver.py --- project/ctest_driver.py | 203 +++++++++++++++++++++++++++++++++++++++++++ project/format.py | 1 - pyproject.toml | 6 +- tools/ctest-driver.py | 223 ------------------------------------------------ 4 files changed, 204 insertions(+), 229 deletions(-) create mode 100644 project/ctest_driver.py delete mode 100755 tools/ctest-driver.py diff --git a/project/ctest_driver.py b/project/ctest_driver.py new file mode 100644 index 0000000..5484d51 --- /dev/null +++ b/project/ctest_driver.py @@ -0,0 +1,203 @@ +# 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 logging +import re +import subprocess +import sys + +from project import utils + + +def read_file(path): + with open(path, mode="r") as fd: + return fd.read() + + +def _run_exit_if_invalid_code(actual, expected): + if actual in expected: + return + logging.error( + "Actual exit code %d is not among the allowed codes %s", actual, str(expected) + ) + sys.exit(actual) + + +def run(cmd_line, exit_codes=(0,)): + try: + return utils.run_capture(cmd_line) + except subprocess.CalledProcessError as e: + _run_exit_if_invalid_code(e.returncode, exit_codes) + return e.stdout + + +def run_new_window(cmd_line, exit_codes=(0,)): + try: + utils.run(cmd_line, creationflags=subprocess.CREATE_NEW_CONSOLE) + except subprocess.CalledProcessError as e: + _run_exit_if_invalid_code(e.returncode, exit_codes) + + +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): + logging.error( + "Couldn't match test program's output against all of the regular expressions:" + ) + for regex in regexes: + logging.error(" %s", regex) + sys.exit(1) + + +def match_fail_regexes(output, regexes): + if not regexes: + return + if match_any(output, regexes): + logging.error( + "Matched test program's output against some of the regular expressions:" + ) + for regex in regexes: + logging.error(" %s", 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, args.exit_codes) + if args.new_window and (args.pass_regexes or args.fail_regexes): + logging.error( + "Cannot launch child process in a new window and capture its output" + ) + if output is not None: + 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( + "-e", + "--exit-code", + nargs="*", + type=int, + default=[0], + dest="exit_codes", + metavar="NUM", + help="allowed exit_codes (only 0 by default)", + ) + 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() diff --git a/project/format.py b/project/format.py index 5f24062..e1897db 100644 --- a/project/format.py +++ b/project/format.py @@ -12,7 +12,6 @@ formatting the files in-place. import argparse import difflib -import logging import os import sys diff --git a/pyproject.toml b/pyproject.toml index 7fd21f8..b68ca10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,10 +31,6 @@ boost-build = "project.boost.build:_main" boost-download = "project.boost.download:_main" project-build = "project.build:main" project-format = "project.format:main" - -[tool.setuptools] -script-files = [ - "tools/ctest-driver.py", -] +project-ctest-driver = "project.ctest_driver:main" [tool.setuptools_scm] diff --git a/tools/ctest-driver.py b/tools/ctest-driver.py deleted file mode 100755 index a2151c5..0000000 --- a/tools/ctest-driver.py +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/env python - -# 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, exit_codes=[0]): - 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: - if e.returncode not in exit_codes: - err( - f"Actual exit code {e.returncode} is not among the allowed codes {exit_codes}" - ) - sys.stdout.write(e.output) - sys.exit(e.returncode) - return e.output - - -def run_new_window(cmd_line, exit_codes=[0]): - try: - result = subprocess.run( - cmd_line, check=True, creationflags=subprocess.CREATE_NEW_CONSOLE - ) - assert result.returncode == 0 - except subprocess.CalledProcessError as e: - if e.returncode not in exit_codes: - err( - f"Actual exit code {e.returncode} is not among the allowed codes {exit_codes}" - ) - sys.exit(e.returncode) - return None - - -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, args.exit_codes) - 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( - "-e", - "--exit-code", - nargs="*", - type=int, - default=[0], - dest="exit_codes", - metavar="NUM", - help="allowed exit_codes (only 0 by default)", - ) - 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() -- cgit v1.3