Files
slic/json_to_md.py
T
tligui_y a4527b3747
Run Pytest with Allure and Coverage Reports / tests (push) Successful in 51s
Run Pytest with Allure and Coverage Reports / deploy (push) Has been skipped
Update json_to_md.py
2025-07-10 17:02:55 +02:00

263 lines
12 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")
from collections import defaultdict
if "collectors" in data:
f.write("## 📚 Collectés\n")
# Étape 1 : regrouper hiérarchiquement
root_group = defaultdict(list) # 'ci-reports' -> list of collectors
subgroups = defaultdict(lambda: defaultdict(list)) # 'ci-reports' -> 'allure' -> list of collectors
for collector in data["collectors"]:
nodeid = collector.get("nodeid", "unknown")
path = nodeid.split("::")[0]
parts = path.split("/")
if len(parts) == 1:
root_group[parts[0]].append(collector)
elif len(parts) >= 2:
root, sub = parts[0], parts[1]
subgroups[root][sub].append(collector)
else:
root_group["unknown"].append(collector)
# Étape 2 : afficher les groupes (ex: ci-reports)
for root in sorted(root_group.keys()):
collectors = root_group[root]
children = subgroups.get(root, {})
total = len(collectors) + sum(len(v) for v in children.values())
has_fail = any(c.get("outcome") != "passed" for c in collectors) or any(
any(c.get("outcome") != "passed" for c in v) for v in children.values())
emoji = "" if not has_fail else ""
f.write(f"<details open>\n<summary>{emoji} 📁 `{root}` ({total} tests)</summary>\n\n")
# 1. Tests directs dans root (ex: ci-reports/testA)
for c in collectors:
outcome = c.get("outcome", "unknown")
icon = "" if outcome == "passed" else ""
nodeid = c.get("nodeid", "unknown")
short = nodeid.split("[")[0]
f.write(f"<details>\n<summary>{icon} `{short}`</summary>\n\n")
f.write(f"- **outcome:** `{outcome}`\n")
results = c.get("result", [])
if results:
f.write("- **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("- **result:** `None`\n")
f.write("</details>\n\n")
# 2. Sous-dossiers triés (ex: allure, coverage)
for sub in sorted(children.keys()):
sub_collectors = children[sub]
sub_fail = any(c.get("outcome") != "passed" for c in sub_collectors)
sub_emoji = "" if not sub_fail else ""
f.write(f"<details>\n<summary>{sub_emoji} 📁 `{root}/{sub}` ({len(sub_collectors)} tests)</summary>\n\n")
for c in sub_collectors:
outcome = c.get("outcome", "unknown")
icon = "" if outcome == "passed" else ""
nodeid = c.get("nodeid", "unknown")
short = nodeid.split("[")[0]
f.write(f"<details>\n<summary>{icon} `{short}`</summary>\n\n")
f.write(f"- **outcome:** `{outcome}`\n")
results = c.get("result", [])
if results:
f.write("- **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("- **result:** `None`\n")
f.write("</details>\n\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()