From e80ded5037badc7534f33a596df98e5eee85124f Mon Sep 17 00:00:00 2001 From: tligui_y Date: Wed, 16 Jul 2025 14:22:57 +0200 Subject: [PATCH] Update json_to_md.py --- json_to_md.py | 101 +++++++++++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 42 deletions(-) diff --git a/json_to_md.py b/json_to_md.py index b8dced39..4f35abd9 100644 --- a/json_to_md.py +++ b/json_to_md.py @@ -18,7 +18,7 @@ def stringify(obj): return str(obj) def normalize_nodeid(nodeid): - match = re.match(r"(tests[/\\\\].+?)\\.py::(.+?)(?:\[.*)?$", nodeid) + match = re.match(r"(tests[/\\].+?)\.py::(.+?)(?:\[.*)?$", nodeid) if match: file_part = match.group(1).replace("/", ".").replace("\\", ".") func_part = match.group(2) @@ -34,77 +34,87 @@ def extract_param_str_from_nodeid(nodeid): def get_details_block(summary, body, level=0, params_str=None): indent = " " * level - bullet = f"{indent}- " if params_str: - summary += f' parameters: [{params_str}]' - return ( - f"{bullet}
\n" - f"{indent} {summary}\n\n" - + "\n".join(f"{indent} {line}" for line in body.strip().splitlines()) + "\n" - f"{indent}
\n" - ) + summary = f"{summary} [parameters: {params_str}]" + return f"{indent}- {summary}\n{body}" def make_test_block(test, status, emoji, level): nodeid = test.get("nodeid", "") param_str = extract_param_str_from_nodeid(nodeid) + body_test = "" + if param_str: - body_test += f"- **Parameters:** `{param_str}`\n" + body_test += f"{' '*(level+1)}- Parameters: `{param_str}`\n" + skip_keys = set(["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" + phase_body += f"{' '*(level+3)}- {field.capitalize()}: None\n" else: - details_body = "```\n" + stringify(value) + "\n```" - phase_body += get_details_block(f"๐Ÿ“Œ {field.capitalize()}", details_body, level+2) + details_body = f"{' '*(level+4)}```\n{' '*(level+4)}{stringify(value)}\n{' '*(level+4)}```\n" + phase_body += f"{' '*(level+3)}- {field.capitalize()}:\n{details_body}" if phase_body: - body_test += f"\n### ๐Ÿ”ง {phase.capitalize()} Phase\n\n" + phase_body + body_test += f"{' '*(level+2)}- {phase.capitalize()} Phase:\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): 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") + 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") + 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]: @@ -113,6 +123,7 @@ def json_to_md_nested(json_path, md_path): 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(): @@ -122,6 +133,7 @@ def json_to_md_nested(json_path, md_path): 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)) + if "collectors" in data: f.write("## ๐Ÿ“š Collected files\n") grouped = defaultdict(list) @@ -130,9 +142,11 @@ def json_to_md_nested(json_path, md_path): 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: @@ -141,15 +155,17 @@ def json_to_md_nested(json_path, md_path): short_node = nodeid.split("[")[0] results = collector.get("result", []) if outcome != "passed" and results: - outputs.append(f"### โŒ {short_node}\n```\n" + "\n".join( + outputs.append(f"{' '*(level+2)}- โŒ {short_node}\n{' '*(level+3)}```\n{' '*(level+3)}" + "\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```") + ) + f"\n{' '*(level+3)}```\n") + if outputs: - body_collectors += "### ๐Ÿงพ Error or Result Summary\n\n" + body_collectors += f"{' '*(level+1)}- Error or Result Summary:\n" for out in outputs: body_collectors += out + "\n" + collectors_sorted = sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0]) folder_body = "" for collector in collectors_sorted: @@ -157,38 +173,37 @@ def json_to_md_nested(json_path, md_path): emoji = "โœ…" if outcome == "passed" else "โŒ" nodeid = collector.get("nodeid", "unknown") short_node = nodeid.split("[")[0] - body_coll = f"- **Outcome:** `{outcome}`\n" + + body_coll = f"{' '*(level+2)}- Outcome: `{outcome}`\n" other_keys = {k: v for k, v in collector.items() if k not in {"nodeid", "outcome"}} if other_keys: - body_coll += "- **Details:**\n``` -" + body_coll += f"{' '*(level+2)}- Details:\n{' '*(level+3)}```\n" for k, v in other_keys.items(): - body_coll += f"{k}:\n" + body_coll += f"{' '*(level+3)}{k}:\n" if v is None: - body_coll += " None\n" + body_coll += f"{' '*(level+3)} None\n" else: - body_coll += stringify(v) + "\n" - body_coll += "\n" - body_coll += "``` -" + body_coll += f"{' '*(level+3)}{stringify(v)}\n" + body_coll += f"\n{' '*(level+3)}" + body_coll += f"```\n" else: - body_coll += "- **Details:** `None`\n" + body_coll += f"{' '*(level+2)}- Details: `None`\n" folder_body += get_details_block(f"{emoji} {short_node}", body_coll, level=2) f.write(get_details_block(f"{folder_emoji} {folder} ({len(collectors)} tests)", folder_body + body_collectors, level=1)) + if 'warnings' in data and data['warnings']: f.write("## โš ๏ธ Warnings\n\n") for i, warning in enumerate(data['warnings'], 1): - warn_body = "``` -" + warn_body = f"{' '*(level+1)}```\n" for k, v in warning.items(): - warn_body += f"{k}: {v}\n" - warn_body += "``` -" + warn_body += f"{' '*(level+1)}{k}: {v}\n" + warn_body += f"{' '*(level+1)}```\n" f.write(get_details_block(f"Warning #{i}", warn_body, level=1)) 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" @@ -214,12 +229,14 @@ def run_pytest_and_generate_banner_with_logs(md_path, log_path, exit_code): 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: @@ -229,24 +246,21 @@ def run_pytest_and_generate_banner_with_logs(md_path, log_path, exit_code): 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 + - "
\n๐Ÿ“‹ Short test summary info\n\n" + - "``` -" + "".join(short_summary_lines) + "``` -
\n\n" + - "
\n๐Ÿชต Full raw pytest log\n\n" + - "``` -" + "".join(log_lines) + "``` -
\n\n" + + "- ๐Ÿ“‹ Short test summary info:\n ```\n" + "".join(short_summary_lines) + " ```\n\n" + + "- ๐Ÿชต Full raw pytest log:\n ```\n" + "".join(log_lines) + " ```\n\n" + "---\n\n" ) + try: with open(md_path, "w") as f: f.write(full_banner + original_md) @@ -254,17 +268,20 @@ def run_pytest_and_generate_banner_with_logs(md_path, log_path, exit_code): except Exception as e: print(f"โŒ Failed to update markdown report: {e}") return - + 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("--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__":