From cd785cd8e5519320affaf5f29419d26dc6379c47 Mon Sep 17 00:00:00 2001 From: Gerald Combs Date: Thu, 28 Nov 2024 11:47:57 -0800 Subject: [PATCH] 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. --- .gitlab-ci.yml | 87 ++++------------------------ tools/wrap-ci-test.py | 129 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 77 deletions(-) create mode 100755 tools/wrap-ci-test.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5cfaf6e6e06..b3fe25c8672 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -753,83 +753,16 @@ Commit Check: # https://docs.gitlab.com/ee/ci/testing/unit_test_reports.html # https://www.ibm.com/docs/en/developer-for-zos/14.2?topic=formats-junit-xml-format # - # Let's generate a unit test report, even if that means wondering which life choices - # led us to assemlbing an XML document from fragments in a shell script which is - # embedded in YAML. - - FAILURE_COUNT=0 - - TESTCASES="" - FAILURE_COUNT=$(( FAILURE_COUNT + 1 )) - else - TESTCASES+="/>" - fi - - PC_EXIT_CODE=0 - - ANALYSIS_MESSAGE=$( ./tools/pre-commit "${CI_COMMIT_SHA}~$NUM_COMMITS" ) || PC_EXIT_CODE=1 - - TESTCASES+=" commit_checks.xml < - - $TESTCASES - - FIN - # - cat commit_checks.xml - - grep ' 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())