1*da0073e9SAndroid Build Coastguard Worker#!/usr/bin/env python3 2*da0073e9SAndroid Build Coastguard Worker 3*da0073e9SAndroid Build Coastguard Workerfrom __future__ import annotations 4*da0073e9SAndroid Build Coastguard Worker 5*da0073e9SAndroid Build Coastguard Workerimport argparse 6*da0073e9SAndroid Build Coastguard Workerimport os 7*da0073e9SAndroid Build Coastguard Workerfrom typing import Any 8*da0073e9SAndroid Build Coastguard Worker 9*da0073e9SAndroid Build Coastguard Worker 10*da0073e9SAndroid Build Coastguard Workertry: 11*da0073e9SAndroid Build Coastguard Worker from junitparser import ( # type: ignore[import] 12*da0073e9SAndroid Build Coastguard Worker Error, 13*da0073e9SAndroid Build Coastguard Worker Failure, 14*da0073e9SAndroid Build Coastguard Worker JUnitXml, 15*da0073e9SAndroid Build Coastguard Worker TestCase, 16*da0073e9SAndroid Build Coastguard Worker TestSuite, 17*da0073e9SAndroid Build Coastguard Worker ) 18*da0073e9SAndroid Build Coastguard Workerexcept ImportError as e: 19*da0073e9SAndroid Build Coastguard Worker raise ImportError( 20*da0073e9SAndroid Build Coastguard Worker "junitparser not found, please install with 'pip install junitparser'" 21*da0073e9SAndroid Build Coastguard Worker ) from e 22*da0073e9SAndroid Build Coastguard Worker 23*da0073e9SAndroid Build Coastguard Workertry: 24*da0073e9SAndroid Build Coastguard Worker import rich 25*da0073e9SAndroid Build Coastguard Workerexcept ImportError: 26*da0073e9SAndroid Build Coastguard Worker print("rich not found, for color output use 'pip install rich'") 27*da0073e9SAndroid Build Coastguard Worker 28*da0073e9SAndroid Build Coastguard Worker 29*da0073e9SAndroid Build Coastguard Workerdef parse_junit_reports(path_to_reports: str) -> list[TestCase]: # type: ignore[no-any-unimported] 30*da0073e9SAndroid Build Coastguard Worker def parse_file(path: str) -> list[TestCase]: # type: ignore[no-any-unimported] 31*da0073e9SAndroid Build Coastguard Worker try: 32*da0073e9SAndroid Build Coastguard Worker return convert_junit_to_testcases(JUnitXml.fromfile(path)) 33*da0073e9SAndroid Build Coastguard Worker except Exception as err: 34*da0073e9SAndroid Build Coastguard Worker rich.print( 35*da0073e9SAndroid Build Coastguard Worker f":Warning: [yellow]Warning[/yellow]: Failed to read {path}: {err}" 36*da0073e9SAndroid Build Coastguard Worker ) 37*da0073e9SAndroid Build Coastguard Worker return [] 38*da0073e9SAndroid Build Coastguard Worker 39*da0073e9SAndroid Build Coastguard Worker if not os.path.exists(path_to_reports): 40*da0073e9SAndroid Build Coastguard Worker raise FileNotFoundError(f"Path '{path_to_reports}', not found") 41*da0073e9SAndroid Build Coastguard Worker # Return early if the path provided is just a file 42*da0073e9SAndroid Build Coastguard Worker if os.path.isfile(path_to_reports): 43*da0073e9SAndroid Build Coastguard Worker return parse_file(path_to_reports) 44*da0073e9SAndroid Build Coastguard Worker ret_xml = [] 45*da0073e9SAndroid Build Coastguard Worker if os.path.isdir(path_to_reports): 46*da0073e9SAndroid Build Coastguard Worker for root, _, files in os.walk(path_to_reports): 47*da0073e9SAndroid Build Coastguard Worker for fname in [f for f in files if f.endswith("xml")]: 48*da0073e9SAndroid Build Coastguard Worker ret_xml += parse_file(os.path.join(root, fname)) 49*da0073e9SAndroid Build Coastguard Worker return ret_xml 50*da0073e9SAndroid Build Coastguard Worker 51*da0073e9SAndroid Build Coastguard Worker 52*da0073e9SAndroid Build Coastguard Workerdef convert_junit_to_testcases(xml: JUnitXml | TestSuite) -> list[TestCase]: # type: ignore[no-any-unimported] 53*da0073e9SAndroid Build Coastguard Worker testcases = [] 54*da0073e9SAndroid Build Coastguard Worker for item in xml: 55*da0073e9SAndroid Build Coastguard Worker if isinstance(item, TestSuite): 56*da0073e9SAndroid Build Coastguard Worker testcases.extend(convert_junit_to_testcases(item)) 57*da0073e9SAndroid Build Coastguard Worker else: 58*da0073e9SAndroid Build Coastguard Worker testcases.append(item) 59*da0073e9SAndroid Build Coastguard Worker return testcases 60*da0073e9SAndroid Build Coastguard Worker 61*da0073e9SAndroid Build Coastguard Worker 62*da0073e9SAndroid Build Coastguard Workerdef render_tests(testcases: list[TestCase]) -> None: # type: ignore[no-any-unimported] 63*da0073e9SAndroid Build Coastguard Worker num_passed = 0 64*da0073e9SAndroid Build Coastguard Worker num_skipped = 0 65*da0073e9SAndroid Build Coastguard Worker num_failed = 0 66*da0073e9SAndroid Build Coastguard Worker for testcase in testcases: 67*da0073e9SAndroid Build Coastguard Worker if not testcase.result: 68*da0073e9SAndroid Build Coastguard Worker num_passed += 1 69*da0073e9SAndroid Build Coastguard Worker continue 70*da0073e9SAndroid Build Coastguard Worker for result in testcase.result: 71*da0073e9SAndroid Build Coastguard Worker if isinstance(result, Error): 72*da0073e9SAndroid Build Coastguard Worker icon = ":rotating_light: [white on red]ERROR[/white on red]:" 73*da0073e9SAndroid Build Coastguard Worker num_failed += 1 74*da0073e9SAndroid Build Coastguard Worker elif isinstance(result, Failure): 75*da0073e9SAndroid Build Coastguard Worker icon = ":x: [white on red]Failure[/white on red]:" 76*da0073e9SAndroid Build Coastguard Worker num_failed += 1 77*da0073e9SAndroid Build Coastguard Worker else: 78*da0073e9SAndroid Build Coastguard Worker num_skipped += 1 79*da0073e9SAndroid Build Coastguard Worker continue 80*da0073e9SAndroid Build Coastguard Worker rich.print( 81*da0073e9SAndroid Build Coastguard Worker f"{icon} [bold red]{testcase.classname}.{testcase.name}[/bold red]" 82*da0073e9SAndroid Build Coastguard Worker ) 83*da0073e9SAndroid Build Coastguard Worker print(f"{result.text}") 84*da0073e9SAndroid Build Coastguard Worker rich.print(f":white_check_mark: {num_passed} [green]Passed[green]") 85*da0073e9SAndroid Build Coastguard Worker rich.print(f":dash: {num_skipped} [grey]Skipped[grey]") 86*da0073e9SAndroid Build Coastguard Worker rich.print(f":rotating_light: {num_failed} [grey]Failed[grey]") 87*da0073e9SAndroid Build Coastguard Worker 88*da0073e9SAndroid Build Coastguard Worker 89*da0073e9SAndroid Build Coastguard Workerdef parse_args() -> Any: 90*da0073e9SAndroid Build Coastguard Worker parser = argparse.ArgumentParser( 91*da0073e9SAndroid Build Coastguard Worker description="Render xunit output for failed tests", 92*da0073e9SAndroid Build Coastguard Worker ) 93*da0073e9SAndroid Build Coastguard Worker parser.add_argument( 94*da0073e9SAndroid Build Coastguard Worker "report_path", 95*da0073e9SAndroid Build Coastguard Worker help="Base xunit reports (single file or directory) to compare to", 96*da0073e9SAndroid Build Coastguard Worker ) 97*da0073e9SAndroid Build Coastguard Worker return parser.parse_args() 98*da0073e9SAndroid Build Coastguard Worker 99*da0073e9SAndroid Build Coastguard Worker 100*da0073e9SAndroid Build Coastguard Workerdef main() -> None: 101*da0073e9SAndroid Build Coastguard Worker options = parse_args() 102*da0073e9SAndroid Build Coastguard Worker testcases = parse_junit_reports(options.report_path) 103*da0073e9SAndroid Build Coastguard Worker render_tests(testcases) 104*da0073e9SAndroid Build Coastguard Worker 105*da0073e9SAndroid Build Coastguard Worker 106*da0073e9SAndroid Build Coastguard Workerif __name__ == "__main__": 107*da0073e9SAndroid Build Coastguard Worker main() 108