File: wrap-ci-test.py

package info (click to toggle)
wireshark 4.6.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 351,244 kB
  • sloc: ansic: 3,101,885; cpp: 129,710; xml: 100,972; python: 56,512; perl: 24,575; sh: 5,874; lex: 4,383; pascal: 4,304; makefile: 165; ruby: 113; objc: 91; tcl: 35
file content (129 lines) | stat: -rwxr-xr-x 4,792 bytes parent folder | download | duplicates (3)
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
#!/usr/bin/env python3

#
# Add arbritrary commands to a GitLab CI compatible (JUnit) test report
# SPDX-License-Identifier: MIT
#
# Usage:
#   wrap-ci-test --file foo.xml --suite "Suite" --case "Name" --command "command"
#   wrap-ci-test --file foo.xml --suite "Suite" --case "Name" command [args] ...

# This script runs a command and adds it to a JUnit report which can then
# be used as a GitLab CI test report:
#
#   https://docs.gitlab.com/ee/ci/testing/unit_test_reports.html
#
# Commands can be specified with the "--command" flag, which will run
# in a subshell, or as a list of extra arguments, which will be run
# directly.
#
# Command output will be "teed". Scrubbed versions will be added to the
# report and unmodified versions will be printed to stdout and stderr.
#
# If the command exit code is nonzero it will be added to the report
# as a failure.
#
# The wrapper will return the command exit code.

# JUnit report information can be found at
# https://github.com/testmoapp/junitxml
# https://www.ibm.com/docs/en/developer-for-zos/14.2?topic=formats-junit-xml-format


import argparse
import html
import time
import pathlib
import re
import subprocess
import sys
import xml.etree.ElementTree as ET


def main():
    parser = argparse.ArgumentParser(usage='\n  %(prog)s [options] --command "command"\n  %(prog)s [options] command ...')
    parser.add_argument('--file', required=True, type=pathlib.Path, help='The JUnit-compatible XML file')
    parser.add_argument('--suite', required=True, help='The testsuite_el name')
    parser.add_argument('--case', required=True, help='The testcase name')
    parser.add_argument('--command', help='The command to run if no extra arguments are provided')

    args, command_list = parser.parse_known_args()

    if (args.command and len(command_list) > 0) or (args.command is None and len(command_list) == 0):
        sys.stderr.write('Error: The command must be provided via the --command flag or extra arguments.\n')
        sys.exit(1)

    try:
        tree = ET.parse(args.file)
        testsuites_el = tree.getroot()
    except FileNotFoundError:
        testsuites_el = ET.Element('testsuites')
        tree = ET.ElementTree(testsuites_el)
    except ET.ParseError:
        sys.stderr.write(f'Error: {args.file} is invalid.\n')
        sys.exit(1)

    suites_time = float(testsuites_el.get('time', 0.0))
    suites_tests = int(testsuites_el.get('tests', 0)) + 1
    suites_failures = int(testsuites_el.get('failures', 0))

    testsuite_el = testsuites_el.find(f'./testsuite[@name="{args.suite}"]')
    if testsuite_el is None:
        testsuite_el = ET.Element('testsuite', attrib={'name': args.suite})
        testsuites_el.append(testsuite_el)

    suite_time = float(testsuite_el.get('time', 0.0))
    suite_tests = int(testsuite_el.get('tests', 0)) + 1
    suite_failures = int(testsuite_el.get('failures', 0))

    testcase_el = ET.Element('testcase', attrib={'name': args.case})
    testsuite_el.append(testcase_el)

    if args.command:
        proc_args = args.command
        in_shell = True
    else:
        proc_args = command_list
        in_shell = False

    start_time = time.perf_counter()
    proc = subprocess.run(proc_args, shell=in_shell, encoding='UTF-8', errors='replace', capture_output=True)
    case_time = time.perf_counter() - start_time

    testcase_el.set('time', f'{case_time}')
    testsuite_el.set('time', f'{suite_time + case_time}')
    testsuites_el.set('time', f'{suites_time + case_time}')

    # XXX Try to interleave them?
    sys.stdout.write(proc.stdout)
    sys.stderr.write(proc.stderr)

    # Remove ANSI control sequences and escape other invalid characters
    # https://stackoverflow.com/a/14693789/82195
    ansi_seq_re = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
    scrubbed_stdout = html.escape(ansi_seq_re.sub('', proc.stdout), quote=False)
    scrubbed_stderr = html.escape(ansi_seq_re.sub('', proc.stderr), quote=False)

    if proc.returncode != 0:
        failure_el = ET.Element('failure')
        failure_el.text = f'{scrubbed_stdout}{scrubbed_stderr}'
        testcase_el.append(failure_el)
        testsuite_el.set('failures', f'{suite_failures + 1}')
        testsuites_el.set('failures', f'{suites_failures + 1}')
    else:
        system_out_el = ET.Element('system-out')
        system_out_el.text = f'{scrubbed_stdout}'
        testcase_el.append(system_out_el)
        system_err_el = ET.Element('system-err')
        system_err_el.text = f'{scrubbed_stderr}'
        testcase_el.append(system_err_el)

    testsuite_el.set('tests', f'{suite_tests}')
    testsuites_el.set('tests', f'{suites_tests}')

    tree.write(args.file, encoding='UTF-8', xml_declaration=True)

    return proc.returncode

if __name__ == '__main__':
    sys.exit(main())