diff --git a/json_to_md.py b/json_to_md.py
index 71b12c79..002349a5 100644
--- a/json_to_md.py
+++ b/json_to_md.py
@@ -8,17 +8,20 @@ import pytz
def stringify(obj, indent=0):
space = ' ' * indent
+
if obj is None or obj == "":
return "None"
+
if isinstance(obj, list):
if not obj:
return "[]"
return '\n'.join(
f"{space}- (vide)" if not e or (isinstance(e, dict) and not e)
else f"{space}- {stringify(e, indent + 1)}" if not isinstance(e, dict)
- else f"{space}- {stringify(e, indent + 1)}"
+ else f"{space}- {stringify(e, indent + 1)}" # ← plus de \n !
for e in obj
)
+
if isinstance(obj, dict):
if not obj:
return "{}"
@@ -27,6 +30,7 @@ def stringify(obj, indent=0):
else f"{space}{k}:\n{stringify(v, indent + 1)}"
for k, v in obj.items()
)
+
return str(obj)
def sanitize_param_value(v):
@@ -35,92 +39,157 @@ def sanitize_param_value(v):
s.replace('&', '^')
.replace('<', '(')
.replace('>', ')')
- .replace('"', '')
- .replace("'", '')
- .replace('`', '')
- .replace('=', '≡')
- .replace('\\', '/')
+ .replace('"', '')
+ .replace("'", '')
+ .replace('`', '')
+ .replace('=', '≡')
+ .replace('\\', '/')
)
-def make_test_block(test, status, emoji, level, runtime_params):
+def make_test_details(test, level, runtime_params):
nodeid = test.get("nodeid", "")
- indent = " " * level
- output = ""
-
- summary_note = f"{emoji} Test #{test['global_number']}"
-
+ details = []
+
+ # Ajouter les paramètres d'exécution s'ils existent
callspec = runtime_params.get(nodeid, {})
- params = callspec.get("params", {}) if isinstance(callspec, dict) else {}
- param_display = ""
- 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})"
-
- output += f"{indent}- \n"
- output += f"{indent} {summary_note}
\n\n"
-
- # Runtime parameters block
if callspec:
- callspec_block = "```python\n" + stringify(callspec) + "\n```"
- output += f"{indent} - \n"
- output += f"{indent} 📌 Runtime Parameters
\n\n"
- output += f"{callspec_block}\n\n"
- output += f"{indent} \n"
-
+ params_str = "```python\n" + stringify(callspec) + "\n```"
+ details.append(f" - **Paramètres**:\n{params_str}")
+
+ # Ajouter les phases (setup, call, teardown)
skip_keys = {"nodeid"}
phase_keys = [k for k in test.keys() if isinstance(test[k], dict) and k not in skip_keys]
+
for phase in phase_keys:
- output += f"{indent} ### 🔧 {phase.capitalize()} Phase\n"
+ phase_details = []
for field, value in test[phase].items():
- details_body = "```python\n" + stringify(value) + "\n```" if value is not None else "None"
- output += f"{indent} - \n"
- output += f"{indent} 📌 {field.capitalize()}
\n\n"
- output += f"{details_body}\n\n"
- output += f"{indent} \n"
-
- output += f"{indent} \n"
- return output
+ if value is not None:
+ phase_details.append(f" - **{field}**:\n ```python\n{stringify(value)}\n ```")
+
+ if phase_details:
+ details.append(f" - **Phase {phase}**:\n" + "\n".join(phase_details))
+
+ return "\n".join(details)
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")
-
+ f.write("# 🧪 Rapport de tests\n\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")
+ f.write(f"*Généré le {now.strftime('%Y-%m-%d %H:%M:%S %Z')}*\n\n")
- f.write("## 🔎 Tests\n")
+ # Section Informations générales
+ general_info = {}
+ for k, v in data.items():
+ if k == "summary":
+ break
+ if k not in {"created", "exitcode"}:
+ general_info[k] = v
- test_counter = 1
- for test in data.get("tests", []):
- test['global_number'] = test_counter
- test_counter += 1
+ if general_info:
+ f.write("## 🧾 Informations générales\n")
+ for key, value in general_info.items():
+ f.write(f"- **{key}**: {stringify(value)}\n")
+ f.write("\n")
- grouped = defaultdict(lambda: defaultdict(list))
- for test in data.get("tests", []):
- nodeid = test.get("nodeid", "")
- parts = nodeid.split("::")
- filename = parts[0].replace("tests\\", "").replace("tests/", "")
- funcname = parts[1].split("[")[0]
- grouped[filename][funcname].append(test)
+ # Section Résumé
+ if 'summary' in data:
+ f.write("## 📋 Résumé\n")
+ for key, value in data['summary'].items():
+ f.write(f"- **{key.capitalize()}**: {stringify(value)}\n")
+ f.write("\n")
- for filename, funcs in grouped.items():
- f.write(f"\n📁 {filename}
\n\n")
- for funcname, tests in funcs.items():
- f.write(f"- 🔧 {funcname}\n\n")
- for test in tests:
- test_block = make_test_block(test, test.get("outcome"), "➡️", level=2, runtime_params=runtime_params)
- f.write(test_block + "\n")
- f.write(" \n\n")
+ # Section Tests
+ 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")
+
+ # Grouper les tests par statut
+ tests_by_status = defaultdict(list)
+ for test in data["tests"]:
+ outcome = test.get("outcome", "unknown")
+ tests_by_status[outcome].append(test)
+
+ # Grouper les tests par fichier et fonction
+ grouped = defaultdict(lambda: defaultdict(list))
+ for test in data["tests"]:
+ nodeid = test.get("nodeid", "")
+ parts = nodeid.split("::")
+ filename = parts[0].replace("tests\\", "").replace("tests/", "")
+ funcname = parts[1].split("[")[0] if len(parts) > 1 else "unknown"
+ grouped[filename][funcname].append(test)
+
+ # Générer le rapport par fichier
+ for filename, funcs in grouped.items():
+ f.write(f"- \n 📁 {filename}
\n\n")
+
+ for funcname, tests in funcs.items():
+ f.write(f" - 🔧 {funcname}\n\n")
+
+ for test in sorted(tests, key=lambda x: x['global_number']):
+ outcome = test.get("outcome", "unknown")
+ emoji = "✅" if outcome == "passed" else "❌"
+ duration = test.get("duration", 0)
+
+ # Header du test
+ f.write(f" - \n {emoji} Test #{test['global_number']}
\n\n")
+
+ # Corps du test
+ test_details = make_test_details(test, level=3, runtime_params=runtime_params)
+ if test_details:
+ f.write(test_details + "\n")
+
+ f.write(" \n\n")
+
+ f.write(" \n\n")
+
+ # Section Collecteurs
+ if "collectors" in data:
+ f.write("## 📚 Fichiers collectés\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 "❌"
+
+ f.write(f"- \n {folder_emoji} {folder}
\n\n")
+
+ for collector in sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0]):
+ outcome = collector.get("outcome", "unknown")
+ 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" - **Statut**: `{outcome}`\n")
+
+ other_keys = {k: v for k, v in collector.items() if k not in {"nodeid", "outcome"}}
+ if other_keys:
+ for k, v in other_keys.items():
+ f.write(f" - **{k}**:\n ```python\n{stringify(v)}\n ```\n")
+
+ f.write(" \n\n")
+
+ f.write(" \n\n")
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("--params", required=False, help="Path to runtime-params.json")
+ parser = argparse.ArgumentParser(description="Convertir les résultats de tests JSON en Markdown.")
+ parser.add_argument("--input", required=True, help="Chemin vers le fichier JSON pytest")
+ parser.add_argument("--output", required=True, help="Chemin vers le fichier Markdown de sortie")
+ parser.add_argument("--params", required=False, help="Chemin vers runtime-params.json")
args = parser.parse_args()
@@ -133,10 +202,10 @@ def main():
if "nodeid" in entry and isinstance(entry.get("callspec"), dict):
runtime_params[entry["nodeid"]] = entry["callspec"]
except Exception as e:
- print(f"❌ Failed to read runtime parameters: {e}")
-
+ print(f"❌ Échec de lecture des paramètres d'exécution: {e}")
+
json_to_md_nested(args.input, args.output, runtime_params)
- print(f"✅ Report generated at {args.output}")
+ print(f"✅ Rapport généré à {args.output}")
if __name__ == "__main__":
- main()
+ main()
\ No newline at end of file