Files
slic/json_to_md.py
T
tligui_y 4b6448f53e
Run Pytest with HTML and XML Test Reports / tests (push) Successful in 25s
Update json_to_md.py
2025-07-17 13:44:10 +02:00

153 lines
6.1 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 '\n'.join(stringify(e) for e in obj)
if isinstance(obj, dict):
return '\n'.join(f"{k}: {stringify(v)}" for k, v in obj.items())
return str(obj)
def normalize_nodeid(nodeid):
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 extract_param_str_from_nodeid(nodeid):
first = nodeid.find('[')
last = nodeid.rfind(']')
if first != -1 and last != -1 and last > first:
return nodeid[first+1:last]
return None
def get_details_block(summary, body, level=0, params_str=None):
margin = 18 * level
border = f"border-left: 2px solid #eee;" if level > 0 else ""
if params_str:
summary = f"{summary} <span style='color: #888; font-size: 0.9em;'>params: [{params_str}]</span>"
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")
def make_test_block(test, status, emoji, level, callspec_data):
nodeid = test.get("nodeid", "")
param_str = extract_param_str_from_nodeid(nodeid)
body_test = ""
if nodeid in callspec_data:
spec = callspec_data[nodeid]
extracted = {k: v for k, v in spec.items() if k not in {"getparam", "setmulti", "_arg2scope", "_idlist"}}
body_test += f"### 🔬 Parameters
```
{stringify(extracted)}
```
"
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:
phase_body = ""
for field, value in test[phase].items():
if value is None:
phase_body += f"- **{field.capitalize()}:** None\n"
else:
details_body = "```
" + stringify(value) + "\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, params_str=param_str)
def json_to_md_nested(json_path, md_path, params_path=None):
with open(json_path) as f:
data = json.load(f)
callspec_data = {}
if params_path and os.path.exists(params_path):
with open(params_path) as f:
for entry in json.load(f):
if 'nodeid' in entry and 'callspec' in entry:
callspec_data[entry['nodeid']] = entry['callspec']
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")
general_info = {k: v for k, v in data.items() if k not in {"summary", "tests", "collectors", "created", "exitcode"}}
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")
if "tests" in data:
for i, test in enumerate(data["tests"], 1):
test['global_number'] = i
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]:
parts = test.get("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, callspec_data=callspec_data)
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))
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 (optional)")
args = parser.parse_args()
json_to_md_nested(args.input, args.output, params_path=args.params)
print(f"✅ Report generated at {args.output}")
if __name__ == "__main__":
main()