aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_trace.py45
-rw-r--r--tests/test_trace_ui.py34
-rw-r--r--tests/test_trace_vs_tenet.py21
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 = []