From b22b8c3f6c5378b47c36370225d7c41d4394bb7c Mon Sep 17 00:00:00 2001 From: eunhhu Date: Sun, 18 Oct 2026 19:30:16 +0100 Subject: Module format v7, 027's 4b-2: keyed documents gone, a keyed list of records holding bytes doing their work: no collections table, DOC, DOC_SIZE or REPLACE (106 opcodes now 103, those from LEN on moved up), keyed messages or a base revision (pure_app_message takes an input and a payload), no PureData document callbacks (a data host opens states and saves them together), no burst by document key; the header 38 bytes; pure-edit-module gone; pure-serve without /objects, ETag, If-Match and If-None-Match, 412 or an app a thread for a module of documents alone (one without states or outputs runs so); a hole closed: an instruction in no handler's, view's or function's block was never verified yet the load's later passes read its arguments (a damaged module read out of bounds once the header moved what the mutation test damaged), now blanked to CONST 0; checks moved: server_documents (DOCS_SOURCE, keyed byte documents with the app's revision, against a Python model, one and three connection threads, a restart) in place of the parity with editor.pure, pipelining and bursts through kill -9 on its inputs and views, the lent budget on a text view (lending a document's read is server_lent_views' already), clocks and tokens on it; message tests without a document host; the HTTP benchmark's document suite runs the earlier document server and the baselines alone; the count harness builds against either message API (runs.sh build: -DPURE_KEYED_MESSAGES before 4b); counts: b33 against b32 with batches within 0.002% but BFS -0.33%, without batches BFS +0.63%, knapsack +0.52%, join +0.38%; the bank's queries (s32 on a v6 module, s33 on v7) field +0.68%, found +0.44%, large +0.42%, the rest within 0.13%; 008 as v7, 027's 4b-2 as made and 4c designed --- tests/check.py | 425 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 230 insertions(+), 195 deletions(-) (limited to 'tests') diff --git a/tests/check.py b/tests/check.py index 8814add..1e14e12 100644 --- a/tests/check.py +++ b/tests/check.py @@ -3,14 +3,14 @@ aup builds (the sibling checkout, --aup-root). The core's and the store's own tests are core/test-module.c and store/test-tree-store.c. -1. Documents: pure-serve running the document editor written in aup - (aup's examples/editor.aup) answers random requests exactly as the earlier - document server running editor.pure, with one and with three connection +1. Documents: keyed byte documents as a keyed list (DOCS_SOURCE, each edit + checked against the revision it was made on) answer random edits and + reads exactly as a model says, with one and with three connection threads, on persistent and pipelined connections too. -2. Lending: a read lends the stored extents and still carries its version - while the document changes; lent answers count against the payload budget; - a text view is lent while its answer waits and let go when it is read or - its connection closes, the store keeping one mapping again. +2. Lending: a text view lends the state's stored extents while its answer + waits, each answer whole in one version while the text changes, and lets + them go when it is read or its connection closes, the store keeping one + mapping again; lent answers count against the payload budget. 3. Durability: concurrent edits share commits, and every acknowledged edit survives kill -9, with one and with three threads; so do the app's acknowledged posts (keyed lists, JSON bodies with groups). @@ -39,8 +39,8 @@ from pathlib import Path HERE = Path(__file__).resolve().parent ROOT = HERE.parent -# The app that exercises documents: the editor written in aup, from aup's examples. -EDITOR = ROOT.parent / 'aup' / 'examples' / 'editor.aup' +# aup's examples (its checkout beside this one, or --aup-root). +EXAMPLES = ROOT.parent / 'aup' / 'examples' def run(*args, stdin=None, check=True): @@ -67,19 +67,6 @@ class HttpHost: self.info = json.loads(line) # its port, name, public key self.port = self.info['port'] - def request(self, method, key, body=b'', expected=None): - import http.client - headers = {'Content-Length': str(len(body))} - if expected is not None: - headers['If-None-Match' if expected == 0 else 'If-Match'] = '*' if expected == 0 else f'"{expected}"' - connection = http.client.HTTPConnection('127.0.0.1', self.port, timeout=30) - try: - connection.request(method, f'/objects/{key}', body, headers) - response = connection.getresponse() - return response.status, response.getheader('ETag'), response.getheader('X-Pure-Error'), response.read() - finally: - connection.close() - def stop(self): self.process.terminate() _, error = self.process.communicate(timeout=30) @@ -87,48 +74,146 @@ class HttpHost: return [json.loads(line) for line in error.decode().splitlines() if line.startswith('{')] -def server_parity(aup, pure, work, seeds, operations=150, threads=1): - """The document server running editor.aup answers every request exactly - as the server running editor.pure: status, revision, error and body.""" - import struct - module = work / 'editor-server.pmod' - compile_module(aup, EDITOR, module) +DOCS_SOURCE = """record Doc { + id: text + body: bytes + revision: i64 +} +state docs: list of Doc by id + +input edit = message { + id: text + base: i64 + start: u32 + remove: u32 + insert: bytes +} + +on edit { + if docs.has(edit.id) { + let d = docs[edit.id] + require d.revision == edit.base + require edit.start + edit.remove <= d.body.bytes + docs[edit.id].body = d.body[0 .. edit.start] + edit.insert + d.body[edit.start + edit.remove .. d.body.bytes] + docs[edit.id].revision = d.revision + 1 + } else { + require edit.base == 0 && edit.start == 0 && edit.remove == 0 + docs.add(Doc { id: edit.id, body: edit.insert, revision: 1 }) + } + reply edit.insert +} + +view revision(id: text) = docs[id].revision +view size(id: text) = docs[id].body.bytes +view body(id: text) = text(docs[id].body) +""" + + +def edit_body(key, base, start, remove, insert): + """An edit of DOCS_SOURCE as its JSON body.""" + return json.dumps({'id': key, 'base': base, 'start': start, 'remove': remove, 'insert': insert}).encode() + + +def edit_request(key, base, start, remove, insert): + body = edit_body(key, base, start, remove, insert) + return (b'POST /inputs/edit HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\nContent-Length: ' + + str(len(body)).encode() + b'\r\n\r\n' + body) + + +def view_request(view, key): + from urllib.parse import quote + return f'GET /views/{view}?id={quote(key)} HTTP/1.1\r\nHost: x\r\n\r\n'.encode() + + +def docs_call(port, request): + """One raw request on its own connection: status, X-Pure-Error, body.""" + import socket + with socket.create_connection(('127.0.0.1', port), timeout=30) as sock: + sock.sendall(request) + return read_answer(sock.makefile('rb')) + + +def read_answer(reader): + status = int(reader.readline().split()[1]) + length, error = 0, None + for line in iter(reader.readline, b'\r\n'): + name, _, value = line.decode().partition(':') + if name.lower() == 'content-length': length = int(value) + if name.lower() == 'x-pure-error': error = value.strip() + return status, error, reader.read(length) + + +class DocsModel: + """DOCS_SOURCE as Python: an edit's answer (status, error, body) and each document's revision and body.""" + + def __init__(self): + self.docs = {} + + def edit(self, key, base, start, remove, insert): + body, revision = self.docs.get(key, (b'', 0)) + data = insert.encode() + if key in self.docs: + if base != revision or start + remove > len(body): return 422, 'limit', b'' + self.docs[key] = (body[:start] + data + body[start + remove:], revision + 1) + elif base or start or remove: + return 422, 'limit', b'' + else: + self.docs[key] = (data, 1) + return 200, 'none', data + + def view(self, view, key): + if key not in self.docs: return 404, 'no_value', b'' + body, revision = self.docs[key] + if view == 'body': + try: + body.decode() + except UnicodeDecodeError: # cut inside a character: text(...) refuses it + return 400, 'invalid', b'' + return 200, 'none', body + return 200, 'none', str(revision if view == 'revision' else len(body)).encode() + + +def server_documents(aup, pure, work, seeds, operations=150, threads=1): + """Keyed byte documents as a keyed list (027's 4b; DOCS_SOURCE): random + edits (current, stale and future revisions, past the end, texts of any + size up to 5,000 bytes) and reads of a document's revision, size and + body answer exactly as a model says, with one and with three connection + threads; after a restart every document is as acknowledged.""" + module = work / 'docs.pmod' + (work / 'docs.aup').write_text(DOCS_SOURCE) + compile_module(aup, work / 'docs.aup', module) compared = 0 + words = ['', 'a', 'hello', '빨리', 'a "quoted" \\ word', '😀', 'line\nbreak'] for seed in seeds: rng = random.Random(seed + 13000) - old = HttpHost([pure / 'prototypes/connections/build/tree/pure-connections', '--program', pure / 'prototypes/connections/examples/editor.pure', - '--store', work / f'http-old-{seed}-{threads}']) - new = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / f'http-new-{seed}-{threads}', - '--threads', str(threads)]) + store = work / f'docs-{seed}-{threads}' + model = DocsModel() + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(threads)]) try: - revision, size = {}, {} for _ in range(operations): - key = rng.choice(['a', 'b', 'long-key_1']) - current, length = revision.get(key, 0), size.get(key, 0) + key = rng.choice(['a', 'b', 'long-key_1', '문서']) + body, current = model.docs.get(key, (b'', 0)) if rng.random() < 0.35: - request = ('GET', key, b'', None) + view = rng.choice(['revision', 'size', 'body']) + want, got = model.view(view, key), docs_call(host.port, view_request(view, key)) else: pick = rng.random() base = current if pick < 0.75 else max(current - 1, 0) if pick < 0.9 else current + 3 - if rng.random() < 0.05: - body = bytes(rng.randrange(256) for _ in range(rng.randint(0, 7))) # too short - else: - insert = bytes(rng.randrange(256) for _ in range(rng.choice([0, 1, 7, 300, 5000]))) - body = struct.pack(' stats['commit_groups'], \ - (statuses, alone, stats) + host.stop() + assert statuses == [200] * 80 and alone < 1, (statuses, alone) seen['alone_after_groups_s'] = round(alone, 4) return seen @@ -5790,7 +5825,7 @@ def server_view_bodies(aup, pure, work): import random import threading module = work / 'routes-view.pmod' - compile_module(aup, EDITOR.parent / 'routes.aup', module) + compile_module(aup, EXAMPLES / 'routes.aup', module) rng = random.Random(18) n, edges = 300, set() while len(edges) < 1200: @@ -6268,8 +6303,9 @@ def server_tokens(aup, pure, work): for name, text in (('tokens', TOKENS_SOURCE), ('tokens-sender', TOKENS_SENDER)): (work / f'{name}.aup').write_text(text) compile_module(aup, work / f'{name}.aup', work / f'{name}.pmod') - editor = work / 'tokens-editor.pmod' - compile_module(aup, EDITOR, editor) + editor = work / 'tokens-docs.pmod' + (work / 'tokens-docs.aup').write_text(DOCS_SOURCE) + compile_module(aup, work / 'tokens-docs.aup', editor) serve = pure / 'build/pure-serve' names = {'read': 'r' * 40, 'write': 'w' * 40, 'input=add': 'i' * 40, 'read2': 'q' * 40} lengths = {n: bytes(65 + (k * 7) % 57 for k in range(n)).decode() for n in (1, 55, 56, 63, 64, 65, 119, 200)} @@ -6306,7 +6342,7 @@ def server_tokens(aup, pure, work): try: none = call('GET', '/t/views/total') assert none[:3] == (401, 'unauthorized', 'Bearer'), none - for method, path in (('POST', '/t/inputs/add?step=1'), ('GET', '/docs/objects/k'), ('PUT', '/t/queries/q'), + for method, path in (('POST', '/t/inputs/add?step=1'), ('GET', '/docs/views/body?id=k'), ('PUT', '/t/queries/q'), ('GET', '/t/queries/q')): assert call(method, path)[:2] == (401, 'unauthorized'), (method, path) assert call('GET', '/t/views/total', 'x' * 40)[:2] == (401, 'unauthorized') @@ -6314,11 +6350,11 @@ def server_tokens(aup, pure, work): assert call('GET', '/t/views/total', r)[0] == 200 assert call('GET', '/t/queries/q', r)[:2] == (404, 'no_value') # read, and there is none assert call('POST', '/t/inputs/add?step=1', r)[:2] == (403, 'forbidden') - assert call('PUT', '/docs/objects/k', r, b'x', {'If-None-Match': '*'})[:2] == (403, 'forbidden') + json_type = {'Content-Type': 'application/json'} + assert call('POST', '/docs/inputs/edit', r, edit_body('k', 0, 0, 0, 'x'), json_type)[:2] == (403, 'forbidden') assert call('POST', '/t/inputs/add?step=1', w)[0] == 200 - edit = struct.pack('