178 lines
6.6 KiB
Python
178 lines
6.6 KiB
Python
from collections import defaultdict
|
|
from datetime import datetime
|
|
import json
|
|
import argparse
|
|
import os
|
|
import re
|
|
import pytz
|
|
import traceback
|
|
|
|
def stringify(obj, indent=0):
|
|
"""Convertit un objet en chaîne formatée avec indentation"""
|
|
space = ' ' * indent
|
|
if obj is None or obj == "":
|
|
return "None"
|
|
if isinstance(obj, list):
|
|
if not obj:
|
|
return "[]"
|
|
return '\n'.join(f"{space}- {stringify(e, indent + 1)}" for e in obj)
|
|
if isinstance(obj, dict):
|
|
if not obj:
|
|
return "{}"
|
|
return '\n'.join(f"{space}{k}: {stringify(v, indent + 1)}" for k, v in obj.items())
|
|
return str(obj)
|
|
|
|
def sanitize_param_value(v):
|
|
"""Nettoie les valeurs pour le Markdown"""
|
|
return str(v).translate(str.maketrans({
|
|
'&': '^', '<': '(', '>': ')', '"': '', "'": '',
|
|
'`': '', '=': '≡', '\\': '/'
|
|
}))
|
|
|
|
def generate_test_details(test, runtime_params):
|
|
"""Génère les détails d'un test"""
|
|
details = []
|
|
nodeid = test.get("nodeid", "")
|
|
|
|
# Paramètres
|
|
callspec = runtime_params.get(nodeid, {})
|
|
if callspec:
|
|
params = callspec.get("params", {})
|
|
if params:
|
|
details.append("```python")
|
|
details.extend(f"{k} = {sanitize_param_value(v)}" for k, v in params.items())
|
|
details.append("```")
|
|
|
|
# Phases
|
|
for phase in ["setup", "call", "teardown"]:
|
|
if phase in test and test[phase]:
|
|
phase_info = test[phase]
|
|
details.append(f"**Phase {phase}**:")
|
|
details.append("```python")
|
|
for k, v in phase_info.items():
|
|
if v is not None:
|
|
details.append(f"{k}: {stringify(v)}")
|
|
details.append("```")
|
|
|
|
return "\n".join(details)
|
|
|
|
def json_to_md_nested(json_path, md_path, runtime_params=None):
|
|
"""Génère le rapport Markdown complet"""
|
|
runtime_params = runtime_params or {}
|
|
|
|
with open(json_path) as f:
|
|
data = json.load(f)
|
|
|
|
with open(md_path, 'w', encoding='utf-8') as f:
|
|
# En-tête
|
|
f.write("# 🧪 Rapport de tests\n\n")
|
|
f.write(f"*Généré le {datetime.now(pytz.timezone('Europe/Paris')).strftime('%Y-%m-%d %H:%M:%S')}*\n\n")
|
|
|
|
# Section Résumé
|
|
if 'summary' in data:
|
|
f.write("## 📋 Résumé\n")
|
|
for k, v in data['summary'].items():
|
|
f.write(f"- **{k}**: {v}\n")
|
|
f.write("\n")
|
|
|
|
# Section Tests
|
|
if "tests" in data and data["tests"]:
|
|
f.write("## 🔎 Tests\n\n")
|
|
|
|
# Numérotation et groupement
|
|
test_counter = 1
|
|
file_groups = defaultdict(list)
|
|
for test in data["tests"]:
|
|
test['num'] = test_counter
|
|
test_counter += 1
|
|
filename = test["nodeid"].split("::")[0].replace("tests/", "").replace("tests\\", "")
|
|
file_groups[filename].append(test)
|
|
|
|
# Par fichier
|
|
for filename, tests in file_groups.items():
|
|
f.write(f"- <details>\n")
|
|
f.write(f" <summary>📁 {filename}</summary>\n\n")
|
|
|
|
# Par fonction
|
|
func_groups = defaultdict(list)
|
|
for test in tests:
|
|
funcname = test["nodeid"].split("::")[1].split("[")[0]
|
|
func_groups[funcname].append(test)
|
|
|
|
for funcname, func_tests in func_groups.items():
|
|
f.write(f" - 🔧 {funcname}\n\n")
|
|
|
|
# Tests individuels
|
|
for test in func_tests:
|
|
status = "✅" if test['outcome'] == 'passed' else "❌"
|
|
f.write(f" - <details>\n")
|
|
f.write(f" <summary>➡️ Test #{test['num']} {status}</summary>\n\n")
|
|
|
|
details = generate_test_details(test, runtime_params)
|
|
if details:
|
|
f.write(f"{details}\n")
|
|
else:
|
|
f.write("Aucun détail disponible\n")
|
|
|
|
f.write(f" </details>\n\n")
|
|
|
|
f.write(f" </details>\n\n")
|
|
|
|
# Section Collecteurs
|
|
if "collectors" in data and data["collectors"]:
|
|
f.write("## 📚 Collecteurs\n\n")
|
|
for collector in data["collectors"]:
|
|
outcome = collector.get("outcome", "unknown")
|
|
emoji = "✅" if outcome == "passed" else "❌"
|
|
f.write(f"- {emoji} {collector.get('nodeid', 'unknown')}\n")
|
|
if outcome != "passed" and "result" in collector:
|
|
f.write(" ```\n")
|
|
for item in collector["result"]:
|
|
if isinstance(item, dict):
|
|
for k, v in item.items():
|
|
f.write(f" {k}: {v}\n")
|
|
else:
|
|
f.write(f" {item}\n")
|
|
f.write(" ```\n")
|
|
f.write("\n")
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--input", required=True, help="Fichier JSON d'entrée")
|
|
parser.add_argument("--output", required=True, help="Fichier Markdown de sortie")
|
|
parser.add_argument("--log", help="Fichier log pytest")
|
|
parser.add_argument("--params", help="Fichier de paramètres runtime")
|
|
parser.add_argument("--exit-code", type=int, default=0, help="Code de sortie pytest")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Chargement des paramètres
|
|
runtime_params = {}
|
|
if args.params and os.path.exists(args.params):
|
|
try:
|
|
with open(args.params) as f:
|
|
for entry in json.load(f):
|
|
if "nodeid" in entry and "callspec" in entry:
|
|
runtime_params[entry["nodeid"]] = entry["callspec"]
|
|
except Exception as e:
|
|
print(f"⚠️ Erreur lecture paramètres: {e}")
|
|
|
|
try:
|
|
json_to_md_nested(args.input, args.output, runtime_params)
|
|
|
|
# Ajout des logs si erreur
|
|
if args.log and os.path.exists(args.log) and args.exit_code != 0:
|
|
with open(args.log) as lf:
|
|
logs = lf.read()
|
|
with open(args.output, 'r+') as f:
|
|
content = f.read()
|
|
f.seek(0, 0)
|
|
f.write(f"## ⚠️ Logs d'exécution\n```\n{logs}\n```\n\n{content}")
|
|
|
|
print(f"✅ Rapport généré: {args.output}")
|
|
except Exception as e:
|
|
print(f"❌ Erreur: {str(e)}")
|
|
traceback.print_exc()
|
|
|
|
if __name__ == "__main__":
|
|
main() |