aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-18 19:30:16 +0100
committereunhhu <[email protected]>2026-10-18 19:30:16 +0100
commit26658fea927aa87e861c936b2d7aa4163d38bdb4 (patch)
tree860799d514d6705bc4d0d11415ade1eda7dcb08a /tests
parent8709c7fdb9d18e30e6bb776f536aa230389fa527 (diff)
downloadaup-26658fea927aa87e861c936b2d7aa4163d38bdb4.tar.gz
aup-26658fea927aa87e861c936b2d7aa4163d38bdb4.zip
Module format v7 (Pure 027's 4b-2): keyed documents gone from the language: no bytes by key, message by key, name[key] or .replace(first .. last, value), the opcodes DOC, DOC_SIZE and REPLACE (those after them moved up), the header 38 bytes; examples/editor.aup and bounded-editor.aup gone (examples/documents.aup does their work in a keyed list), and with them the parity with the earlier editor (documents_model checks the new one); diagnostics on documents rewritten for plain messages or gone (304); a module's records counted at their v7 place; a field changed by '=' said so where a call was written
Diffstat (limited to 'tests')
-rw-r--r--tests/check.py122
1 files changed, 18 insertions, 104 deletions
diff --git a/tests/check.py b/tests/check.py
index 26cc3f8..444c444 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -28,10 +28,10 @@ builds its apps with this compiler.)
module while the source has an error (a library's error too), saving the
same bytes builds nothing, a file caught halfway through being written is
not built, and a module is only ever replaced whole.
-5. Editor parity: the document editor written in aup, run by pure-edit-module,
- answers random show/edit/apply operations exactly as pure-edit running the
- original editor.pure (stdout, stderr, exit code), on the same tree store
- format, and each CLI reads the other's store identically.
+5. Documents: examples/documents.aup (keyed byte documents in a keyed list,
+ each edit checked against the revision it was made on) keeps its documents
+ and faults exactly as a reference model, in memory and on a tree store
+ restarting at random points.
"""
import argparse
import json
@@ -171,7 +171,6 @@ DIAGNOSTICS = [
('state if = 0\n', '1:7', "'if' is reserved"),
('state transient = 0\n', '1:7', "'transient' is reserved"),
('transient x = 0\n', '1:11', "expected 'state' after 'transient'"),
- ('transient state docs: bytes by key\n', '1:17', 'a keyed collection is stored; only a state can be transient'),
('transient state note: text\nstate at: cursor in note\n', '2:7',
"'at' is stored but points into transient 'note'; make it transient too"),
('record R {\n n = 0\n}\ntransient state rs: list of R\nstate at: index of rs\n', '5:7',
@@ -187,17 +186,14 @@ DIAGNOSTICS = [
'1:14', "program exceeds 65535 instructions (in fn 'f0' called at 2:15)"),
('view v = ' + '(' * 70 + '1' + ')' * 70 + '\n', '1:', 'expression nesting exceeds 64'),
('state s = 0\n' * 1 + ''.join(f'state s{i} = 0\n' for i in range(32)), '33:7', 'at most 32 states are allowed'),
- # v0b: messages, keyed documents and types
- ('state d: bytes by key\ninput m = message\non m {\n reply d[key]\n}\n', '4:9', 'd[key] needs the handler of a message by key'),
- ('input m = message by key { a: u32 }\non m {\n reply m.a\n}\n', '3:9', 'reply takes bytes or text, not a number'),
+ # v0b: messages and types
+ ('input m = message { a: u32 }\non m {\n reply m.a\n}\n', '3:9', 'reply takes bytes or text, not a number'),
('input m = message {\n a: float\n}\n', '2:6', 'field types are u32, i64, number, bool, bytes, text and list of a record'),
- ('input m = message by key\non m.down {\n}\n', '2:4', "'m' is a message; write on m { }"),
+ ('input m = message\non m.down {\n}\n', '2:4', "'m' is a message; write on m { }"),
('input k = key Space\non k {\n}\n', '2:4', "'k' is a key input; write on k.down or on k.up"),
- ('state d: bytes by key\ninput m = message by key { a: bytes }\non m {\n d[1].replace(0 .. 0, m.a)\n}\n', '4:5',
- 'a collection is indexed by the message key'),
- ('input m = message by key { a: u32 }\nview v = m.a\n', '2:10', "fields of 'm' are only available in its handler"),
- ('state x = 0\ninput m = message by key { a: bytes }\non m {\n x = m.a\n}\n', '4:7', 'expected a number, not bytes'),
- ('input m = message by key { a: u32 }\non m {\n reply m.z\n}\n', '3:9', "message 'm' has no field 'z'"),
+ ('input m = message { a: u32 }\nview v = m.a\n', '2:10', "fields of 'm' are only available in its handler"),
+ ('state x = 0\ninput m = message { a: bytes }\non m {\n x = m.a\n}\n', '4:7', 'expected a number, not bytes'),
+ ('input m = message { a: u32 }\non m {\n reply m.z\n}\n', '3:9', "message 'm' has no field 'z'"),
('input m = message {\n g: {\n a: text\n }\n}\non m {\n reply m.g\n}\n', '7:9',
"'m.g' is a group of fields; read one of them, as in m.g.a"),
('input m = message {\n g: {\n a: text\n }\n}\non m {\n reply m.g.b\n}\n', '7:9', "message 'm' has no field 'g.b'"),
@@ -218,10 +214,7 @@ DIAGNOSTICS = [
('input m = message {\n group_of_things: {\n another_long_name: text\n }\n}\n', '3:5',
"'group_of_things.another_long_name' is longer than 31 characters"),
('state x = 0\non frame {\n require x.bytes\n}\n', '3:11', "a number has no 'bytes'"),
- ('state d: bytes by key\nstate e: bytes by key\n', '2:7', 'at most one keyed collection is allowed'),
- ('state d: bytes by key\ninput m = message by key { a: bytes }\non m {\n d[key].replace(0 .. m.a, m.a)\n}\n', '4:23',
- 'expected a number, not bytes'),
- ('input m = message by key { a: u32 }\non frame {\n reply m.a\n}\n', '3:3', 'reply is only available in a message handler'),
+ ('input m = message { a: u32 }\non frame {\n reply m.a\n}\n', '3:3', 'reply is only available in a message handler'),
# text and cursors
('state at: cursor in missing\n', '1:7', "a cursor moves in a text state; 'missing' is not one"),
('state a: text\nstate b: text\nstate at: cursor in a\ninput k = key KeyK\non k.down {\n b.insert(at, "x")\n}\n',
@@ -645,15 +638,13 @@ ACCEPTED = [
'state a = 0\non frame {\n if a > 1 {\n a = 1\n }\n else {\n a = 2\n }\n}\n',
'state a = 0; state b = 1\nview v = clamp(\n a,\n 0,\n b\n)\non frame { a = a + 1; b = b + 2 }\n',
'# empty handlers and comments only\ninput k = key KeyZ\non k.down {\n}\non k.up { }\n',
- 'state d: bytes by key\nstate edits = 0\ninput m = message by key { at: u32; text: bytes }\n'
- 'input peek = message by key\non m {\n d[key].replace(at_or_end .. at_or_end, m.text)\n}\n'.replace('at_or_end', 'min(m.at, d[key].bytes)')
- + 'on m {\n edits = edits + 1\n require m.text.bytes > 0\n}\non peek { reply d[key] }\n',
- # a keyed document loaded into a text state, edited with cursors and saved back as its UTF-8 bytes
- 'state documents: bytes by key\nstate note: text\nstate at: cursor in note\n'
- 'input open = message by key\ninput save = message by key\ninput typed = text\n'
- 'on open {\n note = text(documents[key])\n at = note.end\n}\n'
+ # a keyed list's document loaded into a text state, edited with cursors and saved back as its UTF-8 bytes
+ 'record Doc {\n id: text\n body: bytes\n}\nstate docs: list of Doc by id\nstate note: text\nstate at: cursor in note\n'
+ 'input open = message { id: text }\ninput save = message { id: text }\ninput typed = text\n'
+ 'on open {\n note = text(docs[open.id].body)\n at = note.end\n}\n'
'on typed { note.insert(at, typed) }\n'
- 'on save {\n documents[key].replace(0 .. documents[key].bytes, note)\n reply "saved {note.chars} characters"\n}\n',
+ 'on save {\n if docs.has(save.id) { docs[save.id].body = bytes(note) } else { docs.add(Doc { id: save.id, body: bytes(note) }) }\n'
+ ' reply "saved {note.chars} characters"\n}\n',
# a record literal over several lines, text defaults, inserting at the first item
'record Item {\n name: text = "untitled"\n price = 0; count = -1\n}\n'
'state items: list of Item\nstate at: index of items\ninput add = key KeyA\n'
@@ -700,11 +691,6 @@ def run(*args, stdin=None, check=True):
check=check, timeout=60)
-def run_bytes(*args):
- """Document bytes are arbitrary; compare outputs as bytes."""
- return subprocess.run([str(a) for a in args], capture_output=True, check=False, timeout=60)
-
-
def compile_module(aup, source_path, out):
run(aup, 'build', source_path, '-o', out)
return out.read_bytes()
@@ -894,75 +880,6 @@ def properties(aup, pure_run, work, rounds):
return accepted_damaged, over_limit
-def editor_parity(aup, pure, work, seeds, operations=160):
- """Differential run of the old and the new editor on separate stores."""
- old, new = pure / 'prototypes/connections/build/tree/pure-edit', pure / 'build/pure-edit-module'
- variants = [('examples/editor.aup', pure / 'prototypes/connections/examples/editor.pure'),
- ('examples/bounded-editor.aup', pure / 'prototypes/connections/examples/bounded-editor.pure')]
- compared, outcomes = 0, {}
- for source, program in variants:
- module = work / (Path(source).stem + '.pmod')
- compile_module(aup, ROOT / source, module)
- for seed in seeds:
- rng = random.Random(seed * 31 + len(source))
- stores = {'old': work / f'old-{seed}', 'new': work / f'new-{seed}'}
- for store in stores.values():
- if store.exists():
- for path in sorted(store.rglob('*'), reverse=True):
- path.unlink() if path.is_file() else path.rmdir()
- revision, size = {}, {}
- keys = ['a', 'b', 'long-key_1']
- for step in range(operations):
- key = rng.choice(keys) if rng.random() > 0.03 else 'bad.key'
- current, length = revision.get(key, 0), size.get(key, 0)
- choice = rng.random()
- pick = rng.random()
- base = (current if pick < 0.7 else max(current - 1, 0) if pick < 0.8 else 0 if pick < 0.9
- else rng.choice([7, 9223372036854775807]))
- budget = ['--budget', str(rng.choice([4, 16]))] if rng.random() < 0.05 else []
- if choice < 0.35:
- command = ['show', key]
- elif choice < 0.85:
- offset = rng.randint(0, length + 3)
- remove = rng.choice([0, 0, 1, 2, 5, length])
- body = bytes(rng.randrange(256) for _ in range(rng.choice([0, 1, 5, 50, 5000])))
- insert = work / f'insert-{step}'
- insert.write_bytes(body)
- command = ['edit', key, str(base), str(offset), str(remove), str(insert)]
- else:
- if rng.random() < 0.5:
- payload = bytes(rng.randrange(256) for _ in range(rng.randint(0, 12)))
- else:
- payload = (rng.randint(0, length).to_bytes(4, 'little') +
- rng.randint(0, 3).to_bytes(4, 'little') + b'raw')
- raw = work / f'raw-{step}'
- raw.write_bytes(payload)
- command = ['apply', key, str(base), str(raw)]
- theirs = run_bytes(old, '--program', program, '--store', stores['old'], *budget, *command)
- ours = run_bytes(new, '--module', module, '--store', stores['new'], *budget, *command)
- where = f'{source} seed {seed} step {step}: {command}'
- assert (ours.returncode, ours.stdout, ours.stderr) == (theirs.returncode, theirs.stdout, theirs.stderr), \
- (where, ours, theirs)
- compared += 1
- text = theirs.stderr.decode(errors='replace').strip().splitlines()
- kind = (json.loads(text[-1]).get('error', 'ok') if text and text[-1].startswith('{')
- else text[-1] if text else 'ok') if theirs.returncode else 'ok'
- outcomes[f'{command[0]}:{kind}'] = outcomes.get(f'{command[0]}:{kind}', 0) + 1
- report = theirs.stderr if command[0] == 'show' else theirs.stdout
- if theirs.returncode == 0:
- revision[key] = int(json.loads(report.decode().splitlines()[-1])['revision'])
- shown = theirs if command[0] == 'show' else \
- run_bytes(old, '--program', program, '--store', stores['old'], 'show', key)
- size[key] = len(shown.stdout)
- # Each CLI reads the store the other one wrote.
- for key in keys:
- across = [run_bytes(old, '--program', program, '--store', stores['new'], 'show', key),
- run_bytes(new, '--module', module, '--store', stores['old'], 'show', key),
- run_bytes(old, '--program', program, '--store', stores['old'], 'show', key)]
- assert len({(r.returncode, r.stdout, r.stderr) for r in across}) == 1, (source, seed, key)
- return compared, dict(sorted(outcomes.items()))
-
-
class NotesModel:
"""Reference semantics of examples/notes.aup: positions are Unicode
scalars; every cursor at or after an insertion point moves after it; a
@@ -1195,7 +1112,7 @@ def library_records(aup, pure_run, pure, work):
'on add { teams.append(people.Team { name: add, size: 12 }) }\n'
'view big = count(t in teams where people.big(t.size))\n').returncode == 0
module = (lib / 'teams.pmod').read_bytes()
- assert struct.unpack_from('<H', module, 30)[0] == 1, struct.unpack_from('<H', module, 30)
+ assert struct.unpack_from('<H', module, 28)[0] == 1, struct.unpack_from('<H', module, 28)
lines = run(pure_run, lib / 'teams.pmod', stdin='type add 61\nshow\n').stdout.splitlines()
assert json.loads(lines[-1])['views'] == {'big': 1}, lines[-1]
# A migration: another module of the same library changes what the app stored.
@@ -2442,7 +2359,6 @@ def main():
parser.add_argument('--aup', type=Path, required=True)
parser.add_argument('--pure-run', type=Path, required=True)
parser.add_argument('--pure', type=Path, default=ROOT.parent / 'pure')
- parser.add_argument('--editor-seeds', type=int, default=4)
parser.add_argument('--text-seeds', type=int, default=12)
parser.add_argument('--workbench', type=Path, default=ROOT.parent / 'pure' / 'prototypes' / 'workbench')
parser.add_argument('--seeds', type=int, default=12)
@@ -2568,8 +2484,6 @@ def main():
report['functions'] = functions(aup, pure_run, work)
report['stored_nested'] = stored_nested(aup, pure_run, work, range(6))
report['documents'] = documents_model(aup, pure_run, work, range(4))
- report['editor_parity_operations'], report['editor_parity_outcomes'] = \
- editor_parity(aup, args.pure.resolve(), work, range(args.editor_seeds))
print(json.dumps(report))