| #!/usr/bin/env python3 |
| |
| from __future__ import annotations |
| |
| import argparse |
| import os |
| from typing import Any |
| |
| |
| try: |
| from junitparser import ( # type: ignore[import] |
| Error, |
| Failure, |
| JUnitXml, |
| TestCase, |
| TestSuite, |
| ) |
| except ImportError as e: |
| raise ImportError( |
| "junitparser not found, please install with 'pip install junitparser'" |
| ) from e |
| |
| 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: 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() |