From de2604fda09c68c80da267e174e717bb50d02d47 Mon Sep 17 00:00:00 2001 From: tligui_y Date: Wed, 16 Jul 2025 01:22:14 +0200 Subject: [PATCH] Update json_to_md.py --- json_to_md.py | 262 +++++++++++++++++--------------------------------- 1 file changed, 88 insertions(+), 174 deletions(-) diff --git a/json_to_md.py b/json_to_md.py index f4bb8ed2..9f6d189b 100644 --- a/json_to_md.py +++ b/json_to_md.py @@ -4,9 +4,6 @@ import json import argparse import os import re -import pytest -from pytest import ExitCode -import traceback def stringify(obj): if obj is None or obj == "": @@ -17,18 +14,7 @@ def stringify(obj): return ', '.join(f"`{k}: {stringify(v)}`" for k, v in obj.items()) return f"`{str(obj)}`" -def normalize_nodeid(nodeid): - """Convert pytest nodeid to Allure fullName format""" - match = re.match(r"(tests[/\\].+?)\.py::(.+?)(?:\[.*)?$", nodeid) - if match: - file_part = match.group(1).replace("/", ".").replace("\\", ".") - func_part = match.group(2) - return f"{file_part}#{func_part}" - return None - def extract_param_str_from_nodeid(nodeid): - """Extract parameter string from nodeid, e.g. test[x-y-z] → x-y-z. - Handles nested or inner brackets.""" first = nodeid.find('[') last = nodeid.rfind(']') if first != -1 and last != -1 and last > first: @@ -38,7 +24,6 @@ def extract_param_str_from_nodeid(nodeid): def get_details_block(summary, body, level=0, params_str=None): margin = 18 * level border = f"border-left: 2px solid #eee;" if level > 0 else "" - # Affiche les paramètres dans le résumé si présent if params_str: summary = f"{summary} parameters: [{params_str}]" return (f'
\n' @@ -47,33 +32,23 @@ def get_details_block(summary, body, level=0, params_str=None): f"\n" f"
\n\n") - - def make_test_block(test, status, emoji, level): - nodeid = test.get("nodeid", "") - param_str = extract_param_str_from_nodeid(nodeid) + keys = list(test.keys()) + first_key = keys[0] if keys else "?" + param_str = extract_param_str_from_nodeid(test.get(first_key, "")) body_test = f"- **Status:** {emoji} `{status}`\n" duration = test.get("call", {}).get("duration") body_test += f"- **Duration:** `{duration:.6f}` s\n" if duration else "- **Duration**: `None`\n" - if param_str: body_test += f"- **Parameters:** `{param_str}`\n" - - for phase in ['setup', 'call', 'teardown']: - if phase in test: + for section in test: + if section not in {"outcome", "global_number"}: phase_body = "" - for field, value in test[phase].items(): - if value is None: - phase_body += f"- **{field.capitalize()}:** None\n" - else: - details_body = "```\n" - details_body += stringify(value) + "\n" - details_body += "```\n" - phase_body += get_details_block(f"📌 {field.capitalize()}", details_body, level+2) + for field, value in test[section].items(): + details_body = "```\n" + stringify(value) + "\n```\n" + phase_body += get_details_block(f"📌 {field}", details_body, level+2) if phase_body: - body_test += f"\n### 🔧 {phase.capitalize()} Phase\n\n" + phase_body - - # Ajoute param_str au résumé si dispo + body_test += f"\n### 🔧 {section.capitalize()}\n\n" + phase_body return get_details_block(f"{emoji} #{test['global_number']}", body_test, level+1, params_str=param_str) def json_to_md_nested(json_path, md_path): @@ -84,158 +59,98 @@ def json_to_md_nested(json_path, md_path): f.write(f"# 🧪 Test Report\n") f.write(f"*Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n") + # Metadata before summary + for key, value in data.items(): + if key == "summary": + break + f.write(f"- **{key}**: {stringify(value)}\n") + f.write("\n") + + # Summary if 'summary' in data: f.write("## 📋 Summary\n") for key, value in data['summary'].items(): f.write(f"- **{key.capitalize()}**: {stringify(value)}\n") - duration = data.get("duration") - f.write(f"- **Total Duration**: `{duration:.3f}`s\n" if duration else "- **Total Duration**: `None`\n") f.write("\n") - # --------- Tests section ---------- - if "tests" in data and "summary" in data: - test_counter = 1 - for test in data["tests"]: - test['global_number'] = test_counter - test_counter += 1 + keys_after_summary = list(data.keys())[list(data.keys()).index("summary") + 1:] - f.write("## 🔎 Tests\n") + for section in keys_after_summary: + section_data = data[section] - summary_items = list(data["summary"].items()) - total_index = next((i for i, (k, _) in enumerate(summary_items) if k == "total"), len(summary_items)) - status_order = [k for k, _ in summary_items[:total_index]] + if section == "tests": + f.write("## 🔎 Tests\n") + test_counter = 1 + tests_by_status = defaultdict(list) + for test in section_data: + test['global_number'] = test_counter + test_counter += 1 + outcome = test.get("outcome", "unknown") + tests_by_status[outcome].append(test) - tests_by_status = defaultdict(list) - for test in data["tests"]: - outcome = test.get("outcome", "unknown") - tests_by_status[outcome].append(test) + for status in sorted(tests_by_status): + tests = tests_by_status[status] + emoji = "✅" if status == "passed" else "❌" + body_status = "" + grouped = defaultdict(lambda: defaultdict(list)) + for test in tests: + keys = list(test.keys()) + nodeid = test[keys[0]] + parts = nodeid.split("::") + filename = parts[0].replace("tests\\", "").replace("tests/", "") + funcname = parts[1].split("[")[0] if len(parts) > 1 else "?" + grouped[filename][funcname].append(test) - for status in status_order: - if status not in tests_by_status: - continue + for filename, funcs in grouped.items(): + body_file = "" + for funcname, tests in funcs.items(): + body_func = "" + for test in sorted(tests, key=lambda x: x['global_number']): + body_func += make_test_block(test, status, emoji, level=3) + body_file += get_details_block(f"🔧 Function: `{funcname}`", body_func, level=2) + body_status += get_details_block(f"📁 {filename}", body_file, level=1) - count = len(tests_by_status[status]) - emoji = "✅" if status == "passed" else "❌" - status_label = status.capitalize().replace('_', ' ') + f.write(get_details_block(f"{emoji} {status.capitalize()} ({len(tests)})", body_status, level=0)) - body_status = "" - grouped = defaultdict(lambda: defaultdict(list)) - for test in tests_by_status[status]: - nodeid = test.get("nodeid", "") - parts = nodeid.split("::") - filename = parts[0].replace("tests\\", "").replace("tests/", "") - funcname = parts[1].split("[")[0] - grouped[filename][funcname].append(test) - - for filename, funcs in grouped.items(): - body_file = "" - for funcname, tests in funcs.items(): - body_func = "" - for test in sorted(tests, key=lambda x: x['global_number']): - body_func += make_test_block(test, status, emoji, level=3) - body_file += get_details_block(f"🔧 Function: `{funcname}`", body_func, level=2) - body_status += get_details_block(f"📁 {filename}", body_file, level=1) - f.write(get_details_block(f"{emoji} {status_label} ({count})", body_status, level=0)) - - # ---------- Collectors section ----------- - if "collectors" in data: - f.write("## 📚 Collected files\n") - grouped = defaultdict(list) - for collector in data["collectors"]: - nodeid = collector.get("nodeid", "unknown") - path = nodeid.split("::")[0] - main_folder = path.split("/")[0] if "/" in path else path - grouped[main_folder].append(collector) - - for folder, collectors in grouped.items(): - has_fail = any(c.get("outcome") != "passed" for c in collectors) - folder_emoji = "✅" if not has_fail else "❌" - - body_collectors = "" - outputs = [] - for collector in collectors: - outcome = collector.get("outcome", "unknown") + elif section == "collectors": + f.write("## 📚 Collected files\n") + grouped = defaultdict(list) + for collector in section_data: nodeid = collector.get("nodeid", "unknown") - short_node = nodeid.split("[")[0] - results = collector.get("result", []) - if outcome != "passed" and results: - outputs.append(f"### ❌ {short_node}\n```\n" + "\n".join( - f"{k}: {v}" if isinstance(item, dict) else str(item) - for item in results - for k, v in item.items() if isinstance(item, dict) - ) + "\n```") + path = nodeid.split("::")[0] + folder = path.split("/")[0] if "/" in path else path + grouped[folder].append(collector) - if outputs: - body_collectors += "### 🧾 Error or Result Summary\n\n" - for out in outputs: - body_collectors += out + "\n" + for folder, collectors in grouped.items(): + folder_emoji = "✅" if all(c.get("outcome") == "passed" for c in collectors) else "❌" + folder_body = "" + for i, collector in enumerate(collectors): + coll_body = "" + for k, v in collector.items(): + coll_body += get_details_block(k, f"```\n{stringify(v)}\n```", level=2) + folder_body += get_details_block(f"Collector #{i+1}", coll_body, level=1) + f.write(get_details_block(f"{folder_emoji} {folder} ({len(collectors)} entries)", folder_body, level=0)) - collectors_sorted = sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0]) - folder_body = "" - for collector in collectors_sorted: - outcome = collector.get("outcome", "unknown") - emoji = "✅" if outcome == "passed" else "❌" - nodeid = collector.get("nodeid", "unknown") - short_node = nodeid.split("[")[0] - - body_coll = f"- **Outcome:** `{outcome}`\n" - other_keys = {k: v for k, v in collector.items() if k not in {"nodeid", "outcome"}} - if other_keys: - body_coll += "- **Details:**\n" - body_coll += "```\n" - for k, v in other_keys.items(): - body_coll += f"{k}:\n" - if v is None: - body_coll += " None\n" - else: - body_coll += stringify(v) + "\n" - body_coll += "\n" - body_coll += "```\n" - else: - body_coll += "- **Details:** `None`\n" - folder_body += get_details_block(f"{emoji} {short_node}", body_coll, level=2) - f.write(get_details_block(f"{folder_emoji} {folder} ({len(collectors)} tests)", folder_body + body_collectors, level=1)) - - # ---------- Warnings section ----------- - if 'warnings' in data and data['warnings']: - f.write("## ⚠️ Warnings\n\n") - for i, warning in enumerate(data['warnings'], 1): - warn_body = "```\n" - for k, v in warning.items(): - warn_body += f"{k}: {v}\n" - warn_body += "```\n" - f.write(get_details_block(f"Warning #{i}", warn_body, level=1)) + else: + f.write(f"## 📦 {section.capitalize()}\n\n") + entries = section_data if isinstance(section_data, list) else [section_data] + for i, item in enumerate(entries): + block = "" + for k, v in item.items(): + block += get_details_block(k, f"```\n{stringify(v)}\n```", level=2) + f.write(get_details_block(f"{section.capitalize()} #{i+1}", block, level=1)) def run_pytest_and_generate_banner_with_logs(md_path, log_path, exit_code): - #exit_code = pytest.main(["tests/"]) - if exit_code == 0 or exit_code == 1: + if exit_code in (0, 1): return - if exit_code == 2: - banner = ( - "⚠️ **Test execution interrupted**\n\n" - "> The test run was interrupted by the user (reasons : KeyboardInterrupt or ...).\n\n" - ) - elif exit_code == 3: - banner = ( - "🛑 **Internal error during testing**\n\n" - "> An internal error occurred while executing the tests.\n\n" - ) - elif exit_code == 4: - banner = ( - "❗ **Pytest command line usage error**\n\n" - "> There was an error in how pytest was invoked.\n\n" - ) - elif exit_code == 5: - banner = ( - "❗ **No tests were collected**\n\n" - "> Pytest did not find any tests to run.\n\n" - ) - else: - banner = ( - f"❓ **Unknown pytest exit code: {exit_code}**\n\n" - "> Unexpected result during test execution.\n\n" - ) + banners = { + 2: "⚠️ **Test execution interrupted**\n\n> The test run was interrupted by the user.", + 3: "🛑 **Internal error during testing**\n\n> An internal error occurred while executing the tests.", + 4: "❗ **Pytest command line usage error**\n\n> There was an error in how pytest was invoked.", + 5: "❗ **No tests were collected**\n\n> Pytest did not find any tests to run." + } + banner = banners.get(exit_code, f"❓ **Unknown pytest exit code: {exit_code}**\n\n> Unexpected result during test execution.") try: with open(log_path, "r") as lf: @@ -262,27 +177,26 @@ def run_pytest_and_generate_banner_with_logs(md_path, log_path, exit_code): return full_banner = ( - banner + + "\n---\n\n" + + banner + "\n\n" + "
\n📋 Short test summary info\n\n" + "```\n" + "".join(short_summary_lines) + "```\n
\n\n" + "
\n🪵 Full raw pytest log\n\n" + - "```\n" + "".join(log_lines) + "```\n
\n\n" + - "---\n\n" + "```\n" + "".join(log_lines) + "```\n\n\n" ) try: with open(md_path, "w") as f: - f.write(full_banner + original_md) + f.write(original_md + full_banner) print("✅ Banner and log summary added to markdown report.") except Exception as e: print(f"❌ Failed to update markdown report: {e}") - return - + def main(): parser = argparse.ArgumentParser(description="Convert JSON test results to Markdown.") parser.add_argument("--input", required=True, help="Path to pytest JSON file") parser.add_argument("--output", required=True, help="Path to output Markdown file") - parser.add_argument("--log", required=False, help="Path to raw pytest output log (optional)") + parser.add_argument("--log", required=False, help="Path to raw pytest output log (optional)") parser.add_argument("--json", required=False, help="Path to pytest-report.json") parser.add_argument("--exit-code", type=int, default=0, help="Exit code from pytest to determine the banner.")