aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/tools/ctest-driver.py
blob: 5edec6a6d422e2b4e464fbdfde85c4153469ad08 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#!/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 for CTest

CTest suffers from at least two issues, in particular with regard to its
PASS_REGULAR_EXPRESSION feature:

1. The regular expression syntax 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_any(s, regexes):
    if not regexes:
        return True
    for regex in regexes:
        if re.search(regex, s, flags=re.MULTILINE):
            return True
    return False


def match_pass_regexes(output, regexes):
    if not match_any(output, regexes):
        err("Couldn't match test program's output against any 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:
        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)


def grep_file(args):
    contents = read_file(args.path)
    match_pass_regexes(contents, args.pass_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('-r', '--pass-regex', nargs='*',
                            dest='pass_regexes', metavar='REGEX',
                            help='regular expressions to match program output against')
    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('-r', '--pass-regex', nargs='*',
                             dest='pass_regexes', metavar='REGEX',
                             help='regular expressions to check file contents against')
    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()