This commit is contained in:
+48
-175
@@ -4,9 +4,6 @@ import json
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import pytest
|
||||
from pytest import ExitCode
|
||||
import traceback
|
||||
import pytz
|
||||
|
||||
def stringify(obj, indent=0):
|
||||
@@ -48,42 +45,28 @@ def sanitize_param_value(v):
|
||||
.replace('=', '≡')
|
||||
.replace('\\', '/')
|
||||
)
|
||||
|
||||
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 get_details_block(summary, body, level=0, params_str=None, block_id=None, total_blocks=None):
|
||||
"""Version améliorée avec navigation"""
|
||||
if params_str and params_str.strip() != "{}":
|
||||
summary += f" [{params_str}]"
|
||||
def get_clickable_block(summary, body, block_id, level=0):
|
||||
"""Version avec navigation cliquable (Précédent/Accueil)"""
|
||||
indent = " " * level
|
||||
|
||||
body = body.strip()
|
||||
|
||||
# Ajout de la navigation si block_id et total_blocks sont fournis
|
||||
navigation = ""
|
||||
if block_id is not None and total_blocks is not None:
|
||||
nav_links = []
|
||||
if block_id > 1:
|
||||
nav_links.append(f"[← Précédent](#block_{block_id-1})")
|
||||
nav_links.append(f"[↑ Accueil](#top)")
|
||||
if block_id < total_blocks:
|
||||
nav_links.append(f"[→ Suivant](#block_{block_id+1})")
|
||||
navigation = f"\n\n<div style='text-align:right;font-size:0.9em;'>\n{' | '.join(nav_links)}\n</div>"
|
||||
# Navigation
|
||||
nav_links = []
|
||||
if block_id > 1:
|
||||
nav_links.append(f"[← Précédent](#block_{block_id-1})")
|
||||
nav_links.append(f"[↑ Accueil](#top)")
|
||||
|
||||
return (
|
||||
f"<details id='block_{block_id}' {'open' if level == 0 else ''}>\n"
|
||||
f"<summary><strong>{summary}</strong></summary>\n\n"
|
||||
f"{body}{navigation}\n\n"
|
||||
f"</details>\n\n"
|
||||
f"{indent}<div id='block_{block_id}' style='margin-bottom:20px;border-bottom:1px solid #eee;padding-bottom:15px'>\n"
|
||||
f"{indent} <h3>{summary}</h3>\n"
|
||||
f"{indent} <div>{body}</div>\n"
|
||||
f"{indent} <div style='text-align:right;font-size:0.9em;margin-top:10px'>\n"
|
||||
f"{indent} {' | '.join(nav_links)}\n"
|
||||
f"{indent} </div>\n"
|
||||
f"{indent}</div>\n"
|
||||
)
|
||||
|
||||
def make_test_block(test, status, emoji, level, runtime_params, block_id=None, total_blocks=None):
|
||||
def make_test_block(test, status, emoji, level, runtime_params, block_id):
|
||||
nodeid = test.get("nodeid", "")
|
||||
body_test = ""
|
||||
summary_note = f"{emoji} #{test['global_number']}"
|
||||
@@ -93,11 +76,11 @@ def make_test_block(test, status, emoji, level, runtime_params, block_id=None, t
|
||||
|
||||
if isinstance(params, dict) and params:
|
||||
param_display = ", ".join(f"{k}={sanitize_param_value(v)}" for k, v in params.items())
|
||||
summary_note += f" <span style='color: #888; font-size: 0.9em;'>params: {param_display}</span>"
|
||||
summary_note += f" <small style='color:#666'>[{param_display}]</small>"
|
||||
|
||||
if callspec:
|
||||
callspec_block = "```python\n" + stringify(callspec) + "\n```"
|
||||
body_test += get_details_block("📌 Runtime Parameters", callspec_block, level + 1)
|
||||
body_test += callspec_block + "\n\n"
|
||||
|
||||
skip_keys = {"nodeid"}
|
||||
phase_keys = [k for k in test.keys() if isinstance(test[k], dict) and k not in skip_keys]
|
||||
@@ -106,27 +89,27 @@ def make_test_block(test, status, emoji, level, runtime_params, block_id=None, t
|
||||
phase_body = ""
|
||||
for field, value in test[phase].items():
|
||||
details_body = "```python\n" + stringify(value) + "\n```" if value is not None else "None"
|
||||
phase_body += get_details_block(f"📌 {field.capitalize()}", details_body, level + 2)
|
||||
phase_body += f"**{field.capitalize()}:**\n{details_body}\n\n"
|
||||
if phase_body:
|
||||
body_test += f"\n### 🔧 {phase.capitalize()} Phase\n\n" + phase_body
|
||||
body_test += f"### {phase.capitalize()} Phase\n{phase_body}"
|
||||
|
||||
return get_details_block(summary_note, body_test, level + 1, block_id=block_id, total_blocks=total_blocks)
|
||||
return get_clickable_block(summary_note, body_test, block_id, level + 1)
|
||||
|
||||
def json_to_md_nested(json_path, md_path, runtime_params=None):
|
||||
with open(json_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
with open(md_path, 'w', encoding='utf-8') as f:
|
||||
# Ajout du CSS et ancres
|
||||
# En-tête avec CSS
|
||||
f.write("""<style>
|
||||
details { margin-bottom: 10px; }
|
||||
summary { cursor: pointer; }
|
||||
#top { padding-top: 20px; }
|
||||
a { color: #0366d6; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
#top { padding-top: 20px; }
|
||||
h3 { margin: 0 0 8px 0; color: #333; }
|
||||
small { color: #666; }
|
||||
</style>
|
||||
<a id="top"></a>\n\n""")
|
||||
|
||||
|
||||
f.write(f"# 🧪 Test Report\n")
|
||||
|
||||
local = pytz.timezone("Europe/Zurich")
|
||||
@@ -152,6 +135,7 @@ def json_to_md_nested(json_path, md_path, runtime_params=None):
|
||||
f.write(f"- **{key.capitalize()}**: {stringify(value)}\n")
|
||||
f.write("\n")
|
||||
|
||||
|
||||
if "tests" in data and "summary" in data:
|
||||
test_counter = 1
|
||||
for test in data["tests"]:
|
||||
@@ -160,6 +144,26 @@ def json_to_md_nested(json_path, md_path, runtime_params=None):
|
||||
|
||||
f.write("## 🔎 Tests\n")
|
||||
|
||||
# Compter le nombre total de tests
|
||||
total_tests = len(data["tests"])
|
||||
current_block = 1
|
||||
|
||||
for test in data["tests"]:
|
||||
outcome = test.get("outcome", "unknown")
|
||||
emoji = "✅" if outcome == "passed" else "❌"
|
||||
|
||||
block = make_test_block(
|
||||
test=test,
|
||||
status=outcome,
|
||||
emoji=emoji,
|
||||
level=1,
|
||||
runtime_params=runtime_params,
|
||||
block_id=current_block
|
||||
)
|
||||
f.write(block)
|
||||
current_block += 1
|
||||
|
||||
|
||||
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]]
|
||||
@@ -173,113 +177,6 @@ def json_to_md_nested(json_path, md_path, runtime_params=None):
|
||||
total_blocks = sum(len(tests) for tests in tests_by_status.values())
|
||||
current_block = 1
|
||||
|
||||
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,
|
||||
runtime_params=runtime_params,
|
||||
block_id=current_block,
|
||||
total_blocks=total_blocks)
|
||||
current_block += 1
|
||||
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, block_id=None))
|
||||
|
||||
|
||||
def json_to_md_nested(json_path, md_path, runtime_params=None):
|
||||
with open(json_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
with open(md_path, 'w', encoding='utf-8') as f:
|
||||
f.write(f"# 🧪 Test Report\n")
|
||||
|
||||
local = pytz.timezone("Europe/Zurich")
|
||||
now = datetime.now(local)
|
||||
f.write(f"*Generated on {now.strftime('%Y-%m-%d %H:%M:%S %Z')}*\n\n")
|
||||
|
||||
general_info = {}
|
||||
for k, v in data.items():
|
||||
if k == "summary":
|
||||
break
|
||||
if k not in {"created", "exitcode"}:
|
||||
general_info[k] = v
|
||||
|
||||
if general_info:
|
||||
f.write("## 🧾 General Info\n")
|
||||
for key, value in general_info.items():
|
||||
f.write(f"- **{key}**: {stringify(value)}\n")
|
||||
f.write("\n")
|
||||
|
||||
if 'summary' in data:
|
||||
f.write("## 📋 Summary\n")
|
||||
for key, value in data['summary'].items():
|
||||
f.write(f"- **{key.capitalize()}**: {stringify(value)}\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, runtime_params=runtime_params)
|
||||
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")
|
||||
@@ -287,31 +184,7 @@ def json_to_md_nested(json_path, md_path, runtime_params=None):
|
||||
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"
|
||||
main
|
||||
|
||||
collectors_sorted = sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0])
|
||||
folder_body = ""
|
||||
|
||||
Reference in New Issue
Block a user