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