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()