from collections import defaultdict from datetime import datetime import json import argparse import os import re def stringify(obj): if obj is None or obj == "": return "`None`" if isinstance(obj, list): return ', '.join(stringify(e) for e in obj) if isinstance(obj, dict): return ', '.join(f"`{k}: {stringify(v)}`" for k, v in obj.items()) return f"`{str(obj)}`" 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 load_allure_metadata(allure_test_cases_dir): allure_data = {} if not os.path.exists(allure_test_cases_dir): print(f"❌ Allure document untraceable : {allure_test_cases_dir}") return allure_data print(f"Chargement des fichiers Allure depuis : {allure_test_cases_dir}") for file in os.listdir(allure_test_cases_dir): if file.endswith(".json"): path = os.path.join(allure_test_cases_dir, file) with open(path, 'r', encoding='utf-8') as f: try: data = json.load(f) full_name = data.get("fullName") if not full_name: continue params = data.get("parameters", []) severity = data.get("extra", {}).get("severity", None) allure_data[full_name] = { "parameters": params, "severity": severity } param_vals = tuple(p["value"] for p in params) print(f"[✔️] Parsed: {full_name} {param_vals}") except Exception as e: print(f"❌ Erreur dans {file}: {e}") return allure_data def json_to_md_nested(json_path, md_path, allure_dir=None): with open(json_path) as f: data = json.load(f) allure_data = load_allure_metadata(allure_dir) if allure_dir else {} with open(md_path, 'w', encoding='utf-8') as f: f.write(f"# 🧪 Rapport de Tests\n") f.write(f"*Généré le {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n") if 'summary' in data: f.write("## 📋 Résumé\n") for key, value in data['summary'].items(): f.write(f"- **{key.capitalize()}**: {stringify(value)}\n") duration = data.get("duration") f.write(f"- **Durée totale**: `{duration:.3f}`s\n" if duration else "- **Durée totale**: `None`\n") f.write("\n") if "tests" in data: # Numérotation globale avant séparation all_tests = data["tests"] for idx, test in enumerate(all_tests, 1): test['global_index'] = idx f.write("## 🔎 Tests (Groupés par Statut)\n") # Récupérer l'ordre des statuts depuis le summary 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] if k not in ['collected', 'duration']] # Grouper les tests par statut tests_by_status = defaultdict(list) for test in all_tests: outcome = test.get("outcome", "unknown") tests_by_status[outcome].append(test) # Afficher dans l'ordre du summary 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('_', ' ') f.write(f"
\n{emoji} {status_label} ({count})\n\n") for test in tests_by_status[status]: f.write(f"
\n{emoji} #{test['global_index']}\n\n") nodeid = test.get("nodeid", "") f.write(f"- **Statut:** {emoji} `{status}`\n") duration = test.get("call", {}).get("duration") f.write(f"- **Durée:** `{duration:.6f}` s\n" if duration else "- **Durée:** `None`\n") # Partie Allure full_name = normalize_nodeid(nodeid) allure_info = allure_data.get(full_name) if allure_info: if allure_info["parameters"]: f.write("- **Paramètres (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") if allure_info["severity"]: f.write(f"- **Sévérité:** `{allure_info['severity']}`\n") if status != "passed": f.write("- **Détails d'erreur:**\n") f.write("
\nDétails complets\n\n") call_data = test.get("call", {}) if call_data: for key, value in call_data.items(): f.write(f"- **{key}:**\n") f.write("```\n") if isinstance(value, (dict, list)): f.write(f"{json.dumps(value, indent=2, ensure_ascii=False)}\n") else: f.write(f"{str(value)}\n") f.write("```\n\n") else: f.write("Aucun détail disponible\n") f.write("
\n") if "collectors" in data: f.write("## 📚 Collectés (Arborescence)\n") # Construire l'arborescence hiérarchique def build_tree(collectors): tree = {} for collector in collectors: nodeid = collector.get("nodeid", "") parts = [p for p in nodeid.replace("\\", "/").split("/") if p] current = tree for part in parts[:-1]: if part not in current: current[part] = {} current = current[part] if parts: last_part = parts[-1] if last_part not in current: current[last_part] = [] # Initialise comme liste if isinstance(current[last_part], list): current[last_part].append(collector) else: current[last_part] = [collector] # Convertit en liste si nécessaire return tree # Afficher l'arborescence récursivement def print_tree(node, level=0): indent = " " * level for key, value in node.items(): if isinstance(value, dict): f.write(f"{indent}
\n{indent}📁 {key}\n\n") print_tree(value, level+1) f.write(f"{indent}
\n\n") elif isinstance(value, list): for collector in value: outcome = collector.get("outcome", "unknown") emoji = "✅" if outcome == "passed" else "❌" f.write(f"{indent}
\n{indent}{emoji} {key}\n\n") f.write(f"{indent}- **outcome:** `{outcome}`\n") results = collector.get("result", []) if results: f.write(f"{indent}- **result:**\n```\n") for item in results: if isinstance(item, dict): for k, v in item.items(): f.write(f"{k}: {v}\n") f.write("\n") else: f.write(f"{item}\n") f.write("```\n") else: f.write(f"{indent}- **result:** `None`\n") f.write(f"{indent}
\n\n") collectors_tree = build_tree(data["collectors"]) print_tree(collectors_tree) def main(): parser = argparse.ArgumentParser(description="Convert JSON test results to Markdown.") parser.add_argument("--input", required=True, help="Chemin du fichier JSON pytest") parser.add_argument("--output", required=True, help="Chemin du fichier Markdown de sortie") parser.add_argument("--allure-dir", required=False, help="Dossier des test-cases Allure (facultatif)") args = parser.parse_args() json_to_md_nested(args.input, args.output, args.allure_dir) print(f"✅ Rapport généré dans {args.output}") if __name__ == "__main__": main()