wireshark/tools/wrap-ci-test.py

130 lines
4.7 KiB
Python
Raw Permalink Normal View History

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