Loading .gitlab-ci.yml +1 −1 Original line number Diff line number Diff line Loading @@ -63,7 +63,7 @@ Publish Unit Tests: script: - pip install --upgrade junit2html - mkdir -p unittest - python util/junit_merge.py results.*/junit.xml > junit.xml - python .gitlab/ci/junit_merge.py results.*/junit.xml > junit.xml - junit2html junit.xml unittest/index.html artifacts: when: always Loading util/junit_merge.py→.gitlab/ci/junit_merge.py +107 −0 Original line number Diff line number Diff line # # Copyright 2018 Dominik Sekotill <dom.sekotill@kodo.org.uk> # Copyright 2018, 2026 Dominik Sekotill <dom.sekotill@kodo.org.uk> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. Loading @@ -18,16 +18,20 @@ Simple jUnit XML merger """ import sys from collections.abc import Iterable from xml.etree import ElementTree as etree # ElementTree is a stupid module name SUITE_METRICS = 'tests', 'failures', 'errors', 'skipped' type Element = etree.Element[str] type ElementTree = etree.ElementTree[Element] SUITE_METRICS = "tests", "failures", "errors", "skipped" def merge(testsuites, tree, name_format=None): def merge(testsuites: Element, tree: ElementTree, name_format: str | None = None) -> None: """ Merge a jUnit tree into a 'testsuites' element """ assert testsuites.tag == 'testsuites' assert testsuites.tag == "testsuites" total_metrics = get_suite_metrics(testsuites) for test_suite in get_suites(tree): Loading @@ -36,32 +40,32 @@ def merge(testsuites, tree, name_format=None): for name in metrics: total_metrics[name] += metrics[name] if name_format: test_suite.set('name', name_format.format(test_suite.get('name'))) test_suite.set("name", name_format.format(test_suite.get("name"))) for name, value in total_metrics.items(): testsuites.set(name, str(value)) def get_suite_metrics(suite): def get_suite_metrics(suite: Element) -> dict[str, int | float]: """ Return a dict of the metric attributes of a test suite """ metrics = {name: int(suite.get(name, 0)) for name in SUITE_METRICS} metrics['time'] = float(suite.get('time', 0.0)) metrics: dict[str, int | float] = {name: int(suite.get(name, 0)) for name in SUITE_METRICS} metrics["time"] = float(suite.get("time", 0.0)) if metrics['tests']: if metrics["tests"]: return metrics # No metric attributes: make some for testcase in suite.iter('testcase'): metrics['time'] += float(testcase.get('time', 0.0)) metrics['tests'] += 1 if testcase.find('failure'): metrics['failures'] += 1 elif testcase.find('error'): metrics['errors'] += 1 elif testcase.find('skipped'): metrics['skipped'] += 1 for testcase in suite.iter("testcase"): metrics["time"] += float(testcase.get("time", 0.0)) metrics["tests"] += 1 if testcase.find("failure"): metrics["failures"] += 1 elif testcase.find("error"): metrics["errors"] += 1 elif testcase.find("skipped"): metrics["skipped"] += 1 for name, value in metrics.items(): suite.set(name, str(value)) Loading @@ -69,33 +73,35 @@ def get_suite_metrics(suite): return metrics def get_suites(tree): def get_suites(tree: ElementTree) -> Iterable[Element]: """ Get all the testsuites as a list """ root = tree.find('.') if root.tag == 'testsuite': root = tree.find(".") assert root is not None if root.tag == "testsuite": return [root] return root.find('.//testsuite') suites = root.find(".//testsuite") assert suites is not None return suites def main(): def main() -> None: """ Main CLI entrypoint function Merge each JUnit results file on the command line and output the result to stdout """ testsuites = etree.Element('testsuites') testsuites.text = '\n' # .text & .tail are a deeply brain-dead design testsuites = etree.Element("testsuites") testsuites.text = "\n" # .text & .tail are a deeply brain-dead design for arg in sys.argv[1:]: filename, *name_prefix = arg.split(':', 1) name_fmt = '{0[0]}: ({{}})'.format(name_prefix) if name_prefix else None filename, *name_prefix = arg.split(":", 1) name_fmt = f"{name_prefix[0]}: ({{}})" if name_prefix else None tree = etree.parse(filename) merge(testsuites, tree, name_format=name_fmt) tree.getroot().tail = '\n' tree.getroot().tail = "\n" with open(1, 'wb') as stdout: etree.ElementTree(testsuites) \ .write(stdout, encoding='UTF-8', xml_declaration=True) with open(1, "wb") as stdout: etree.ElementTree(testsuites).write(stdout, encoding="UTF-8", xml_declaration=True) if __name__ == '__main__': if __name__ == "__main__": main() Loading
.gitlab-ci.yml +1 −1 Original line number Diff line number Diff line Loading @@ -63,7 +63,7 @@ Publish Unit Tests: script: - pip install --upgrade junit2html - mkdir -p unittest - python util/junit_merge.py results.*/junit.xml > junit.xml - python .gitlab/ci/junit_merge.py results.*/junit.xml > junit.xml - junit2html junit.xml unittest/index.html artifacts: when: always Loading
util/junit_merge.py→.gitlab/ci/junit_merge.py +107 −0 Original line number Diff line number Diff line # # Copyright 2018 Dominik Sekotill <dom.sekotill@kodo.org.uk> # Copyright 2018, 2026 Dominik Sekotill <dom.sekotill@kodo.org.uk> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. Loading @@ -18,16 +18,20 @@ Simple jUnit XML merger """ import sys from collections.abc import Iterable from xml.etree import ElementTree as etree # ElementTree is a stupid module name SUITE_METRICS = 'tests', 'failures', 'errors', 'skipped' type Element = etree.Element[str] type ElementTree = etree.ElementTree[Element] SUITE_METRICS = "tests", "failures", "errors", "skipped" def merge(testsuites, tree, name_format=None): def merge(testsuites: Element, tree: ElementTree, name_format: str | None = None) -> None: """ Merge a jUnit tree into a 'testsuites' element """ assert testsuites.tag == 'testsuites' assert testsuites.tag == "testsuites" total_metrics = get_suite_metrics(testsuites) for test_suite in get_suites(tree): Loading @@ -36,32 +40,32 @@ def merge(testsuites, tree, name_format=None): for name in metrics: total_metrics[name] += metrics[name] if name_format: test_suite.set('name', name_format.format(test_suite.get('name'))) test_suite.set("name", name_format.format(test_suite.get("name"))) for name, value in total_metrics.items(): testsuites.set(name, str(value)) def get_suite_metrics(suite): def get_suite_metrics(suite: Element) -> dict[str, int | float]: """ Return a dict of the metric attributes of a test suite """ metrics = {name: int(suite.get(name, 0)) for name in SUITE_METRICS} metrics['time'] = float(suite.get('time', 0.0)) metrics: dict[str, int | float] = {name: int(suite.get(name, 0)) for name in SUITE_METRICS} metrics["time"] = float(suite.get("time", 0.0)) if metrics['tests']: if metrics["tests"]: return metrics # No metric attributes: make some for testcase in suite.iter('testcase'): metrics['time'] += float(testcase.get('time', 0.0)) metrics['tests'] += 1 if testcase.find('failure'): metrics['failures'] += 1 elif testcase.find('error'): metrics['errors'] += 1 elif testcase.find('skipped'): metrics['skipped'] += 1 for testcase in suite.iter("testcase"): metrics["time"] += float(testcase.get("time", 0.0)) metrics["tests"] += 1 if testcase.find("failure"): metrics["failures"] += 1 elif testcase.find("error"): metrics["errors"] += 1 elif testcase.find("skipped"): metrics["skipped"] += 1 for name, value in metrics.items(): suite.set(name, str(value)) Loading @@ -69,33 +73,35 @@ def get_suite_metrics(suite): return metrics def get_suites(tree): def get_suites(tree: ElementTree) -> Iterable[Element]: """ Get all the testsuites as a list """ root = tree.find('.') if root.tag == 'testsuite': root = tree.find(".") assert root is not None if root.tag == "testsuite": return [root] return root.find('.//testsuite') suites = root.find(".//testsuite") assert suites is not None return suites def main(): def main() -> None: """ Main CLI entrypoint function Merge each JUnit results file on the command line and output the result to stdout """ testsuites = etree.Element('testsuites') testsuites.text = '\n' # .text & .tail are a deeply brain-dead design testsuites = etree.Element("testsuites") testsuites.text = "\n" # .text & .tail are a deeply brain-dead design for arg in sys.argv[1:]: filename, *name_prefix = arg.split(':', 1) name_fmt = '{0[0]}: ({{}})'.format(name_prefix) if name_prefix else None filename, *name_prefix = arg.split(":", 1) name_fmt = f"{name_prefix[0]}: ({{}})" if name_prefix else None tree = etree.parse(filename) merge(testsuites, tree, name_format=name_fmt) tree.getroot().tail = '\n' tree.getroot().tail = "\n" with open(1, 'wb') as stdout: etree.ElementTree(testsuites) \ .write(stdout, encoding='UTF-8', xml_declaration=True) with open(1, "wb") as stdout: etree.ElementTree(testsuites).write(stdout, encoding="UTF-8", xml_declaration=True) if __name__ == '__main__': if __name__ == "__main__": main()