This commit is contained in:
+137
-68
@@ -8,17 +8,20 @@ import pytz
|
||||
|
||||
def stringify(obj, indent=0):
|
||||
space = ' ' * indent
|
||||
|
||||
if obj is None or obj == "":
|
||||
return "None"
|
||||
|
||||
if isinstance(obj, list):
|
||||
if not obj:
|
||||
return "[]"
|
||||
return '\n'.join(
|
||||
f"{space}- (vide)" if not e or (isinstance(e, dict) and not e)
|
||||
else f"{space}- {stringify(e, indent + 1)}" if not isinstance(e, dict)
|
||||
else f"{space}- {stringify(e, indent + 1)}"
|
||||
else f"{space}- {stringify(e, indent + 1)}" # ← plus de \n !
|
||||
for e in obj
|
||||
)
|
||||
|
||||
if isinstance(obj, dict):
|
||||
if not obj:
|
||||
return "{}"
|
||||
@@ -27,6 +30,7 @@ def stringify(obj, indent=0):
|
||||
else f"{space}{k}:\n{stringify(v, indent + 1)}"
|
||||
for k, v in obj.items()
|
||||
)
|
||||
|
||||
return str(obj)
|
||||
|
||||
def sanitize_param_value(v):
|
||||
@@ -35,92 +39,157 @@ def sanitize_param_value(v):
|
||||
s.replace('&', '^')
|
||||
.replace('<', '(')
|
||||
.replace('>', ')')
|
||||
.replace('"', '')
|
||||
.replace("'", '')
|
||||
.replace('`', '')
|
||||
.replace('=', '≡')
|
||||
.replace('\\', '/')
|
||||
.replace('"', '')
|
||||
.replace("'", '')
|
||||
.replace('`', '')
|
||||
.replace('=', '≡')
|
||||
.replace('\\', '/')
|
||||
)
|
||||
|
||||
def make_test_block(test, status, emoji, level, runtime_params):
|
||||
def make_test_details(test, level, runtime_params):
|
||||
nodeid = test.get("nodeid", "")
|
||||
indent = " " * level
|
||||
output = ""
|
||||
|
||||
summary_note = f"{emoji} Test #{test['global_number']}"
|
||||
|
||||
details = []
|
||||
|
||||
# Ajouter les paramètres d'exécution s'ils existent
|
||||
callspec = runtime_params.get(nodeid, {})
|
||||
params = callspec.get("params", {}) if isinstance(callspec, dict) else {}
|
||||
param_display = ""
|
||||
if isinstance(params, dict) and params:
|
||||
param_display = ", ".join(f"{k}={sanitize_param_value(v)}" for k, v in params.items())
|
||||
summary_note += f" (params: {param_display})"
|
||||
|
||||
output += f"{indent}- <details>\n"
|
||||
output += f"{indent} <summary>{summary_note}</summary>\n\n"
|
||||
|
||||
# Runtime parameters block
|
||||
if callspec:
|
||||
callspec_block = "```python\n" + stringify(callspec) + "\n```"
|
||||
output += f"{indent} - <details>\n"
|
||||
output += f"{indent} <summary>📌 Runtime Parameters</summary>\n\n"
|
||||
output += f"{callspec_block}\n\n"
|
||||
output += f"{indent} </details>\n"
|
||||
|
||||
params_str = "```python\n" + stringify(callspec) + "\n```"
|
||||
details.append(f" - **Paramètres**:\n{params_str}")
|
||||
|
||||
# Ajouter les phases (setup, call, teardown)
|
||||
skip_keys = {"nodeid"}
|
||||
phase_keys = [k for k in test.keys() if isinstance(test[k], dict) and k not in skip_keys]
|
||||
|
||||
for phase in phase_keys:
|
||||
output += f"{indent} ### 🔧 {phase.capitalize()} Phase\n"
|
||||
phase_details = []
|
||||
for field, value in test[phase].items():
|
||||
details_body = "```python\n" + stringify(value) + "\n```" if value is not None else "None"
|
||||
output += f"{indent} - <details>\n"
|
||||
output += f"{indent} <summary>📌 {field.capitalize()}</summary>\n\n"
|
||||
output += f"{details_body}\n\n"
|
||||
output += f"{indent} </details>\n"
|
||||
|
||||
output += f"{indent}</details>\n"
|
||||
return output
|
||||
if value is not None:
|
||||
phase_details.append(f" - **{field}**:\n ```python\n{stringify(value)}\n ```")
|
||||
|
||||
if phase_details:
|
||||
details.append(f" - **Phase {phase}**:\n" + "\n".join(phase_details))
|
||||
|
||||
return "\n".join(details)
|
||||
|
||||
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")
|
||||
|
||||
f.write("# 🧪 Rapport de tests\n\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")
|
||||
f.write(f"*Généré le {now.strftime('%Y-%m-%d %H:%M:%S %Z')}*\n\n")
|
||||
|
||||
f.write("## 🔎 Tests\n")
|
||||
# Section Informations générales
|
||||
general_info = {}
|
||||
for k, v in data.items():
|
||||
if k == "summary":
|
||||
break
|
||||
if k not in {"created", "exitcode"}:
|
||||
general_info[k] = v
|
||||
|
||||
test_counter = 1
|
||||
for test in data.get("tests", []):
|
||||
test['global_number'] = test_counter
|
||||
test_counter += 1
|
||||
if general_info:
|
||||
f.write("## 🧾 Informations générales\n")
|
||||
for key, value in general_info.items():
|
||||
f.write(f"- **{key}**: {stringify(value)}\n")
|
||||
f.write("\n")
|
||||
|
||||
grouped = defaultdict(lambda: defaultdict(list))
|
||||
for test in data.get("tests", []):
|
||||
nodeid = test.get("nodeid", "")
|
||||
parts = nodeid.split("::")
|
||||
filename = parts[0].replace("tests\\", "").replace("tests/", "")
|
||||
funcname = parts[1].split("[")[0]
|
||||
grouped[filename][funcname].append(test)
|
||||
# Section Résumé
|
||||
if 'summary' in data:
|
||||
f.write("## 📋 Résumé\n")
|
||||
for key, value in data['summary'].items():
|
||||
f.write(f"- **{key.capitalize()}**: {stringify(value)}\n")
|
||||
f.write("\n")
|
||||
|
||||
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"- 🔧 {funcname}\n\n")
|
||||
for test in tests:
|
||||
test_block = make_test_block(test, test.get("outcome"), "➡️", level=2, runtime_params=runtime_params)
|
||||
f.write(test_block + "\n")
|
||||
f.write("</details>\n\n")
|
||||
# Section Tests
|
||||
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")
|
||||
|
||||
# Grouper les tests par statut
|
||||
tests_by_status = defaultdict(list)
|
||||
for test in data["tests"]:
|
||||
outcome = test.get("outcome", "unknown")
|
||||
tests_by_status[outcome].append(test)
|
||||
|
||||
# Grouper les tests par fichier et fonction
|
||||
grouped = defaultdict(lambda: defaultdict(list))
|
||||
for test in data["tests"]:
|
||||
nodeid = test.get("nodeid", "")
|
||||
parts = nodeid.split("::")
|
||||
filename = parts[0].replace("tests\\", "").replace("tests/", "")
|
||||
funcname = parts[1].split("[")[0] if len(parts) > 1 else "unknown"
|
||||
grouped[filename][funcname].append(test)
|
||||
|
||||
# Générer le rapport par fichier
|
||||
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" - 🔧 {funcname}\n\n")
|
||||
|
||||
for test in sorted(tests, key=lambda x: x['global_number']):
|
||||
outcome = test.get("outcome", "unknown")
|
||||
emoji = "✅" if outcome == "passed" else "❌"
|
||||
duration = test.get("duration", 0)
|
||||
|
||||
# Header du test
|
||||
f.write(f" - <details>\n <summary>{emoji} Test #{test['global_number']}</summary>\n\n")
|
||||
|
||||
# Corps du test
|
||||
test_details = make_test_details(test, level=3, runtime_params=runtime_params)
|
||||
if test_details:
|
||||
f.write(test_details + "\n")
|
||||
|
||||
f.write(" </details>\n\n")
|
||||
|
||||
f.write("</details>\n\n")
|
||||
|
||||
# Section Collecteurs
|
||||
if "collectors" in data:
|
||||
f.write("## 📚 Fichiers 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():
|
||||
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}</summary>\n\n")
|
||||
|
||||
for collector in sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0]):
|
||||
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" - **Statut**: `{outcome}`\n")
|
||||
|
||||
other_keys = {k: v for k, v in collector.items() if k not in {"nodeid", "outcome"}}
|
||||
if other_keys:
|
||||
for k, v in other_keys.items():
|
||||
f.write(f" - **{k}**:\n ```python\n{stringify(v)}\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="Path to pytest JSON file")
|
||||
parser.add_argument("--output", required=True, help="Path to output Markdown file")
|
||||
parser.add_argument("--params", required=False, help="Path to runtime-params.json")
|
||||
parser = argparse.ArgumentParser(description="Convertir les résultats de tests JSON en Markdown.")
|
||||
parser.add_argument("--input", required=True, help="Chemin vers le fichier JSON pytest")
|
||||
parser.add_argument("--output", required=True, help="Chemin vers le fichier Markdown de sortie")
|
||||
parser.add_argument("--params", required=False, help="Chemin vers runtime-params.json")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -133,10 +202,10 @@ def main():
|
||||
if "nodeid" in entry and isinstance(entry.get("callspec"), dict):
|
||||
runtime_params[entry["nodeid"]] = entry["callspec"]
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to read runtime parameters: {e}")
|
||||
|
||||
print(f"❌ Échec de lecture des paramètres d'exécution: {e}")
|
||||
|
||||
json_to_md_nested(args.input, args.output, runtime_params)
|
||||
print(f"✅ Report generated at {args.output}")
|
||||
print(f"✅ Rapport généré à {args.output}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
Reference in New Issue
Block a user