Logbook progress map in the style of decomp.dev: "<project> is NN.NN% decompiled", a layered Code bar (exact solid, verified lighter, readable hatched, with per-segment tooltips), a Data bar (named globals of the data addresses the code touches), Previous / Next / Latest through the history reports, a Units dropdown and filter (text, <70%, >10kb), and a canvas treemap of units (original source files, else 64 KB blocks) that drills into functions
- GET /api/logbook/progress-map: one compact document, cached until an input changes (main's HEAD, the Logbook symbol table and statistics, match/emulator/library results, facts, names, source files, globals, deferral notes); ?have=<version> answers {"same": true}
- definitions follow the Logbook's progress.py (pc102.csv category game, status decompiled or later), so the headline equals game_bytes_decompiled_percent; engine, library and unsorted code are shown but not counted
- history points now record the commit and the game-code bytes
- the page lays out once per load, filter or size change, hit-tests hover on the computed rectangles, builds the 600-entry units list only when opened, and polls once a minute only while the Logbook is visible
This commit is contained in:
1 parent
315b63b571
commit
d5eb82261e
5 files changed
+556
-4
No files matched your search
+80
-1
@@ -5,7 +5,8 @@ import subprocess
|
||||
import unittest
|
||||
|
||||
from helpers import HarborCase
|
||||
from harbor import logbook, logbook_auto
|
||||
from harbor import logbook, logbook_auto, progress_map
|
||||
from harbor.queue import FunctionQueue
|
||||
from harbor.util import HarborError
|
||||
|
||||
|
||||
@@ -58,5 +59,83 @@ class LogbookAuto(HarborCase):
|
||||
self.assertFalse(logbook_auto.refresh(self.h.project)["changed"])
|
||||
|
||||
|
||||
class ProgressMapTests(HarborCase):
|
||||
"""GET /api/logbook/progress-map: units, tiers and the headline, by the Logbook's own definitions."""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
d = self.dir
|
||||
(d / "image.bin").write_bytes(b"\xc3" * 16)
|
||||
facts = [{"address": "0x401000", "size": 100, "unit": "", "data": ["0x500010", "0x500020"]},
|
||||
{"address": "0x401100", "size": 50, "unit": ""}, {"address": "0x401200", "size": 30, "unit": ""},
|
||||
{"address": "0x402000", "size": 40, "unit": "engine/NiFoo"},
|
||||
{"address": "0x403000", "size": 20, "unit": ""}, {"address": "0x404000", "size": 10, "unit": ""}]
|
||||
(d / "facts.jsonl").write_text("\n".join(json.dumps(f) for f in facts), encoding="utf-8")
|
||||
(d / "sections.json").write_text(json.dumps([{"name": ".text", "start": 0x401000, "end": 0x500000},
|
||||
{"name": ".data", "start": 0x500000, "end": 0x501000}]),
|
||||
encoding="utf-8")
|
||||
ann = d / "annotations"
|
||||
ann.mkdir()
|
||||
(ann / "names.csv").write_text("address,name,category,note\n0x00401000,First,game,x\n", encoding="utf-8")
|
||||
(ann / "globals.csv").write_text("address,name,type,comment\n0x00500010,g_one,int,x\n", encoding="utf-8")
|
||||
symbols = d / "stats" / "re" / "symbols"
|
||||
symbols.mkdir(parents=True)
|
||||
rows = [("0x401000", "First", "game", "decompiled"), ("0x401100", "Second", "game", "decompiled"),
|
||||
("0x401200", "", "game", "classified"), ("0x402000", "NiFoo_Bar", "engine", "classified"),
|
||||
("0x403000", "", "platform", "classified"), ("0x404000", "", "unsorted", "unknown")]
|
||||
(symbols / "pc102.csv").write_text("address,end,size,name,category,unit,status,xbox,notes\n" + "".join(
|
||||
f"{a},,0,{n},{c},,{s},,\n" for a, n, c, s in rows), encoding="utf-8")
|
||||
(d / "check.json").write_text(json.dumps([{"address": "0x401000", "status": "exact"}]), encoding="utf-8")
|
||||
(d / "diffrun.json").write_text(json.dumps([{"address": "0x401100", "verdict": "same"}]), encoding="utf-8")
|
||||
(d / "sources.csv").write_text("pc_address,pc_name,source_file,lines,xbox_address,confidence,kind,evidence\n"
|
||||
"0x401000,,a.cpp,,,high,,\n0x401100,,a.cpp,,,high,,\n", encoding="utf-8")
|
||||
self.h.project.reverse = {"image": str(d / "image.bin"), "facts": str(d / "facts.jsonl"),
|
||||
"annotations": str(ann), "match_results": str(d / "check.json"),
|
||||
"diffrun_results": str(d / "diffrun.json"), "source_files": str(d / "sources.csv")}
|
||||
self.h.project.logbook = {"dir": str(d / "logbook"), "stats_repo": str(d / "stats")}
|
||||
logbook.set_stats(self.h.project, {"functions": 6, "commit": "abc1234", "game_bytes_decompiled_percent": 83.33,
|
||||
"by_category": {"game": {"functions": 3, "bytes": 180}}})
|
||||
self.old_ttl, progress_map.KEY_TTL = progress_map.KEY_TTL, 0
|
||||
self.map = progress_map.ProgressMap(self.h.project, lambda: FunctionQueue(self.h))
|
||||
|
||||
def tearDown(self):
|
||||
progress_map.KEY_TTL = self.old_ttl
|
||||
progress_map._MAPS.clear()
|
||||
super().tearDown()
|
||||
|
||||
def test_headline_units_tiers_and_cache(self):
|
||||
doc = self.map.get()
|
||||
self.assertEqual((doc["percent"], doc["logbook_percent"]), (83.33, 83.33)) # 150 of 180 game bytes
|
||||
self.assertEqual(doc["code"], {"exact": 100, "verified": 50, "readable": 0, "bytes": 180, "functions": 3,
|
||||
"decompiled_functions": 2})
|
||||
self.assertEqual((doc["data"]["named"], doc["data"]["addresses"]), (1, 2))
|
||||
self.assertEqual(doc["commit"]["sha"], "abc1234")
|
||||
self.assertEqual(doc["history"][-1][2:], ["abc1234", 180]) # history points keep the commit
|
||||
units = {u[0]: dict(zip(doc["unit_fields"], u)) for u in doc["units"]}
|
||||
self.assertEqual(set(units), {"a.cpp", "game · 0x400000 block (no source file)", "engine/NiFoo",
|
||||
"platform · 0x400000 block (no source file)",
|
||||
"unsorted · 0x400000 block (no source file)"})
|
||||
self.assertEqual((units["a.cpp"]["kind"], units["a.cpp"]["counted"], units["a.cpp"]["exact"]),
|
||||
("game", 150, 100))
|
||||
self.assertEqual(units["engine/NiFoo"]["kind"], "engine")
|
||||
status = {f[1]: doc["statuses"][f[3]] for f in doc["functions"]}
|
||||
self.assertEqual(status, {0x401000: "exact", 0x401100: "verified", 0x401200: "raw", 0x402000: "engine",
|
||||
0x403000: "library", 0x404000: "unsorted"})
|
||||
self.assertEqual({f[1]: f[4] for f in doc["functions"]}[0x401000], "First")
|
||||
self.assertEqual(self.map.get(doc["version"]), {"version": doc["version"], "same": True})
|
||||
self.assertEqual(self.map.computations, 1)
|
||||
FunctionQueue(self.h).defer("Claude", "0x401200", "x87", "precision differs") # a new note: recomputed
|
||||
doc2 = self.map.get(doc["version"])
|
||||
self.assertNotEqual(doc2["version"], doc["version"])
|
||||
self.assertEqual(doc2["statuses"][next(f for f in doc2["functions"] if f[1] == 0x401200)[3]], "deferred")
|
||||
|
||||
def test_endpoint(self):
|
||||
progress_map._MAPS[self.h.project.name] = self.map
|
||||
status, body = progress_map.handle(self.h.project, {})
|
||||
self.assertEqual((status, body["percent"]), (200, 83.33))
|
||||
self.assertEqual(progress_map.handle(self.h.project, {"have": body["version"]})[1]["same"], True)
|
||||
self.h.project.reverse = {}
|
||||
self.assertEqual(progress_map.handle(self.h.project, {})[0], 404)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user