forked from wireshark/wireshark
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
tools+GitLab CI: Add a test report wrapper
Add wrap-test.py, which runs a CI test command and adds it to a JUnit report which can be consumed by GitLab CI. Have the script "tee" output so that errors and warnings show up in CI job output and in the merge request test report.
- Loading branch information
1 parent
efbbd71
commit cd785cd
Showing
2 changed files
with
139 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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()) |