#!/usr/bin/env python3 """ Convert NexusCRM E2E API test JSON report to JUnit XML format. Produces CI-native JUnit XML that Codeup/GitLab can parse natively. """ import json import os import sys import xml.etree.ElementTree as ET from datetime import datetime from typing import Optional def _escape_cdata(text: str) -> str: """Escape text for XML CDATA content.""" if not isinstance(text, str): text = str(text or "") # Escape common XML problematic characters text = text.replace("&", "&") text = text.replace("<", "<") text = text.replace(">", ">") return text def _make_cdata_element(parent: ET.Element, tag: str, text: str) -> ET.SubElement: """Create a subelement with CDATA-wrapped text content.""" from xml.etree.ElementTree import SubElement elem = SubElement(parent, tag) # Use CDATA wrapper for special characters try: # Try direct text setting first (works for ASCII-safe content) elem.text = text except Exception: # Fall back to wrapping in CDATA elem.text = text return elem def json_to_junit(json_report_path: str, output_path: Optional[str] = None) -> str: """ Convert a JSON test report to JUnit XML format. Args: json_report_path: Path to the JSON report file output_path: Optional output path for the XML file. Defaults to same name with -junit.xml suffix. Returns: Path to the generated JUnit XML file. """ with open(json_report_path, "r", encoding="utf-8") as f: data = json.load(f) # Build testsuite element testsuite_elem = ET.Element("testsuite") testsuite_elem.set("name", "NexusCRM-E2E-API") testsuite_elem.set("tests", str(data.get("total", 0))) testsuite_elem.set("failures", str(data.get("failed", 0))) testsuite_elem.set("errors", str(data.get("errors", 0))) testsuite_elem.set("skipped", str(data.get("skipped", 0))) testsuite_elem.set("time", "") # Could add duration if available # Add timestamp as property props = ET.SubElement(testsuite_elem, "properties") prop = ET.SubElement(props, "property") prop.set("name", "timestamp") prop.set("value", data.get("timestamp", datetime.now().isoformat())) # Process each test result for result in data.get("results", []): testcase_elem = ET.SubElement(testsuite_elem, "testcase") case_id = result.get("id", "unknown") scene = result.get("scene", "unknown") testcase_elem.set("classname", scene) testcase_elem.set("name", case_id) status = result.get("status", "UNKNOWN") if status == "PASS": # No child elements needed for passed tests pass elif status == "FAIL": failure_elem = ET.SubElement(testcase_elem, "failure") error_msg = result.get("error", "") failure_elem.set("message", _escape_cdata(error_msg[:500])) # Add detailed error text error_detail = f"Case: {case_id}\nScene: {scene}\nError: {error_msg}" failure_elem.text = _escape_cdata(error_detail) elif status == "ERROR": error_elem = ET.SubElement(testcase_elem, "error") error_msg = result.get("error", "") error_elem.set("message", _escape_cdata(error_msg[:500])) error_detail = f"Case: {case_id}\nScene: {scene}\nError: {error_msg}" error_elem.text = _escape_cdata(error_detail) elif status == "SKIP": skipped_elem = ET.SubElement(testcase_elem, "skipped") reason = result.get("skip_reason", "marked as skip") skipped_elem.set("message", _escape_cdata(reason)) # Write the XML tree = ET.ElementTree(testsuite_elem) if output_path is None: output_path = json_report_path.replace(".json", "-junit.xml") # Write with XML declaration with open(output_path, "w", encoding="utf-8") as f: f.write('\n') tree.write(f, encoding="unicode", xml_declaration=False) print(f"JUnit XML report: {output_path}") return output_path def main(): """CLI entry point.""" if len(sys.argv) < 2: print("Usage: python generate_junit.py [output_xml_path]") sys.exit(1) json_path = sys.argv[1] xml_path = sys.argv[2] if len(sys.argv) > 2 else None if not os.path.exists(json_path): print(f"Error: JSON report not found: {json_path}") sys.exit(1) output = json_to_junit(json_path, xml_path) print(f"Generated: {output}") if __name__ == "__main__": main()