Files
HarborFork/tests/test_logbook.py
T
KeelrLeek d5eb82261e 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
2026-10-06 15:46:42 -05:00

142 lines
8.5 KiB
Python

"""The Logbook store and its automatic commit log."""
import json
import shutil
import subprocess
import unittest
from helpers import HarborCase
from harbor import logbook, logbook_auto, progress_map
from harbor.queue import FunctionQueue
from harbor.util import HarborError
class LogbookStore(HarborCase):
def setUp(self):
super().setUp()
self.h.project.logbook = {"dir": str(self.dir / "logbook")}
def test_entries_stats_and_milestones(self):
e = logbook.add_entry(self.h.project, "First", "Something happened.", kind="tooling", at="2026-10-05T10:00:00Z")
self.assertEqual(e["id"], "20261005-1000-1")
with self.assertRaises(HarborError):
logbook.add_entry(self.h.project, "x", "y", kind="gossip")
logbook.set_stats(self.h.project, {"functions": 10, "classified_percent": 50.0, "weighted_percent": 5.0})
logbook.upsert_milestone(self.h.project, {"id": "m1", "order": 1, "code": "M1", "title": "T", "items": []})
data = logbook.read_all(self.h.project)
self.assertEqual((data["log"][0]["title"], data["stats"]["functions"], data["history"][0]["classified"],
data["milestones"][0]["code"]), ("First", 10, 50.0, "M1"))
class LogbookAuto(HarborCase):
def setUp(self):
super().setUp()
self.repo = self.dir / "repo"
self.repo.mkdir()
git = lambda *a: subprocess.run(["git", "-C", str(self.repo), *a], check=True, capture_output=True)
git("init", "-q")
git("config", "user.name", "T")
git("config", "user.email", "t@example.com")
(self.repo / "a.txt").write_text("1", encoding="utf-8")
git("add", "-A")
git("commit", "-q", "-m", "first")
self.first = subprocess.run(["git", "-C", str(self.repo), "rev-parse", "HEAD"], capture_output=True,
text=True).stdout.strip()
(self.repo / "annotations").mkdir()
(self.repo / "annotations" / "names.csv").write_text("x", encoding="utf-8")
git("add", "-A")
git("commit", "-q", "-m", "Name things", "-m", "Body line one.\nCo-Authored-By: someone")
self.h.project.logbook = {"dir": str(self.dir / "logbook"), "decomp": str(self.repo)}
def test_new_commits_become_entries_once(self):
logbook_auto._save_state(self.h.project, {"last_head": self.first})
out = logbook_auto.refresh(self.h.project)
self.assertEqual(out["entries"], 1)
entry = logbook.read_all(self.h.project)["log"][0]
self.assertEqual((entry["title"], entry["kind"]), ("Name things", "naming"))
self.assertIn("Body line one.", entry["detail"])
self.assertNotIn("Co-Authored-By", entry["detail"])
self.assertTrue(entry["at"].endswith("Z"))
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()