Files
slic/json_to_md.py
T
tligui_y 721a982aaa
Run Pytest with Allure and Coverage Reports / tests (push) Successful in 38s
Update json_to_md.py
2025-07-15 11:19:37 +02:00

328 lines
14 KiB
Python

from collections import defaultdict
from datetime import datetime
import json
import argparse
import os
import re
import pytest
from pytest import ExitCode
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 write_details(f, summary, body, level=0):
margin = 18 * level # 18px de marge/indent par niveau
border = f"border-left: 2px solid #eee;" if level > 0 else ""
f.write(f'<div style="margin-left: {margin}px; {border} padding-left: 8px;">\n')
f.write(f"<details>\n<summary>{summary}</summary>\n\n")
f.write(body)
f.write("</details>\n")
f.write("</div>\n\n")
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"Loading Allure files from: {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
}
except Exception as e:
print(f"❌ Error in {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 {}
def make_test_block(test, status, emoji, level):
nodeid = test.get("nodeid", "")
body_test = f"- **Status:** {emoji} `{status}`\n"
duration = test.get("call", {}).get("duration")
body_test += f"- **Duration:** `{duration:.6f}` s\n" if duration else "- **Duration:** `None`\n"
full_name = normalize_nodeid(nodeid)
allure_info = allure_data.get(full_name)
if allure_info:
if allure_info["parameters"]:
body_test += "- **Parameters (Allure):**\n"
for param in allure_info["parameters"]:
name = param.get("name")
val = param.get("value")
body_test += f" - `{name}` = `{val}`\n"
body_test += "\n"
if allure_info["severity"]:
body_test += f"- **Severity:** `{allure_info['severity']}`\n"
for phase in ['setup', 'call', 'teardown']:
if phase in test:
phase_body = ""
for field, value in test[phase].items():
if value is None:
phase_body += f"- **{field.capitalize()}:** None\n"
else:
details_body = "```\n"
details_body += stringify(value) + "\n"
details_body += "```\n"
phase_body += get_details_block(f"📌 {field.capitalize()}", details_body, level+2)
if phase_body:
body_test += f"\n### 🔧 {phase.capitalize()} Phase\n\n" + phase_body
return get_details_block(f"{emoji} #{test['global_number']}", body_test, level+1)
def get_details_block(summary, body, level=0):
margin = 18 * level
border = f"border-left: 2px solid #eee;" if level > 0 else ""
return (f'<div style="margin-left: {margin}px; {border} padding-left: 8px;">\n'
f"<details>\n<summary>{summary}</summary>\n\n"
f"{body}\n"
f"</details>\n"
f"</div>\n\n")
with open(md_path, 'w', encoding='utf-8') as f:
f.write(f"# 🧪 Test Report\n")
f.write(f"*Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n")
if 'summary' in data:
f.write("## 📋 Summary\n")
for key, value in data['summary'].items():
f.write(f"- **{key.capitalize()}**: {stringify(value)}\n")
duration = data.get("duration")
f.write(f"- **Total Duration**: `{duration:.3f}`s\n" if duration else "- **Total Duration**: `None`\n")
f.write("\n")
# --------- Tests section ----------
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")
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('_', ' ')
body_status = ""
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():
body_file = ""
for funcname, tests in funcs.items():
body_func = ""
for test in sorted(tests, key=lambda x: x['global_number']):
body_func += make_test_block(test, status, emoji, level=3)
body_file += get_details_block(f"🔧 Function: `{funcname}`", body_func, level=2)
body_status += get_details_block(f"📁 {filename}", body_file, level=1)
f.write(get_details_block(f"{emoji} {status_label} ({count})", body_status, level=0))
# ---------- Collectors section -----------
if "collectors" in data:
f.write("## 📚 Collected files\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 ""
body_collectors = ""
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:
body_collectors += "### 🧾 Error or Result Summary\n\n"
for out in outputs:
body_collectors += out + "\n"
collectors_sorted = sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0])
folder_body = ""
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]
body_coll = f"- **Outcome:** `{outcome}`\n"
other_keys = {k: v for k, v in collector.items() if k not in {"nodeid", "outcome"}}
if other_keys:
body_coll += "- **Details:**\n"
body_coll += "```\n"
for k, v in other_keys.items():
body_coll += f"{k}:\n"
if v is None:
body_coll += " None\n"
else:
body_coll += stringify(v) + "\n"
body_coll += "\n"
body_coll += "```\n"
else:
body_coll += "- **Details:** `None`\n"
folder_body += get_details_block(f"{emoji} {short_node}", body_coll, level=2)
f.write(get_details_block(f"{folder_emoji} {folder} ({len(collectors)} tests)", folder_body + body_collectors, level=1))
# ---------- Warnings section -----------
if 'warnings' in data and data['warnings']:
f.write("## ⚠️ Warnings\n\n")
for i, warning in enumerate(data['warnings'], 1):
warn_body = "```\n"
for k, v in warning.items():
warn_body += f"{k}: {v}\n"
warn_body += "```\n"
f.write(get_details_block(f"Warning #{i}", warn_body, level=1))
def run_pytest_and_generate_banner_with_logs(md_path, log_path):
exit_code = pytest.main(["tests/"])
if exit_code == 0 or exit_code == 1:
return
if exit_code == 2:
banner = (
"⚠️ **Test execution interrupted**\n\n"
"> The test run was interrupted by the user (reasons : KeyboardInterrupt or ...).\n\n"
)
elif exit_code == 3:
banner = (
"🛑 **Internal error during testing**\n\n"
"> An internal error occurred while executing the tests.\n\n"
)
elif exit_code == 4:
banner = (
"❗ **Pytest command line usage error**\n\n"
"> There was an error in how pytest was invoked.\n\n"
)
elif exit_code == 5:
banner = (
"❗ **No tests were collected**\n\n"
"> Pytest did not find any tests to run.\n\n"
)
else:
banner = (
f"❓ **Unknown pytest exit code: {exit_code}**\n\n"
"> Unexpected result during test execution.\n\n"
)
try:
with open(log_path, "r") as lf:
log_lines = lf.readlines()
except Exception as e:
print(f"❌ Could not read log file: {e}")
return
short_summary_lines = []
in_summary = False
for line in log_lines:
if "short test summary info" in line.lower():
in_summary = True
if in_summary:
short_summary_lines.append(line)
if re.match(r"=+.* in .*s =+", line):
break
try:
with open(md_path, "r") as f:
original_md = f.read()
except Exception as e:
print(f"❌ Could not read markdown report: {e}")
return
full_banner = (
banner +
"<details>\n<summary>📋 Short test summary info</summary>\n\n" +
"```\n" + "".join(short_summary_lines) + "```\n</details>\n\n" +
"<details>\n<summary>🪵 Full raw pytest log</summary>\n\n" +
"```\n" + "".join(log_lines) + "```\n</details>\n\n" +
"---\n\n"
)
try:
with open(md_path, "w") as f:
f.write(full_banner + original_md)
print("✅ Banner and log summary added to markdown report.")
except Exception as e:
print(f"❌ Failed to update markdown report: {e}")
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("--allure-dir", required=False, help="Directory of Allure test-cases (optional)")
parser.add_argument("--log", required=False, help="Path to raw pytest output log (optional)")
parser.add_argument("--json", required=False, help="Path to pytest-report.json")
args = parser.parse_args()
json_to_md_nested(args.input, args.output, args.allure_dir)
run_pytest_and_generate_banner_with_logs(md_path=args.output, log_path=args.log)
print(f"✅ Report generated at {args.output}")
if __name__ == "__main__":
main()