232 lines
11 KiB
Python
232 lines
11 KiB
Python
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 "collectors" in data:
|
|
f.write("## 📚 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():
|
|
# Outcome global du dossier
|
|
has_fail = any(c.get("outcome") != "passed" for c in collectors)
|
|
folder_emoji = "✅" if not has_fail else "❌"
|
|
|
|
f.write(f"<details>\n<summary>{folder_emoji} {folder} ({len(collectors)} tests)</summary>\n\n")
|
|
|
|
# Résumé des outputs (optionnel : uniquement pour les failed ou all)
|
|
outputs = []
|
|
for collector in collectors:
|
|
outcome = collector.get("outcome", "unknown")
|
|
nodeid = collector.get("nodeid", "unknown")
|
|
short_node = nodeid.split("[")[0]
|
|
results = collector.get("result", [])
|
|
if outcome != "passed" and results:
|
|
outputs.append(f"### ❌ {short_node}\n```\n" + "\n".join(
|
|
f"{k}: {v}" if isinstance(item, dict) else str(item)
|
|
for item in results
|
|
for k, v in item.items() if isinstance(item, dict)
|
|
) + "\n```")
|
|
|
|
if outputs:
|
|
f.write("### 🧾 Résumé des erreurs ou résultats\n\n")
|
|
for out in outputs:
|
|
f.write(out + "\n")
|
|
|
|
# Détail complet de tous les tests (triés)
|
|
collectors_sorted = sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0])
|
|
for collector in collectors_sorted:
|
|
outcome = collector.get("outcome", "unknown")
|
|
emoji = "✅" if outcome == "passed" else "❌"
|
|
nodeid = collector.get("nodeid", "unknown")
|
|
short_node = nodeid.split("[")[0]
|
|
f.write(f"<details>\n<summary>{emoji} {short_node}</summary>\n\n")
|
|
f.write(f"- **Outcome:** `{outcome}`\n")
|
|
|
|
# Get all other keys except 'nodeid' and 'outcome'
|
|
other_keys = {k: v for k, v in collector.items() if k not in {"nodeid", "outcome"}}
|
|
if other_keys:
|
|
f.write("- **Details:**\n```\n")
|
|
for k, v in other_keys.items():
|
|
f.write(f"{k}: {v}\n")
|
|
f.write("```\n")
|
|
else:
|
|
f.write("- **Details:** `None`\n")
|
|
|
|
f.write("</details>\n\n")
|
|
|
|
f.write("</details>\n\n")
|
|
|
|
|
|
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 (Groupés par Statut)\n")
|
|
|
|
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]]
|
|
|
|
tests_by_status = defaultdict(list)
|
|
for test in data["tests"]:
|
|
outcome = test.get("outcome", "unknown")
|
|
tests_by_status[outcome].append(test)
|
|
|
|
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"<details>\n<summary>{emoji} {status_label} ({count})</summary>\n\n")
|
|
|
|
grouped = defaultdict(lambda: defaultdict(list))
|
|
for test in tests_by_status[status]:
|
|
nodeid = test.get("nodeid", "")
|
|
parts = nodeid.split("::")
|
|
filename = parts[0].replace("tests\\", "").replace("tests/", "")
|
|
funcname = parts[1].split("[")[0]
|
|
grouped[filename][funcname].append(test)
|
|
|
|
for filename, funcs in grouped.items():
|
|
f.write(f"<details>\n<summary>📁 {filename}</summary>\n\n")
|
|
for funcname, tests in funcs.items():
|
|
f.write(f"<details>\n<summary>🔧 Fonction: `{funcname}`</summary>\n\n")
|
|
for test in sorted(tests, key=lambda x: x['global_number']):
|
|
f.write(f"<details>\n<summary>{emoji} #{test['global_number']}</summary>\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")
|
|
|
|
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 'call' in test:
|
|
for field, value in test['call'].items():
|
|
if value is None: # Skip les valeurs vides
|
|
continue
|
|
|
|
# Début du bloc dépliable
|
|
f.write(f"<details>\n<summary>📌 {field.capitalize()}</summary>\n\n")
|
|
|
|
# Contenu dans une case grise
|
|
f.write("```\n")
|
|
if isinstance(value, dict):
|
|
for k, v in value.items():
|
|
f.write(f"{k}: {v}\n")
|
|
elif isinstance(value, list):
|
|
for item in value:
|
|
f.write(f"{item}\n")
|
|
else:
|
|
f.write(f"{value}\n")
|
|
f.write("```\n")
|
|
|
|
# Fermeture du bloc
|
|
f.write("</details>\n\n")
|
|
f.write("</details>\n\n")
|
|
f.write("</details>\n\n")
|
|
f.write("</details>\n\n")
|
|
f.write("</details>\n\n")
|
|
|
|
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() |