#!/usr/bin/env python3 from argparse import ArgumentParser import os import json from pathlib import Path from typing import Optional, Tuple script_dir = os.path.dirname(os.path.realpath(__file__)) root_dir = os.path.abspath(os.path.join(script_dir, "..")) UNIT_KEYS_TO_DIFF = [ "fuzzy_match_percent", "matched_code_percent", "matched_data_percent", "complete_code_percent", "complete_data_percent", ] FUNCTION_KEYS_TO_DIFF = [ "fuzzy_match_percent", ] Change = Tuple[str, str, float, float] def format_float(value: float) -> str: if value < 100.0 and value > 99.99: value = 99.99 return "%6.2f" % value def get_changes(changes_file: str) -> Tuple[list[Change], list[Change]]: changes_file = os.path.relpath(changes_file, root_dir) with open(changes_file, "r") as f: changes_json = json.load(f) regressions = [] progressions = [] def diff_key(object_name: Optional[str], object: dict, key: str): from_value = object.get("from", {}).get(key, 0.0) to_value = object.get("to", {}).get(key, 0.0) key = key.removesuffix("_percent") change = (object_name, key, from_value, to_value) if from_value > to_value: regressions.append(change) elif to_value > from_value: progressions.append(change) for key in UNIT_KEYS_TO_DIFF: diff_key(None, changes_json, key) for unit in changes_json.get("units", []): unit_name = unit["name"] for key in UNIT_KEYS_TO_DIFF: diff_key(unit_name, unit, key) # Ignore sections for func in unit.get("functions", []): func_name = func["name"] for key in FUNCTION_KEYS_TO_DIFF: diff_key(func_name, func, key) return regressions, progressions def generate_changes_plaintext(changes: list[Change]) -> str: if len(changes) == 0: return "" table_total_width = 136 percents_max_len = 7 + 4 + 7 key_max_len = max(len(key) for _, key, _, _ in changes) name_max_len = max(len(name or "Total") for name, _, _, _ in changes) max_width_for_name_col = table_total_width - 3 - key_max_len - 3 - percents_max_len name_max_len = min(max_width_for_name_col, name_max_len) out_lines = [] for name, key, from_value, to_value in changes: if name is None: name = "Total" if len(name) > name_max_len: name = name[: name_max_len - len("[...]")] + "[...]" out_lines.append( f"{name:>{name_max_len}} | {key:<{key_max_len}} | {format_float(from_value)}% -> {format_float(to_value)}%" ) return "\n".join(out_lines) def generate_changes_markdown(changes: list[Change], description: str) -> str: if len(changes) == 0: return "" out_lines = [] name_max_len = 100 out_lines.append("
") out_lines.append( f"Detected {len(changes)} {description} compared to the base:" ) out_lines.append("") # Must include a blank line before a table out_lines.append("| Name | Type | Before | After |") out_lines.append("| ---- | ---- | ------ | ----- |") for name, key, from_value, to_value in changes: if name is None: name = "Total" else: if len(name) > name_max_len: name = name[: name_max_len - len("...")] + "..." name = f"`{name}`" # Surround with backticks key = key.replace("_", " ").capitalize() out_lines.append( f"| {name} | {key} | {format_float(from_value)}% | {format_float(to_value)}% |" ) out_lines.append("
") return "\n".join(out_lines) def main(): parser = ArgumentParser(description="Format objdiff-cli report changes.") parser.add_argument( "report_changes_file", type=Path, help="""path to the JSON file containing the changes, generated by objdiff-cli.""", ) parser.add_argument( "-o", "--output", type=Path, help="""Output file (prints to console if unspecified)""", ) parser.add_argument( "--all", action="store_true", help="""Includes progressions as well.""", ) args = parser.parse_args() regressions, progressions = get_changes(args.report_changes_file) if args.output: markdown_output = generate_changes_markdown(regressions, "regressions") if args.all: markdown_output += generate_changes_markdown(progressions, "progressions") with open(args.output, "w", encoding="utf-8") as f: f.write(markdown_output) else: if args.all: changes = progressions + regressions else: changes = regressions text_output = generate_changes_plaintext(changes) print(text_output) if __name__ == "__main__": main()