From d2fc314c971e8cc9446bdf5dc8ea085b6e6f58c1 Mon Sep 17 00:00:00 2001 From: tligui_y Date: Fri, 18 Jul 2025 00:19:50 +0200 Subject: [PATCH] Update json_to_md.py --- json_to_md.py | 223 +++++++++++--------------------------------------- 1 file changed, 48 insertions(+), 175 deletions(-) diff --git a/json_to_md.py b/json_to_md.py index 9f4cbaf8..152f1fb4 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 import pytz def stringify(obj, indent=0): @@ -48,42 +45,28 @@ def sanitize_param_value(v): .replace('=', '≡') .replace('\\', '/') ) - -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 get_details_block(summary, body, level=0, params_str=None, block_id=None, total_blocks=None): - """Version améliorée avec navigation""" - if params_str and params_str.strip() != "{}": - summary += f" [{params_str}]" +def get_clickable_block(summary, body, block_id, level=0): + """Version avec navigation cliquable (Précédent/Accueil)""" + indent = " " * level - body = body.strip() - - # Ajout de la navigation si block_id et total_blocks sont fournis - navigation = "" - if block_id is not None and total_blocks is not None: - nav_links = [] - if block_id > 1: - nav_links.append(f"[← Précédent](#block_{block_id-1})") - nav_links.append(f"[↑ Accueil](#top)") - if block_id < total_blocks: - nav_links.append(f"[→ Suivant](#block_{block_id+1})") - navigation = f"\n\n
\n{' | '.join(nav_links)}\n
" + # Navigation + nav_links = [] + if block_id > 1: + nav_links.append(f"[← Précédent](#block_{block_id-1})") + nav_links.append(f"[↑ Accueil](#top)") return ( - f"
\n" - f"{summary}\n\n" - f"{body}{navigation}\n\n" - f"
\n\n" + f"{indent}
\n" + f"{indent}

{summary}

\n" + f"{indent}
{body}
\n" + f"{indent}
\n" + f"{indent} {' | '.join(nav_links)}\n" + f"{indent}
\n" + f"{indent}
\n" ) -def make_test_block(test, status, emoji, level, runtime_params, block_id=None, total_blocks=None): +def make_test_block(test, status, emoji, level, runtime_params, block_id): nodeid = test.get("nodeid", "") body_test = "" summary_note = f"{emoji} #{test['global_number']}" @@ -93,11 +76,11 @@ def make_test_block(test, status, emoji, level, runtime_params, block_id=None, t if isinstance(params, dict) and params: param_display = ", ".join(f"{k}={sanitize_param_value(v)}" for k, v in params.items()) - summary_note += f" params: {param_display}" + summary_note += f" [{param_display}]" if callspec: callspec_block = "```python\n" + stringify(callspec) + "\n```" - body_test += get_details_block("📌 Runtime Parameters", callspec_block, level + 1) + body_test += callspec_block + "\n\n" skip_keys = {"nodeid"} phase_keys = [k for k in test.keys() if isinstance(test[k], dict) and k not in skip_keys] @@ -106,27 +89,27 @@ def make_test_block(test, status, emoji, level, runtime_params, block_id=None, t phase_body = "" for field, value in test[phase].items(): details_body = "```python\n" + stringify(value) + "\n```" if value is not None else "None" - phase_body += get_details_block(f"📌 {field.capitalize()}", details_body, level + 2) + phase_body += f"**{field.capitalize()}:**\n{details_body}\n\n" if phase_body: - body_test += f"\n### 🔧 {phase.capitalize()} Phase\n\n" + phase_body + body_test += f"### {phase.capitalize()} Phase\n{phase_body}" - return get_details_block(summary_note, body_test, level + 1, block_id=block_id, total_blocks=total_blocks) + return get_clickable_block(summary_note, body_test, block_id, level + 1) def json_to_md_nested(json_path, md_path, runtime_params=None): with open(json_path) as f: data = json.load(f) with open(md_path, 'w', encoding='utf-8') as f: - # Ajout du CSS et ancres + # En-tête avec CSS f.write(""" \n\n""") - + f.write(f"# 🧪 Test Report\n") local = pytz.timezone("Europe/Zurich") @@ -152,6 +135,7 @@ def json_to_md_nested(json_path, md_path, runtime_params=None): f.write(f"- **{key.capitalize()}**: {stringify(value)}\n") f.write("\n") + if "tests" in data and "summary" in data: test_counter = 1 for test in data["tests"]: @@ -160,6 +144,26 @@ def json_to_md_nested(json_path, md_path, runtime_params=None): f.write("## 🔎 Tests\n") + # Compter le nombre total de tests + total_tests = len(data["tests"]) + current_block = 1 + + for test in data["tests"]: + outcome = test.get("outcome", "unknown") + emoji = "✅" if outcome == "passed" else "❌" + + block = make_test_block( + test=test, + status=outcome, + emoji=emoji, + level=1, + runtime_params=runtime_params, + block_id=current_block + ) + f.write(block) + current_block += 1 + + 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]] @@ -173,113 +177,6 @@ def json_to_md_nested(json_path, md_path, runtime_params=None): total_blocks = sum(len(tests) for tests in tests_by_status.values()) current_block = 1 - for status in status_order: - if status not in tests_by_status: - continue - - count = len(tests_by_status[status]) - emoji = "✅" if status == "passed" else "❌" - status_label = status.capitalize().replace('_', ' ') - - 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, - runtime_params=runtime_params, - block_id=current_block, - total_blocks=total_blocks) - current_block += 1 - 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, block_id=None)) - - -def json_to_md_nested(json_path, md_path, runtime_params=None): - with open(json_path) as f: - data = json.load(f) - - with open(md_path, 'w', encoding='utf-8') as f: - f.write(f"# 🧪 Test Report\n") - - local = pytz.timezone("Europe/Zurich") - now = datetime.now(local) - f.write(f"*Generated on {now.strftime('%Y-%m-%d %H:%M:%S %Z')}*\n\n") - - general_info = {} - for k, v in data.items(): - if k == "summary": - break - if k not in {"created", "exitcode"}: - general_info[k] = v - - if general_info: - f.write("## 🧾 General Info\n") - for key, value in general_info.items(): - f.write(f"- **{key}**: {stringify(value)}\n") - f.write("\n") - - if 'summary' in data: - f.write("## 📋 Summary\n") - for key, value in data['summary'].items(): - f.write(f"- **{key.capitalize()}**: {stringify(value)}\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 - - f.write("## 🔎 Tests\n") - - 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]] - - tests_by_status = defaultdict(list) - for test in data["tests"]: - outcome = test.get("outcome", "unknown") - tests_by_status[outcome].append(test) - - for status in status_order: - if status not in tests_by_status: - continue - - count = len(tests_by_status[status]) - emoji = "✅" if status == "passed" else "❌" - status_label = status.capitalize().replace('_', ' ') - - 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, runtime_params=runtime_params) - 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") @@ -287,31 +184,7 @@ def json_to_md_nested(json_path, md_path, runtime_params=None): 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") - 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```") - - if outputs: - body_collectors += "### 🧾 Error or Result Summary\n\n" - for out in outputs: - body_collectors += out + "\n" + main collectors_sorted = sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0]) folder_body = ""