Files
slic/json_to_md.py
T
tligui_y 1201c19dfe
Run Pytest with HTML and XML Test Reports / tests (push) Successful in 22s
Update json_to_md.py
2025-07-16 01:52:28 +02:00

219 lines
7.8 KiB
Python

from collections import defaultdict
from datetime import datetime
import json
import argparse
import os
import re
import pytest
from pytest import ExitCode
import traceback
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):
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;'>parameters: [{params_str}]</span>"
return (f'<div style="margin-left: {margin}px; {border} padding-left: 8px;">
'
f"<details>
<summary>{summary}</summary>
"
f"{body}
"
f"</details>
"
f"</div>
")
def make_test_block(test, status, emoji, level):
nodeid = test.get("nodeid", "")
param_str = extract_param_str_from_nodeid(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"
if param_str:
body_test += f"- **Parameters:** {param_str}\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 = "```
" + stringify(value) + "
```
"
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} {nodeid}", body_test, level+1, params_str=param_str)
def json_to_md_nested(json_path, md_path):
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(f"*Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n")
known = {'summary', 'tests', 'collectors'}
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")
if "tests" in data:
f.write("## 🔎 Tests\n")
for test in data["tests"]:
f.write(make_test_block(test, test.get("outcome", "unknown"), "" if test.get("outcome") == "passed" else "", level=0))
if "collectors" in data:
f.write("## 📚 Collected files\n")
for i, collector in enumerate(data["collectors"], 1):
nodeid = collector.get("nodeid", "unknown")
emoji = "" if collector.get("outcome") == "passed" else ""
result = collector.get("result", [])
result_block = ""
if result:
result_block += "```
"
for item in result:
if isinstance(item, dict):
for k, v in item.items():
result_block += f"{k}: {stringify(v)}\n"
else:
result_block += f"{stringify(item)}\n"
result_block += "```"
f.write(get_details_block(f"{emoji} {nodeid}", result_block, level=1))
for key, value in data.items():
if key not in known:
block = ""
if isinstance(value, list):
block += "```
"
for item in value:
block += stringify(item) + "\n"
block += "```"
else:
block = f"```
{stringify(value)}
```"
f.write(get_details_block(f"🔧 {key.capitalize()}", block, level=0))
def run_pytest_and_generate_banner_with_logs(md_path, log_path, exit_code):
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><summary>📋 Short test summary info</summary>\n\n" +
"```
" + "".join(short_summary_lines) + "```
" +
"</details>\n\n" +
"<details><summary>🪵 Full raw pytest log</summary>\n\n" +
"```
" + "".join(log_lines) + "```
" +
"</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("--log", required=False, help="Path to raw pytest output log (optional)")
parser.add_argument("--json", required=False, help="Path to pytest-report.json")
parser.add_argument("--exit-code", type=int, default=0, help="Exit code from pytest to determine the banner.")
args = parser.parse_args()
json_to_md_nested(args.input, args.output)
run_pytest_and_generate_banner_with_logs(md_path=args.output, log_path=args.log, exit_code=args.exit_code)
print(f"✅ Report generated at {args.output}")
if __name__ == "__main__":
main()