aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/tools/ctest-driver.py
blob: 414e20ce1d5403a1c2b4c5d618491fae105862f4 (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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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()