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 = ""