| #!/usr/bin/env python3 | |
| import argparse | |
| import os | |
| from typing import Any, List, Union | |
| try: | |
| from junitparser import JUnitXml, TestSuite, TestCase, Error, Failure # type: ignore[import] | |
| except ImportError: | |
| raise ImportError( | |
| "junitparser not found, please install with 'pip install junitparser'" | |
| ) | |
| try: | |
| import rich | |
| except ImportError: | |
| print("rich not found, for color output use 'pip install rich'") | |
| def parse_junit_reports(path_to_reports: str) -> List[TestCase]: # type: ignore[no-any-unimported] | |
| def parse_file(path: str) -> List[TestCase]: # type: ignore[no-any-unimported] | |
| try: | |
| return convert_junit_to_testcases(JUnitXml.fromfile(path)) | |
| except Exception as err: | |
| rich.print( | |
| f":Warning: [yellow]Warning[/yellow]: Failed to read {path}: {err}" | |
| ) | |
| return [] | |
| if not os.path.exists(path_to_reports): | |
| raise FileNotFoundError(f"Path '{path_to_reports}', not found") | |
| # Return early if the path provided is just a file | |
| if os.path.isfile(path_to_reports): | |
| return parse_file(path_to_reports) | |
| ret_xml = [] | |
| if os.path.isdir(path_to_reports): | |
| for root, _, files in os.walk(path_to_reports): | |
| for fname in [f for f in files if f.endswith("xml")]: | |
| ret_xml += parse_file(os.path.join(root, fname)) | |
| return ret_xml | |
| def convert_junit_to_testcases(xml: Union[JUnitXml, TestSuite]) -> List[TestCase]: # type: ignore[no-any-unimported] | |
| testcases = [] | |
| for item in xml: | |
| if isinstance(item, TestSuite): | |
| testcases.extend(convert_junit_to_testcases(item)) | |
| else: | |
| testcases.append(item) | |
| return testcases | |
| def render_tests(testcases: List[TestCase]) -> None: # type: ignore[no-any-unimported] | |
| num_passed = 0 | |
| num_skipped = 0 | |
| num_failed = 0 | |
| for testcase in testcases: | |
| if not testcase.result: | |
| num_passed += 1 | |
| continue | |
| for result in testcase.result: | |
| if isinstance(result, Error): | |
| icon = ":rotating_light: [white on red]ERROR[/white on red]:" | |
| num_failed += 1 | |
| elif isinstance(result, Failure): | |
| icon = ":x: [white on red]Failure[/white on red]:" | |
| num_failed += 1 | |
| else: | |
| num_skipped += 1 | |
| continue | |
| rich.print( | |
| f"{icon} [bold red]{testcase.classname}.{testcase.name}[/bold red]" | |
| ) | |
| print(f"{result.text}") | |
| rich.print(f":white_check_mark: {num_passed} [green]Passed[green]") | |
| rich.print(f":dash: {num_skipped} [grey]Skipped[grey]") | |
| rich.print(f":rotating_light: {num_failed} [grey]Failed[grey]") | |
| def parse_args() -> Any: | |
| parser = argparse.ArgumentParser( | |
| description="Render xunit output for failed tests", | |
| ) | |
| parser.add_argument( | |
| "report_path", | |
| help="Base xunit reports (single file or directory) to compare to", | |
| ) | |
| return parser.parse_args() | |
| def main() -> None: | |
| options = parse_args() | |
| testcases = parse_junit_reports(options.report_path) | |
| render_tests(testcases) | |
| if __name__ == "__main__": | |
| main() |