OptMem/test.py

162 lines
6.1 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""OptMem invariants, checked against a synthetic life of 5000 memories.
Uses a fake compressor (join + truncate) so the run is deterministic and free.
"""
import datetime
import os
import shutil
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, HERE)
from blocks import complete, cover # noqa: E402
N = 2000
WAKE_LINES = 320
ok, fail = 0, 0
def check(cond, msg):
global ok, fail
if cond:
ok += 1
else:
fail += 1
print("FAIL: " + msg)
# ---- pure block math -------------------------------------------------
for T in list(range(1, 400)) + [1000, 4096, 10000, 65536, 100003]:
c = cover(T, WAKE_LINES)
check(len(c) <= WAKE_LINES, "T=%d: %d lines > budget" % (T, len(c)))
check(c[0][0] == 0 and c[-1][1] == T, "T=%d: does not span [0,T)" % T)
for a, b in zip(c, c[1:]):
check(a[1] == b[0], "T=%d: gap or overlap at %s %s" % (T, a, b))
for lo, hi in c:
s = hi - lo
check(s & (s - 1) == 0 and lo % s == 0,
"T=%d: [%d,%d) is not an aligned power-of-two block" % (T, lo, hi))
for a, b in zip(c, c[1:]):
check(b[1] - b[0] <= a[1] - a[0],
"T=%d: detail does not increase toward the present" % T)
check(cover(300, 320) == [(i, i + 1) for i in range(300)],
"under budget, memory should be verbatim")
# every block a cover ever needs must be buildable
seen = set()
for T in range(1, 3000):
seen.update(b for b in cover(T, WAKE_LINES) if b[1] - b[0] > 1)
buildable = set(complete(3000))
check(seen <= buildable, "a cover wants a block that complete() never yields")
# work never spikes: naps created by one new memory
worst, prev = 0, 0
for T in range(1, N):
cur = len(complete(T))
worst = max(worst, cur - prev)
prev = cur
check(worst <= 16, "a single memory created %d naps" % worst)
# ---- the real CLI ----------------------------------------------------
d = tempfile.mkdtemp(prefix="optmem-test-")
env = dict(os.environ, MEMORY_DIR=d)
memo = [sys.executable, os.path.join(HERE, "memo")]
def run(*args):
return subprocess.run(memo + list(args), env=env, capture_output=True,
text=True)
r = run("note", "x" * 281)
check(r.returncode == 1 and "REJECTED" in r.stderr, "over-long note accepted")
r = run("note", "two\nlines")
check(r.returncode == 1 and "REJECTED" in r.stderr, "multi-line note accepted")
r = run("note", " ")
check(r.returncode == 1, "empty note accepted")
check("no memories yet" in run("wake").stdout, "empty wake should say so")
with open(os.path.join(d, "seed.txt"), "w") as f:
day = datetime.date(2020, 1, 1)
for i in range(N):
f.write("%s memory number %d, a thing that happened\n"
% ((day + datetime.timedelta(days=i // 5)).isoformat(), i))
r = run("import", os.path.join(d, "seed.txt"))
check("imported %d" % N in r.stdout, "import failed: " + r.stdout + r.stderr)
r = run("wake")
check(r.returncode == 1 and "CANNOT WAKE" in r.stdout,
"wake must refuse while work is pending")
# sleep loop, with a fake compressor
naps = 0
r = run("sleep")
while "You woke up" not in r.stdout:
line = [l for l in r.stdout.splitlines() if l.strip().startswith("memo sleep ")]
check(bool(line), "no command offered:\n" + r.stdout + r.stderr)
if not line:
break
bid = line[0].split()[2]
body = [l.strip() for l in r.stdout.splitlines()
if l.startswith(" #") or (l.startswith(" ") and l.strip()
and not l.strip().startswith("memo"))]
r = run("sleep", bid, (" ".join(body)[:280]).strip() or "empty")
check("REJECTED" not in r.stderr, "sleep rejected a valid nap: " + r.stderr)
naps += 1
check(naps == len(complete(N)), "did %d naps, expected %d" % (naps, len(complete(N))))
r = run("wake")
check(r.returncode == 0, "wake still refuses after a full sleep")
lines = r.stdout.strip().splitlines()
check(len(lines) == WAKE_LINES, "woke with %d lines, want %d" % (len(lines), WAKE_LINES))
check(lines[-1].startswith("#%d " % (N - 1)), "newest memory not last / not raw")
check(lines[0].startswith("#0-"), "oldest line should be a summary block")
# append-only: nothing was ever rewritten
sizes = {f: os.path.getsize(os.path.join(d, f)) for f in ("LOG.txt", "TREE.txt")}
run("note", "one more thing happened today")
for f, s in sizes.items():
check(os.path.getsize(os.path.join(d, f)) >= s, "%s shrank" % f)
tree = open(os.path.join(d, "TREE.txt")).read().splitlines()
check(len(tree) == len(set(l.split()[0] for l in tree)), "a block was written twice")
# writing a block twice is refused, not duplicated
r = run("sleep", tree[0].split()[0], "attempted overwrite")
check("Already dreamt" in r.stdout, "rewriting a block was allowed")
# recall reaches memories the summaries lost
r = run("recall", "memory number 7,")
check(r.returncode == 0 and "#7 " in r.stdout, "recall missed a memory")
# a wrong summary can be dropped, with everything built on top of it
before = len(open(os.path.join(d, "TREE.txt")).read().splitlines())
logsize = os.path.getsize(os.path.join(d, "LOG.txt"))
r = run("forget", "16-32")
check("16-32" in r.stdout, "forget did not report the block: " + r.stdout + r.stderr)
gone = set(open(os.path.join(d, "TREE.txt")).read().splitlines())
check(not any(l.startswith("16-32 ") for l in gone), "forgotten block still present")
check(not any(l.startswith("0-64 ") for l in gone), "an ancestor survived a forget")
check(os.path.getsize(os.path.join(d, "LOG.txt")) == logsize, "forget touched the log")
check(run("wake").returncode == 1, "wake should refuse after a forget")
n = 0
while "You woke up" not in run("sleep").stdout:
r = run("sleep")
bid = [l for l in r.stdout.splitlines() if l.strip().startswith("memo sleep ")][0].split()[2]
run("sleep", bid, "rebuilt after forget")
n += 1
check(n == before - len(gone), "rebuilt %d blocks, forgot %d" % (n, before - len(gone)))
check(run("wake").returncode == 0, "wake still refuses after rebuilding")
r = run("forget", "999999-1000000")
check(r.returncode == 1, "forgetting a nonexistent block should fail")
shutil.rmtree(d)
print("\n%d passed, %d failed" % (ok, fail))
sys.exit(1 if fail else 0)