- 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
142 lines
8.5 KiB
Python
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()
|