diff --git a/json_to_md.py b/json_to_md.py index 4d9348ebc..fdbdaba62 100644 --- a/json_to_md.py +++ b/json_to_md.py @@ -25,34 +25,14 @@ def normalize_nodeid(nodeid): return f"{file_part}#{func_part}" return None -def write_json_value(f, value, indent=0): - prefix = " " * indent - if isinstance(value, dict): - if not value: - f.write(f"{prefix}{{}}\n") - else: - for k, v in value.items(): - if isinstance(v, (dict, list)): - f.write(f"{prefix}{k}:\n") - write_json_value(f, v, indent + 1) - else: - # valeur simple, on écrit sur la même ligne - if v is None: - f.write(f"{prefix}{k}: None\n") - else: - f.write(f"{prefix}{k}: {v}\n") - elif isinstance(value, list): - if not value: - f.write(f"{prefix}[]\n") - else: - for item in value: - write_json_value(f, item, indent) - else: - if value is None: - f.write(f"{prefix}None\n") - else: - f.write(f"{prefix}{value}\n") - +def write_details(f, summary, body, level=0): + margin = 18 * level # 18px de marge/indent par niveau + border = f"border-left: 2px solid #eee;" if level > 0 else "" + f.write(f'
\n') + f.write(f"
\n{summary}\n\n") + f.write(body) + f.write("
\n") + f.write("
\n\n") def load_allure_metadata(allure_test_cases_dir): allure_data = {} @@ -98,6 +78,7 @@ def json_to_md_nested(json_path, md_path, allure_dir=None): 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"]: @@ -122,7 +103,8 @@ def json_to_md_nested(json_path, md_path, allure_dir=None): count = len(tests_by_status[status]) emoji = "✅" if status == "passed" else "❌" status_label = status.capitalize().replace('_', ' ') - f.write(f"
\n{emoji} {status_label} ({count})\n\n") + + body_status = "" grouped = defaultdict(lambda: defaultdict(list)) for test in tests_by_status[status]: @@ -133,51 +115,74 @@ def json_to_md_nested(json_path, md_path, allure_dir=None): grouped[filename][funcname].append(test) for filename, funcs in grouped.items(): - f.write(f"
\n📁 {filename}\n\n") + body_file = "" for funcname, tests in funcs.items(): - f.write(f"
\n🔧 Function: `{funcname}`\n\n") + body_func = "" for test in sorted(tests, key=lambda x: x['global_number']): - f.write(f"
\n{emoji} #{test['global_number']}\n\n") - + body_test = "" nodeid = test.get("nodeid", "") - f.write(f"- **Status:** {emoji} `{status}`\n") + body_test += f"- **Status:** {emoji} `{status}`\n" duration = test.get("call", {}).get("duration") - f.write(f"- **Duration:** `{duration:.6f}` s\n" if duration else "- **Duration:** `None`\n") + body_test += f"- **Duration:** `{duration:.6f}` s\n" if duration else "- **Duration:** `None`\n" full_name = normalize_nodeid(nodeid) allure_info = allure_data.get(full_name) if allure_info: if allure_info["parameters"]: - f.write("- **Parameters (Allure):**\n") + body_test += "- **Parameters (Allure):**\n" for param in allure_info["parameters"]: name = param.get("name") val = param.get("value") - f.write(f" - `{name}` = `{val}`\n") - f.write("\n") + body_test += f" - `{name}` = `{val}`\n" + body_test += "\n" if allure_info["severity"]: - f.write(f"- **Severity:** `{allure_info['severity']}`\n") + body_test += f"- **Severity:** `{allure_info['severity']}`\n" for phase in ['setup', 'call', 'teardown']: if phase in test: - f.write(f"\n### 🔧 {phase.capitalize()} Phase\n\n") + body_test += f"\n### 🔧 {phase.capitalize()} Phase\n\n" for field, value in test[phase].items(): if value is None: - f.write(f"- **{field.capitalize()}:** None\n") + body_test += f"- **{field.capitalize()}:** None\n" else: - f.write(f"
\n📌 {field.capitalize()}\n\n") - f.write("```\n") - write_json_value(f, value) - f.write("```\n") - f.write("
\n\n") - - f.write("
\n\n") - f.write("
\n\n") - f.write("
\n\n") - f.write("
\n\n") + phase_body = "```\n" + phase_body += stringify(value) + "\n" + phase_body += "```\n" + # détail du champ (champ imbriqué, donc niveau +1) + write_details( + f, + f"📌 {field.capitalize()}", + phase_body, + level=4 + ) + write_details( + f, + f"{emoji} #{test['global_number']}", + body_test, + level=3 + ) + write_details( + f, + f"🔧 Function: `{funcname}`", + "", # contenu injecté dans les tests + level=2 + ) + write_details( + f, + f"📁 {filename}", + "", # idem, contenu injecté dans les fonctions + level=1 + ) + write_details( + f, + f"{emoji} {status_label} ({count})", + "", # contenu injecté dans les fichiers + 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") @@ -188,8 +193,8 @@ def json_to_md_nested(json_path, md_path, allure_dir=None): for folder, collectors in grouped.items(): has_fail = any(c.get("outcome") != "passed" for c in collectors) folder_emoji = "✅" if not has_fail else "❌" - f.write(f"
\n{folder_emoji} {folder} ({len(collectors)} tests)\n\n") + body_collectors = "" outputs = [] for collector in collectors: outcome = collector.get("outcome", "unknown") @@ -204,9 +209,9 @@ def json_to_md_nested(json_path, md_path, allure_dir=None): ) + "\n```") if outputs: - f.write("### 🧾 Error or Result Summary\n\n") + body_collectors += "### 🧾 Error or Result Summary\n\n" for out in outputs: - f.write(out + "\n") + body_collectors += out + "\n" collectors_sorted = sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0]) for collector in collectors_sorted: @@ -214,40 +219,49 @@ def json_to_md_nested(json_path, md_path, allure_dir=None): emoji = "✅" if outcome == "passed" else "❌" nodeid = collector.get("nodeid", "unknown") short_node = nodeid.split("[")[0] - f.write(f"
\n{emoji} {short_node}\n\n") - f.write(f"- **Outcome:** `{outcome}`\n") + 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: - f.write("- **Details:**\n") - f.write("```\n") + body_coll += "- **Details:**\n" + body_coll += "```\n" for k, v in other_keys.items(): - f.write(f"{k}:\n") - try: - if v is None: - f.write(" None\n") - else: - write_json_value(f, v, indent=1) - except Exception as e: - f.write(f" \n") - f.write("\n") - f.write("```\n") + 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: - f.write("- **Details:** `None`\n") - - f.write("
\n\n") - - f.write("
\n\n") + body_coll += "- **Details:** `None`\n" + write_details( + f, + f"{emoji} {short_node}", + body_coll, + level=2 + ) + write_details( + f, + f"{folder_emoji} {folder} ({len(collectors)} tests)", + 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): - f.write(f"
\nWarning #{i}\n\n") - f.write("```\n") + warn_body = "```\n" for k, v in warning.items(): - f.write(f"{k}: {v}\n") - f.write("```\n") - f.write("
\n\n") + warn_body += f"{k}: {v}\n" + warn_body += "```\n" + write_details( + f, + f"Warning #{i}", + warn_body, + level=1 + ) def run_pytest_and_generate_banner_with_logs(md_path, log_path): exit_code = pytest.main(["tests/"]) @@ -321,8 +335,6 @@ def run_pytest_and_generate_banner_with_logs(md_path, log_path): except Exception as e: print(f"❌ Failed to update markdown report: {e}") - - def main(): parser = argparse.ArgumentParser(description="Convert JSON test results to Markdown.") parser.add_argument("--input", required=True, help="Path to pytest JSON file") @@ -331,15 +343,12 @@ def main(): 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") - args = parser.parse_args() json_to_md_nested(args.input, args.output, args.allure_dir) - run_pytest_and_generate_banner_with_logs(md_path=args.output, log_path=args.log) print(f"✅ Report generated at {args.output}") - if __name__ == "__main__": - main() \ No newline at end of file + main()