From 1379ce7ec9e07c1b446dc47e4b6058ad07ddbd21 Mon Sep 17 00:00:00 2001 From: MS Date: Sun, 9 Mar 2025 14:37:57 -0400 Subject: [PATCH] Store diffs in JSON report, HTML export for `reccmp-aggregate` (#94) * Include diffs in JSON report. HTML output for aggregate script * Add diff to deserialize * Remove TODO --- reccmp/isledecomp/compare/report.py | 7 +++++-- reccmp/isledecomp/utils.py | 26 +++++++++++++++++++++++++- reccmp/tools/aggregate.py | 15 ++++++++++++--- reccmp/tools/asmcmp.py | 28 ++++++++++++---------------- tests/test_report.py | 18 ++++++++++++++++++ 5 files changed, 72 insertions(+), 22 deletions(-) diff --git a/reccmp/isledecomp/compare/report.py b/reccmp/isledecomp/compare/report.py index fe5ece9b..3f5bc54e 100644 --- a/reccmp/isledecomp/compare/report.py +++ b/reccmp/isledecomp/compare/report.py @@ -99,8 +99,10 @@ def combine_reports(samples: list[ReccmpStatusReport]) -> ReccmpStatusReport: output.entities[addr] = e_list[0] - # Recomp addr will most likely vary between samples, so clear it - output.entities[addr].recomp_addr = None + # Keep the recomp_addr if it is the same across all samples. + # i.e. to detect where function alignment ends + if not all(e_list[0].recomp_addr == e.recomp_addr for e in e_list): + output.entities[addr].recomp_addr = "various" return output @@ -165,6 +167,7 @@ def _deserialize_version_1(obj: JSONReportVersion1) -> ReccmpStatusReport: recomp_addr=e.recomp, is_stub=e.stub, is_effective_match=e.effective, + diff=e.diff, ) return report diff --git a/reccmp/isledecomp/utils.py b/reccmp/isledecomp/utils.py index 26b8f37a..4a04269f 100644 --- a/reccmp/isledecomp/utils.py +++ b/reccmp/isledecomp/utils.py @@ -1,7 +1,31 @@ from datetime import datetime import logging import colorama -from reccmp.isledecomp.compare.report import ReccmpStatusReport, ReccmpComparedEntity +from pystache import Renderer # type: ignore[import-untyped] +from reccmp.assets import get_asset_file +from reccmp.isledecomp.compare.report import ( + ReccmpStatusReport, + ReccmpComparedEntity, + serialize_reccmp_report, +) + + +def write_html_report(html_file: str, report: ReccmpStatusReport): + """Create the interactive HTML diff viewer with the given report.""" + js_path = get_asset_file("../assets/reccmp.js") + with open(js_path, "r", encoding="utf-8") as f: + reccmp_js = f.read() + + # Convert the report to a JSON string to insert in the HTML template. + report_str = serialize_reccmp_report(report, diff_included=True) + + output_data = Renderer().render_path( + get_asset_file("../assets/template.html"), + {"report": report_str, "reccmp_js": reccmp_js}, + ) + + with open(html_file, "w", encoding="utf-8") as htmlfile: + htmlfile.write(output_data) def print_combined_diff(udiff, plain: bool = False, show_both: bool = False): diff --git a/reccmp/tools/aggregate.py b/reccmp/tools/aggregate.py index 563d5cad..e7661e9a 100644 --- a/reccmp/tools/aggregate.py +++ b/reccmp/tools/aggregate.py @@ -4,7 +4,7 @@ import logging from typing import Sequence from pathlib import Path -from reccmp.isledecomp.utils import diff_json +from reccmp.isledecomp.utils import diff_json, write_html_report from reccmp.isledecomp.compare.report import ( ReccmpStatusReport, combine_reports, @@ -90,6 +90,12 @@ def main(): action=TwoOrFewerArgsAction, help="Report files to diff.", ) + parser.add_argument( + "--html", + type=Path, + metavar="", + help="Location for HTML report based on aggregate.", + ) parser.add_argument( "--output", "-o", @@ -116,9 +122,9 @@ def main(): "exepected arguments for --samples or --diff. (No input files specified)" ) - if not (args.output or args.diff): + if not (args.output or args.diff or args.html): parser.error( - "expected arguments for --output or --diff. (No output action specified)" + "expected arguments for --output, --html, or --diff. (No output action specified)" ) agg_report: ReccmpStatusReport | None = None @@ -143,6 +149,9 @@ def main(): if args.output is not None: write_report_file(args.output, agg_report) + if args.html is not None: + write_html_report(args.html, agg_report) + # If --diff has at least one file and we aggregated some samples this run, diff the first file and the aggregate. # If --diff has two files and we did not aggregate this run, diff the files in the list. if args.diff is not None: diff --git a/reccmp/tools/asmcmp.py b/reccmp/tools/asmcmp.py index 59c11781..e83d95ce 100755 --- a/reccmp/tools/asmcmp.py +++ b/reccmp/tools/asmcmp.py @@ -12,6 +12,7 @@ print_combined_diff, diff_json, percent_string, + write_html_report, ) from reccmp.isledecomp.compare import Compare as IsleCompare @@ -44,20 +45,6 @@ def gen_json(json_file: str, json_str: str): f.write(json_str) -def gen_html(html_file: str, report: str): - js_path = get_asset_file("../assets/reccmp.js") - with open(js_path, "r", encoding="utf-8") as f: - reccmp_js = f.read() - - output_data = Renderer().render_path( - get_asset_file("../assets/template.html"), - {"report": report, "reccmp_js": reccmp_js}, - ) - - with open(html_file, "w", encoding="utf-8") as htmlfile: - htmlfile.write(output_data) - - def gen_svg(svg_file, name_svg, icon, svg_implemented_funcs, total_funcs, raw_accuracy): icon_data = None if icon: @@ -162,6 +149,11 @@ def virtual_address(value) -> int: metavar="", help="Generate JSON file with match summary", ) + parser.add_argument( + "--json-diet", + action="store_true", + help="Exclude diff from JSON report.", + ) parser.add_argument( "--diff", metavar="", @@ -301,10 +293,14 @@ def main(): ## Generate files and show summary. if args.json is not None: - gen_json(args.json, serialize_reccmp_report(report)) + # If we're on a diet, hold the diff. + diff_included = not bool(args.json_diet) + gen_json( + args.json, serialize_reccmp_report(report, diff_included=diff_included) + ) if args.html is not None: - gen_html(args.html, serialize_reccmp_report(report, diff_included=True)) + write_html_report(args.html, report) implemented_funcs = function_count diff --git a/tests/test_report.py b/tests/test_report.py index 11510d3c..71a71f47 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -116,3 +116,21 @@ def test_aggregate_different_files(): with pytest.raises(ReccmpReportSameSourceError): combine_reports([x, y]) + + +def test_aggregate_recomp_addr(): + """We combine the entity data based on the orig addr because this will not change. + The recomp addr may vary a lot. If it is the same in all samples, use the value. + Otherwise use a placeholder value.""" + x = create_report([("100", 0.8), ("200", 0.2)]) + y = create_report([("100", 0.2), ("200", 0.8)]) + # These recomp addrs match: + x.entities["100"].recomp_addr = "500" + y.entities["100"].recomp_addr = "500" + # Y report has no addr for this + x.entities["200"].recomp_addr = "600" + + combined = combine_reports([x, y]) + assert combined.entities["100"].recomp_addr == "500" + assert combined.entities["200"].recomp_addr != "600" + assert combined.entities["200"].recomp_addr == "various"