diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_trace.py | 45 | ||||
| -rw-r--r-- | tests/test_trace_ui.py | 34 | ||||
| -rw-r--r-- | tests/test_trace_vs_tenet.py | 21 |
3 files changed, 98 insertions, 2 deletions
diff --git a/tests/test_trace.py b/tests/test_trace.py index 2803a11..58c9d81 100644 --- a/tests/test_trace.py +++ b/tests/test_trace.py @@ -87,6 +87,43 @@ def main() -> int: [o.write for o in t.memory_ops(4)] == [False]) check("an instruction with no memory has none", t.memory_ops(2) == []) + # -- memory state at a timestamp ------------------------------------ # + # The trace wrote 2a000000 at 0xff4 (t=3) and read it back (t=4); it + # pushed/popped 8 zero bytes at 0xff8 (t=1 write, t=5 read). + d, k = t.memory(0xff4, 4, 3) + check("memory reflects a write as of that timestamp", + d == bytes.fromhex("2a000000") and k == b"\x01" * 4, f"{d.hex()} {k.hex()}") + d, k = t.memory(0xff4, 4, 2) + check("and does NOT reflect it before the write happened", + k == b"\x00" * 4, f"{d.hex()} known={k.hex()}") + + # A trace only knows what it saw. A byte nobody touched is unknown, and + # must not be reported as zero — that distinction is the entire reason + # to read memory from a trace instead of from the database. + d, k = t.memory(0x5000, 4, t.length - 1) + check("untouched memory is unknown, not zero", + k == b"\x00" * 4 and d == b"\x00" * 4, f"known={k.hex()}") + # 0xff2..0xff3 was never touched; 0xff4..0xff7 came from the write at + # t=3 and 0xff8..0xff9 from the push at t=1 — a window can be knowable + # from several accesses at different times, which is what makes this + # worth a mask rather than a flag. + d, k = t.memory(0xff2, 8, 4) + check("a partially-covered window marks which bytes are known", + k == bytes([0, 0, 1, 1, 1, 1, 1, 1]), f"known={k.hex()}") + + # Reads are evidence too: an instruction reading a byte reveals what it + # held at that moment. + d, k = t.memory(0xff8, 8, 5) + check("a read reveals memory contents", + k == b"\x01" * 8, f"known={k.hex()}") + + check("memory_writes lists only the writers", + t.memory_writes(0xff4, 4) == [3], f"{t.memory_writes(0xff4, 4)}") + check("memory_accesses includes the readers", + t.memory_accesses(0xff4, 4) == [3, 4], f"{t.memory_accesses(0xff4, 4)}") + check("a never-touched range has no accesses", + t.memory_accesses(0x5000, 16) == []) + # -- execution queries: what painting is built on -------------------- # check("executions lists every timestamp for an address", list(t.executions(0x401000)) == [0, 6], f"{list(t.executions(0x401000))}") @@ -119,8 +156,12 @@ def main() -> int: check("addresses come back in database terms", t.ip(0) == 0x1000 and list(t.executions(0x1000)) == [0, 6], f"{t.ip(0):#x}") - check("memory addresses are slid too", - t.memory_ops(3)[0].addr == 0xff4 + slide) + # Memory addresses are NOT slid: the slide relocates the image, and + # these are overwhelmingly stack/heap addresses with no database + # counterpart — sliding a stack pointer by the image delta produced a + # negative address in testing. + check("memory op addresses stay in trace space", + t.memory_ops(3)[0].addr == 0xff4, f"{t.memory_ops(3)[0].addr:#x}") check("raw_ip still gives the traced address", t.raw_ip(0) == 0x401000) diff --git a/tests/test_trace_ui.py b/tests/test_trace_ui.py index 5d620a9..b672143 100644 --- a/tests/test_trace_ui.py +++ b/tests/test_trace_ui.py @@ -141,6 +141,40 @@ async def run() -> int: app._t == ret, f"{i} -> {app._t}, expected {ret}") check("which is further than a plain step", app._t > i + 1) + # -- memory at time T -------------------------------------------- # + # This is where a trace's memory actually lives: measured on two + # real traces, NONE of the accesses fell inside the image — every + # one was stack or heap. A memory view that could only address the + # image would have nothing to show. + dock = app.query_one(TraceDock) + app._seek(min(60, t.length - 1)) + await pilot.pause(0.8) + stack = str(dock.query_one("#trace-stack", Static).render()) + check("the dock shows the stack at this timestamp", + "stack (" in stack and len(stack.splitlines()) > 4, stack[:60]) + sp_name = next(r for r in ("rsp", "esp", "sp") if r in t.reg_at) + sp = t.register(sp_name, app._t) + check("anchored at the stack pointer", + f"{sp:012x}" in stack, f"sp={sp:#x} / {stack[:80]}") + + # A trace knows what it observed and nothing else. Unseen bytes are + # printed as '?', never as zeros — rendering them as zero would + # invent facts about memory nobody looked at. + data, known = t.memory_raw(sp, 8, app._t) + if not all(known): + check("memory the trace never saw is marked unknown", + "?" in stack, stack[:80]) + else: + check("known stack words are shown as values", + any(c in "0123456789abcdef" for c in stack), stack[:60]) + + # Stepping must move the memory view with time. + before = stack + app._seek(min(80, t.length - 1)) + await pilot.pause(0.8) + check("and it follows as you move through time", + str(dock.query_one("#trace-stack", Static).render()) != before) + # -- trails ------------------------------------------------------ # # Not "every address the trace ever touched": on a loop-heavy # program that's almost everything and says nothing. The last/next diff --git a/tests/test_trace_vs_tenet.py b/tests/test_trace_vs_tenet.py index 6c78c95..aa6002a 100644 --- a/tests/test_trace_vs_tenet.py +++ b/tests/test_trace_vs_tenet.py @@ -134,6 +134,27 @@ def compare(path, ref_cls, arch, dctx, samples=200): check(f"{name}: same execution timestamps for the hottest addresses", not ex_bad, f"{ex_bad[:3]}") + # Memory STATE at a timestamp — reconstructed from the deltas, which is the + # hard part and the whole point of reading memory from a trace. + mem_bad, mem_checked = [], 0 + for i in idxs[::4]: + for op in ours.memory_ops(i)[:2]: + n = min(len(op.data), 8) + mine, known = ours.memory(op.addr, n, i) + ref = theirs.get_memory(op.addr, n, i) + if ref is None: + continue + refb = bytes(ref.data) + # Compare only the bytes we claim to know; the reference reports its + # own coverage separately and a byte neither has seen is not a + # disagreement. + for j in range(n): + if known[j] and refb[j:j + 1] and mine[j] != refb[j]: + mem_bad.append((i, hex(op.addr + j), mine[j], refb[j])) + mem_checked += 1 + check(f"{name}: memory state at a timestamp matches the reference", + not mem_bad, f"{mem_bad[:3]}") + # Memory: the bytes an instruction touched, and which way. with_mem = [i for i in idxs if ours.memory_ops(i)][:40] mem_bad = [] |
