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