tp/tools/changes_fmt.py

163 lines
4.8 KiB
Python

#!/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("<details>")
out_lines.append(
f"<summary>Detected {len(changes)} {description} compared to the base:</summary>"
)
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("</details>")
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()