aboutsummaryrefslogblamecommitdiffstatshomepage
path: root/tools/ctest-driver.py
blob: 414e20ce1d5403a1c2b4c5d618491fae105862f4 (plain) (tree)
1
2
3
4
5
6
7
8

                      




                                                               
                                                 



                                                                        
                                                            



























                                                                    
                  


                                                                              
                                                       






                                              









                                                                            



                                                  
                          




                                                      


                                        



                                                                      


                                    











                                                                        


                                 




                                              
                                                                    



                                                                                 
                                                     




                                                   
                                                   












                                                                                            
                                                            
                                                                 



                                                                        



                                                                                

                                                                                



                                                                                                 
                                                             
                                                                  



                                                                         















                                                                           
#!/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()