This commit is contained in:
+94
-85
@@ -25,34 +25,14 @@ def normalize_nodeid(nodeid):
|
||||
return f"{file_part}#{func_part}"
|
||||
return None
|
||||
|
||||
def write_json_value(f, value, indent=0):
|
||||
prefix = " " * indent
|
||||
if isinstance(value, dict):
|
||||
if not value:
|
||||
f.write(f"{prefix}{{}}\n")
|
||||
else:
|
||||
for k, v in value.items():
|
||||
if isinstance(v, (dict, list)):
|
||||
f.write(f"{prefix}{k}:\n")
|
||||
write_json_value(f, v, indent + 1)
|
||||
else:
|
||||
# valeur simple, on écrit sur la même ligne
|
||||
if v is None:
|
||||
f.write(f"{prefix}{k}: None\n")
|
||||
else:
|
||||
f.write(f"{prefix}{k}: {v}\n")
|
||||
elif isinstance(value, list):
|
||||
if not value:
|
||||
f.write(f"{prefix}[]\n")
|
||||
else:
|
||||
for item in value:
|
||||
write_json_value(f, item, indent)
|
||||
else:
|
||||
if value is None:
|
||||
f.write(f"{prefix}None\n")
|
||||
else:
|
||||
f.write(f"{prefix}{value}\n")
|
||||
|
||||
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 = {}
|
||||
@@ -98,6 +78,7 @@ def json_to_md_nested(json_path, md_path, allure_dir=None):
|
||||
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"]:
|
||||
@@ -122,7 +103,8 @@ def json_to_md_nested(json_path, md_path, allure_dir=None):
|
||||
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")
|
||||
|
||||
body_status = ""
|
||||
|
||||
grouped = defaultdict(lambda: defaultdict(list))
|
||||
for test in tests_by_status[status]:
|
||||
@@ -133,51 +115,74 @@ def json_to_md_nested(json_path, md_path, allure_dir=None):
|
||||
grouped[filename][funcname].append(test)
|
||||
|
||||
for filename, funcs in grouped.items():
|
||||
f.write(f"<details>\n<summary>📁 {filename}</summary>\n\n")
|
||||
body_file = ""
|
||||
for funcname, tests in funcs.items():
|
||||
f.write(f"<details>\n<summary>🔧 Function: `{funcname}`</summary>\n\n")
|
||||
body_func = ""
|
||||
for test in sorted(tests, key=lambda x: x['global_number']):
|
||||
f.write(f"<details>\n<summary>{emoji} #{test['global_number']}</summary>\n\n")
|
||||
|
||||
body_test = ""
|
||||
nodeid = test.get("nodeid", "")
|
||||
f.write(f"- **Status:** {emoji} `{status}`\n")
|
||||
body_test += f"- **Status:** {emoji} `{status}`\n"
|
||||
duration = test.get("call", {}).get("duration")
|
||||
f.write(f"- **Duration:** `{duration:.6f}` s\n" if duration else "- **Duration:** `None`\n")
|
||||
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"]:
|
||||
f.write("- **Parameters (Allure):**\n")
|
||||
body_test += "- **Parameters (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")
|
||||
body_test += f" - `{name}` = `{val}`\n"
|
||||
body_test += "\n"
|
||||
if allure_info["severity"]:
|
||||
f.write(f"- **Severity:** `{allure_info['severity']}`\n")
|
||||
body_test += f"- **Severity:** `{allure_info['severity']}`\n"
|
||||
|
||||
for phase in ['setup', 'call', 'teardown']:
|
||||
if phase in test:
|
||||
f.write(f"\n### 🔧 {phase.capitalize()} Phase\n\n")
|
||||
body_test += f"\n### 🔧 {phase.capitalize()} Phase\n\n"
|
||||
for field, value in test[phase].items():
|
||||
if value is None:
|
||||
f.write(f"- **{field.capitalize()}:** None\n")
|
||||
body_test += f"- **{field.capitalize()}:** None\n"
|
||||
else:
|
||||
f.write(f"<details>\n<summary>📌 {field.capitalize()}</summary>\n\n")
|
||||
f.write("```\n")
|
||||
write_json_value(f, value)
|
||||
f.write("```\n")
|
||||
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")
|
||||
phase_body = "```\n"
|
||||
phase_body += stringify(value) + "\n"
|
||||
phase_body += "```\n"
|
||||
# détail du champ (champ imbriqué, donc niveau +1)
|
||||
write_details(
|
||||
f,
|
||||
f"📌 {field.capitalize()}",
|
||||
phase_body,
|
||||
level=4
|
||||
)
|
||||
write_details(
|
||||
f,
|
||||
f"{emoji} #{test['global_number']}",
|
||||
body_test,
|
||||
level=3
|
||||
)
|
||||
write_details(
|
||||
f,
|
||||
f"🔧 Function: `{funcname}`",
|
||||
"", # contenu injecté dans les tests
|
||||
level=2
|
||||
)
|
||||
write_details(
|
||||
f,
|
||||
f"📁 {filename}",
|
||||
"", # idem, contenu injecté dans les fonctions
|
||||
level=1
|
||||
)
|
||||
write_details(
|
||||
f,
|
||||
f"{emoji} {status_label} ({count})",
|
||||
"", # contenu injecté dans les fichiers
|
||||
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")
|
||||
@@ -188,8 +193,8 @@ def json_to_md_nested(json_path, md_path, allure_dir=None):
|
||||
for folder, collectors in grouped.items():
|
||||
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")
|
||||
|
||||
body_collectors = ""
|
||||
outputs = []
|
||||
for collector in collectors:
|
||||
outcome = collector.get("outcome", "unknown")
|
||||
@@ -204,9 +209,9 @@ def json_to_md_nested(json_path, md_path, allure_dir=None):
|
||||
) + "\n```")
|
||||
|
||||
if outputs:
|
||||
f.write("### 🧾 Error or Result Summary\n\n")
|
||||
body_collectors += "### 🧾 Error or Result Summary\n\n"
|
||||
for out in outputs:
|
||||
f.write(out + "\n")
|
||||
body_collectors += out + "\n"
|
||||
|
||||
collectors_sorted = sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0])
|
||||
for collector in collectors_sorted:
|
||||
@@ -214,40 +219,49 @@ def json_to_md_nested(json_path, md_path, allure_dir=None):
|
||||
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")
|
||||
|
||||
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:
|
||||
f.write("- **Details:**\n")
|
||||
f.write("```\n")
|
||||
body_coll += "- **Details:**\n"
|
||||
body_coll += "```\n"
|
||||
for k, v in other_keys.items():
|
||||
f.write(f"{k}:\n")
|
||||
try:
|
||||
if v is None:
|
||||
f.write(" None\n")
|
||||
else:
|
||||
write_json_value(f, v, indent=1)
|
||||
except Exception as e:
|
||||
f.write(f" <error formatting value: {e}>\n")
|
||||
f.write("\n")
|
||||
f.write("```\n")
|
||||
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:
|
||||
f.write("- **Details:** `None`\n")
|
||||
|
||||
f.write("</details>\n\n")
|
||||
|
||||
f.write("</details>\n\n")
|
||||
body_coll += "- **Details:** `None`\n"
|
||||
write_details(
|
||||
f,
|
||||
f"{emoji} {short_node}",
|
||||
body_coll,
|
||||
level=2
|
||||
)
|
||||
write_details(
|
||||
f,
|
||||
f"{folder_emoji} {folder} ({len(collectors)} tests)",
|
||||
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):
|
||||
f.write(f"<details>\n<summary>Warning #{i}</summary>\n\n")
|
||||
f.write("```\n")
|
||||
warn_body = "```\n"
|
||||
for k, v in warning.items():
|
||||
f.write(f"{k}: {v}\n")
|
||||
f.write("```\n")
|
||||
f.write("</details>\n\n")
|
||||
warn_body += f"{k}: {v}\n"
|
||||
warn_body += "```\n"
|
||||
write_details(
|
||||
f,
|
||||
f"Warning #{i}",
|
||||
warn_body,
|
||||
level=1
|
||||
)
|
||||
|
||||
def run_pytest_and_generate_banner_with_logs(md_path, log_path):
|
||||
exit_code = pytest.main(["tests/"])
|
||||
@@ -321,8 +335,6 @@ def run_pytest_and_generate_banner_with_logs(md_path, log_path):
|
||||
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")
|
||||
@@ -331,15 +343,12 @@ def main():
|
||||
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()
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user