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
commitb22b8c3f6c5378b47c36370225d7c41d4394bb7c (patch)
treed1a3cbf1b0e718f0acafa08ec6741761f08e5dd5 /tests
parente8b3499289022c8729900a58ed218d961a85fec8 (diff)
downloadpure-b22b8c3f6c5378b47c36370225d7c41d4394bb7c.tar.gz
pure-b22b8c3f6c5378b47c36370225d7c41d4394bb7c.zip
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
Diffstat (limited to 'tests')
-rw-r--r--tests/check.py425
1 files changed, 230 insertions, 195 deletions
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('<II', rng.randint(0, length + 2), rng.choice([0, 1, 3, length])) + insert
- request = ('PUT', key, body, base)
- theirs, ours = old.request(*request), new.request(*request)
- assert ours == theirs, (seed, request[:2], request[3], ours[:3], theirs[:3])
- if theirs[0] == 200 and theirs[1]:
- revision[key] = int(theirs[1].strip('"'))
- if request[0] == 'GET':
- size[key] = len(theirs[3])
- else:
- size[key] = len(old.request('GET', key)[3])
- assert new.request('GET', key) == old.request('GET', key)
+ insert = ''.join(rng.choice(words) for _ in range(rng.choice([0, 1, 7, 300, 700])))[:5000]
+ start, remove = rng.randint(0, len(body) + 2), rng.choice([0, 1, 3, len(body)])
+ want = model.edit(key, base, start, remove, insert)
+ got = docs_call(host.port, edit_request(key, base, start, remove, insert))
+ assert got == want, (seed, key, got[:2], want[:2])
compared += 1
finally:
- old.stop()
- new.stop()
+ host.stop()
+ host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(threads)])
+ try:
+ for key in model.docs:
+ for view in ('revision', 'size', 'body'):
+ assert docs_call(host.port, view_request(view, key)) == model.view(view, key), (key, view)
+ finally:
+ host.stop()
return compared
@@ -136,122 +221,77 @@ def server_pipelining(aup, pure, work):
"""One connection, many requests: a keep-alive client and a pipelined
burst (all requests in one write, one split across writes) get the same
answers, in order, as separate connections would."""
- import http.client
import socket
- import struct
- module = work / 'editor-pipeline.pmod'
- compile_module(aup, EDITOR, module)
+ module = work / 'docs-pipeline.pmod'
+ (work / 'docs.aup').write_text(DOCS_SOURCE)
+ compile_module(aup, work / 'docs.aup', module)
host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'pipeline'])
+ answers = []
try:
- connection = http.client.HTTPConnection('127.0.0.1', host.port, timeout=30)
- answers = []
- for body, headers in [(struct.pack('<II', 0, 0) + b'hello', {'If-None-Match': '*'}),
- (struct.pack('<II', 5, 0) + b'!', {'If-Match': '"1"'}),
- (struct.pack('<II', 0, 0) + b'x', {'If-Match': '"1"'})]:
- connection.request('PUT', '/objects/p', body, headers)
- response = connection.getresponse()
- answers.append((response.status, response.getheader('ETag'), response.read()))
- connection.request('GET', '/objects/p')
- response = connection.getresponse()
- answers.append((response.status, response.getheader('ETag'), response.read()))
- connection.close()
- assert answers == [(200, '"1"', b'hello'), (200, '"2"', b'!'), (412, '"2"', b''), (200, '"2"', b'hello!')], answers
-
- def request(method, key, body=b'', condition=''):
- return (f'{method} /objects/{key} HTTP/1.1\r\nHost: x\r\nContent-Length: {len(body)}\r\n{condition}\r\n'
- .encode() + body)
- pipeline = (request('GET', 'p') + request('PUT', 'q', struct.pack('<II', 0, 0) + b'q1', 'If-None-Match: *\r\n') +
- request('GET', 'q') + request('GET', 'missing') +
- request('PUT', 'q', struct.pack('<II', 2, 0) + b'!', 'If-Match: "1"\r\n') + request('GET', 'q'))
- expected = [(200, b'hello!'), (200, b'q1'), (200, b'q1'), (404, b''), (200, b'!'), (200, b'q1!')]
+ with socket.create_connection(('127.0.0.1', host.port), timeout=30) as sock:
+ reader = sock.makefile('rb')
+ for request in (edit_request('p', 0, 0, 0, 'hello'), edit_request('p', 1, 5, 0, '!'),
+ edit_request('p', 1, 0, 0, 'x'), view_request('body', 'p')):
+ sock.sendall(request)
+ answers.append(read_answer(reader))
+ assert answers == [(200, 'none', b'hello'), (200, 'none', b'!'), (422, 'limit', b''), (200, 'none', b'hello!')], answers
+ pipeline = (view_request('body', 'p') + edit_request('q', 0, 0, 0, 'q1') + view_request('body', 'q') +
+ view_request('body', 'missing') + edit_request('q', 1, 2, 0, '!') + view_request('body', 'q'))
+ expected = [(200, 'none', b'hello!'), (200, 'none', b'q1'), (200, 'none', b'q1'), (404, 'no_value', b''),
+ (200, 'none', b'!'), (200, 'none', b'q1!')]
for split in (len(pipeline), 7, 61): # one write, and small writes that cut heads and bodies
with socket.create_connection(('127.0.0.1', host.port), timeout=30) as sock:
for at in range(0, len(pipeline), split):
sock.sendall(pipeline[at:at + split])
reader = sock.makefile('rb')
- for status, body in expected:
- code = int(reader.readline().split()[1])
- length = 0
- for line in iter(reader.readline, b'\r\n'):
- name, _, value = line.decode().partition(':')
- if name.lower() == 'content-length':
- length = int(value)
- assert (code, reader.read(length)) == (status, body), (split, code, status, body)
- # The next round starts from the same documents.
- host.request('PUT', 'q', struct.pack('<II', 0, 3) + b'q1', int(host.request('GET', 'q')[1].strip('"')))
- expected = [(200, b'hello!'), (412, b''), (200, b'q1'), (404, b''), (412, b''), (200, b'q1')]
- pipeline = (request('GET', 'p') + request('PUT', 'q', struct.pack('<II', 0, 0) + b'q1', 'If-None-Match: *\r\n') +
- request('GET', 'q') + request('GET', 'missing') +
- request('PUT', 'q', struct.pack('<II', 2, 0) + b'!', 'If-Match: "1"\r\n') + request('GET', 'q'))
+ got = [read_answer(reader) for _ in expected]
+ assert got == expected, (split, got)
+ # The next round edits a document made already: its creation is refused, its later edit not.
+ revision = int(docs_call(host.port, view_request('revision', 'q'))[2])
+ pipeline = (view_request('body', 'p') + edit_request('q', 0, 0, 0, 'q1') + view_request('size', 'q') +
+ view_request('body', 'missing') + edit_request('q', revision, 0, 0, '!') + view_request('size', 'q'))
+ size = int(docs_call(host.port, view_request('size', 'q'))[2])
+ expected = [(200, 'none', b'hello!'), (422, 'limit', b''), (200, 'none', str(size).encode()),
+ (404, 'no_value', b''), (200, 'none', b'!'), (200, 'none', str(size + 1).encode())]
finally:
stats = host.stop()
assert stats[0]['active'] == 0 and stats[0]['retained_payload_bytes'] == 0, stats
return len(answers) + 3 * len(expected)
-def server_lent_reads(aup, pure, work):
- """A read lends the stored document's extents instead of copying them: a
- 1 MiB answer sent slowly still carries the version read while the
- document is replaced five times meanwhile."""
- import socket
- import struct
- module = work / 'editor-lent.pmod'
- compile_module(aup, EDITOR, module)
- host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'lent'])
- big = bytes(range(256)) * 4096
- try:
- assert host.request('PUT', 'big', struct.pack('<II', 0, 0) + big, 0)[0] == 200
- with socket.create_connection(('127.0.0.1', host.port), timeout=30) as sock:
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096)
- sock.sendall(b'GET /objects/big HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\n\r\n')
- data = sock.recv(100)
- for revision in range(1, 6):
- edit = struct.pack('<II', 0, len(big) if revision == 1 else 1) + b'Z'
- assert host.request('PUT', 'big', edit, revision)[0] == 200
- while b'\r\n\r\n' not in data or len(data) < data.index(b'\r\n\r\n') + 4 + len(big):
- chunk = sock.recv(65536)
- assert chunk, len(data)
- data += chunk
- assert data[data.index(b'\r\n\r\n') + 4:] == big
- assert host.request('GET', 'big') == (200, '"6"', 'none', b'Z')
- finally:
- stats = host.stop()
- assert stats[0]['active'] == 0 and stats[0]['retained_payload_bytes'] == 0, stats
- return len(big)
-
-
def server_lent_budget(aup, pure, work):
"""Lent extents count against the payload budget while an answer waits
- on its socket. Two readers pipeline 1 MiB reads without reading: the
- kernel takes what it can and each reader leaves one answer waiting. A
- third request finds no room (503 payload_budget) until a reader goes."""
+ on its socket. Two readers pipeline 1 MiB reads of a text view without
+ reading: the kernel takes what it can and each reader leaves one answer
+ waiting. A third request finds no room (503 payload_budget) until a
+ reader goes."""
import socket
- import struct
import time
- module = work / 'editor-budget.pmod'
- compile_module(aup, EDITOR, module)
- big = bytes(range(256)) * 4096
+ module, source = work / 'note-budget.pmod', work / 'note.aup'
+ source.write_text(NOTE_SOURCE)
+ compile_module(aup, source, module)
+ big = (bytes(range(32, 127)) * ((1 << 20) // 95 + 1))[:1 << 20] # 1 MiB of text
budget = 2 * len(big) + len(big) // 2
host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'lent-budget',
'--budget', budget])
readers = []
try:
- assert host.request('PUT', 'big', struct.pack('<II', 0, 0) + big, 0)[0] == 200
+ assert post(host.port, '/inputs/write', {'text': big.decode()})[0] == 200
for _ in range(2):
sock = socket.create_connection(('127.0.0.1', host.port), timeout=30)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096)
- sock.sendall(b'GET /objects/big HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\n\r\n' * 32)
+ sock.sendall(b'GET /views/page HTTP/1.1\r\nHost: x\r\n\r\n' * 32)
readers.append(sock)
time.sleep(0.3)
- refused = host.request('GET', 'big')
- assert refused[0] == 503 and refused[2] == 'payload_budget', refused[:3]
+ refused = http(host.port, 'GET', '/views/page')
+ assert refused[:2] == (503, 'payload_budget'), refused[:2]
readers.pop().close()
for _ in range(100):
- answered = host.request('GET', 'big')
+ answered = http(host.port, 'GET', '/views/page')
if answered[0] == 200:
break
time.sleep(0.05)
- assert answered[0] == 200 and answered[3] == big, answered[:3]
+ assert answered[0] == 200 and answered[3] == big, answered[:2]
finally:
for sock in readers:
sock.close()
@@ -351,25 +391,25 @@ def server_lent_views(aup, pure, work, threads=1):
def server_bursts(aup, pure, work, clients=8, edits=30, shards=1):
"""Concurrent edits share commits, and every acknowledged edit survives a
kill -9 of the server."""
- import http.client
import signal
- import struct
import threading
import time
- module = work / 'editor-bursts.pmod'
- compile_module(aup, EDITOR, module)
+ module = work / 'docs-bursts.pmod'
+ (work / 'docs.aup').write_text(DOCS_SOURCE)
+ compile_module(aup, work / 'docs.aup', module)
+ store = work / f'bursts-{shards}'
def load(host, rounds, acknowledged):
def client(number):
key, base = f'c{number}', acknowledged[number][1]
for _ in range(rounds):
try:
- status, tag, _, _ = host.request('PUT', key, struct.pack('<II', 0, 0) + b'x', base)
- except (OSError, http.client.HTTPException): # the server died mid-answer: not acknowledged
+ status, _, _ = docs_call(host.port, edit_request(key, base, 0, 0, 'x'))
+ except Exception: # the server died mid-answer: not acknowledged
return
if status != 200:
return
- base = int(tag.strip('"'))
+ base += 1
acknowledged[number] = (acknowledged[number][0] + 1, base)
threads = [threading.Thread(target=client, args=(n,)) for n in range(clients)]
start = time.perf_counter()
@@ -377,22 +417,18 @@ def server_bursts(aup, pure, work, clients=8, edits=30, shards=1):
thread.start()
return threads, start
- timings = {}
- for name, args in [('program', [pure / 'prototypes/connections/build/tree/pure-connections', '--program', pure / 'prototypes/connections/examples/editor.pure']),
- ('module', [pure / 'build/pure-serve', '--module', module, '--threads', str(shards)])]:
- host = HttpHost(args + ['--store', work / f'bursts-{name}-{shards}'])
- acknowledged = [(0, 0)] * clients
- threads, start = load(host, edits, acknowledged)
- for thread in threads:
- thread.join()
- timings[name] = time.perf_counter() - start
- assert all(count == edits for count, _ in acknowledged), acknowledged
- stats = host.stop()
- if name == 'module':
- bursts = stats[0]['bursts']
- assert stats[0]['burst_events'] == clients * edits and (clients == 1 or bursts < clients * edits), stats
- # Kill the module server while clients edit; what it acknowledged is stored.
- store = work / f'bursts-module-{shards}'
+ host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(shards)])
+ acknowledged = [(0, 0)] * clients
+ threads, start = load(host, edits, acknowledged)
+ for thread in threads:
+ thread.join()
+ seconds = time.perf_counter() - start
+ assert all(count == edits for count, _ in acknowledged), acknowledged
+ stats = host.stop()
+ owner = next(line for line in stats if 'bursts' in line)
+ bursts = owner['bursts']
+ assert owner['burst_events'] == clients * edits and (clients == 1 or bursts < clients * edits), stats
+ # Kill the server while clients edit; what it acknowledged is stored.
host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(shards)])
acknowledged = [(edits, edits)] * clients
threads, _ = load(host, 1000, acknowledged)
@@ -404,13 +440,13 @@ def server_bursts(aup, pure, work, clients=8, edits=30, shards=1):
host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(shards)])
try:
for number, (count, base) in enumerate(acknowledged):
- status, tag, _, body = host.request('GET', f'c{number}')
- stored = int(tag.strip('"'))
- assert status == 200 and stored in (base, base + 1) and len(body) == stored, (number, count, base, tag)
+ status, _, body = docs_call(host.port, view_request('revision', f'c{number}'))
+ stored = int(body)
+ size = int(docs_call(host.port, view_request('size', f'c{number}'))[2])
+ assert status == 200 and stored in (base, base + 1) and size == stored, (number, count, base, stored)
finally:
host.stop()
- return {'writes': clients * edits, 'bursts': bursts, 'program_seconds': round(timings['program'], 3),
- 'module_seconds': round(timings['module'], 3),
+ return {'writes': clients * edits, 'bursts': bursts, 'seconds': round(seconds, 3),
'acknowledged_before_kill': sum(count for count, _ in acknowledged) - clients * edits}
@@ -1700,7 +1736,7 @@ def module_specs(aup, pure, work):
import struct
runs = {}
program = work / 'sha256.pmod'
- compile_module(aup, EDITOR.parent / 'sha256.aup', program)
+ compile_module(aup, EXAMPLES / 'sha256.aup', program)
chooser = random.Random(140)
messages = [b'abc', b'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq',
b'abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu']
@@ -1715,7 +1751,7 @@ def module_specs(aup, pure, work):
assert digests == [hashlib.sha256(m).hexdigest() for m in messages], 'sha256'
runs['sha256'] = len(messages)
program = work / 'json.pmod'
- compile_module(aup, EDITOR.parent / 'json.aup', program)
+ compile_module(aup, EXAMPLES / 'json.aup', program)
suite = pure / 'tests/specs/json'
cases = {name: (suite / name).read_bytes() for name in sorted(os.listdir(suite)) if name.endswith('.json')}
cases['n_structure_100000_opening_arrays.json'] = b'[' * 100000
@@ -1776,7 +1812,7 @@ def module_specs(aup, pure, work):
cases[f'r_{k}.json'] = json.dumps(value(0), ensure_ascii=chooser.random() < 0.5, indent=chooser.choice([None, 1, 2])).encode()
(work / f'r_{k}.json').write_bytes(cases[f'r_{k}.json'])
program = work / 'json_tree.pmod'
- compile_module(aup, EDITOR.parent / 'json_tree.aup', program)
+ compile_module(aup, EXAMPLES / 'json_tree.aup', program)
given = ''.join(f'message check @{work / name}\n' for name in cases)
outputs = {name: subprocess.run([str(pure / 'build/pure-run'), str(program)], input=given, capture_output=True, text=True,
timeout=600, env=dict(os.environ, **env)).stdout
@@ -4445,12 +4481,12 @@ def cpu_seconds(pid):
def server_clocks(aup, pure, work):
"""The wall clock moved an hour while a node runs (build/clock-back.so
moves what the node reads of it, from its start): its timers, the pauses
- of a wire whose receiver is down, the owner's waits and a store's group
- commit all count on the monotonic clock. Were a wait's deadline read
- from the wall clock, set back it would end at once, again and again (a
- core spun, the wire tried thousands of times); set forward it would last
- an hour (the timers stopped, the wire tried once, and a lone commit
- after a group waited that long for the others to join it)."""
+ of a wire whose receiver is down and the owner's waits all count on the
+ monotonic clock. Were a wait's deadline read from the wall clock, set
+ back it would end at once, again and again (a core spun, the wire tried
+ thousands of times); set forward it would last an hour (the timers
+ stopped, the wire tried once, and a lone edit after three at once waited
+ that long)."""
import os
import socket
import struct
@@ -4479,12 +4515,12 @@ def server_clocks(aup, pure, work):
(shift, stats['ticks'], share, stats['delivery_attempts'])
seen[f'shift_{shift}'] = {'ticks': stats['ticks'], 'cpu_share': round(share, 3),
'wire_attempts': stats['delivery_attempts']}
- editor = work / 'clocks-editor.pmod'
- compile_module(aup, EDITOR, editor)
+ editor = work / 'clocks-docs.pmod'
+ (work / 'clocks-docs.aup').write_text(DOCS_SOURCE)
+ compile_module(aup, work / 'clocks-docs.aup', editor)
env = {**os.environ, 'LD_PRELOAD': str(pure / 'build/clock-back.so'), 'CLOCK_BACK_MS': '0', 'CLOCK_SHIFT_S': '3600'}
host = HttpHost([pure / 'build/pure-serve', '--module', editor, '--store', work / 'clocks-docs', '--threads', 3],
env=env)
- new = struct.pack('<II', 0, 0) # an edit that makes the document: at 0, none removed, then its bytes
statuses, alone = [], 0.0
try:
for round_number in range(20): # three at once, grouped; then one alone
@@ -4492,7 +4528,7 @@ def server_clocks(aup, pure, work):
def put(k):
together.wait()
- statuses.append(host.request('PUT', f'd{round_number}-{k}', new + b'x' * 100000, 0)[0])
+ statuses.append(docs_call(host.port, edit_request(f'd{round_number}-{k}', 0, 0, 0, 'x' * 100000))[0])
threads = [threading.Thread(target=put, args=(k,)) for k in range(3)]
for thread in threads:
@@ -4500,12 +4536,11 @@ def server_clocks(aup, pure, work):
for thread in threads:
thread.join()
began = time.time()
- statuses.append(host.request('PUT', f'alone-{round_number}', new + b'y', 0)[0])
+ statuses.append(docs_call(host.port, edit_request(f'alone-{round_number}', 0, 0, 0, 'y'))[0])
alone = max(alone, time.time() - began)
finally:
- stats = host.stop()[-1]
- assert statuses == [200] * 80 and alone < 1 and stats['grouped_commits'] > 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('<II', 0, 0) + b'hello' # the edit: start, remove, insert
- assert call('PUT', '/docs/objects/k', w, edit, {'If-None-Match': '*'})[0] == 200
- assert call('GET', '/docs/objects/k', r)[::3] == (200, b'hello')
+ assert call('POST', '/docs/inputs/edit', w, edit_body('k', 0, 0, 0, 'hello'), json_type)[0] == 200
+ assert call('GET', '/docs/views/body?id=k', r)[::3] == (200, b'hello')
assert call('POST', '/t/inputs/add?step=2', i)[0] == 200
assert call('POST', '/t/inputs/other?step=1', i)[:2] == (403, 'forbidden')
assert call('GET', '/t/views/total', i)[:2] == (403, 'forbidden')
@@ -6417,14 +6453,14 @@ def server_tokens(aup, pure, work):
def main():
- global EDITOR
+ global EXAMPLES
parser = argparse.ArgumentParser()
parser.add_argument('--aup', type=Path, required=True, help='the aup compiler')
- parser.add_argument('--aup-root', type=Path, default=ROOT.parent / 'aup', help='its checkout, for examples/editor.aup')
+ parser.add_argument('--aup-root', type=Path, default=ROOT.parent / 'aup', help='its checkout, for its examples')
parser.add_argument('--seeds', type=int, default=4)
args = parser.parse_args()
aup, pure = args.aup.resolve(), ROOT
- EDITOR = args.aup_root.resolve() / 'examples' / 'editor.aup'
+ EXAMPLES = args.aup_root.resolve() / 'examples'
report = {}
with tempfile.TemporaryDirectory(prefix='pure-check-') as directory:
work = Path(directory)
@@ -6432,10 +6468,9 @@ def main():
report['module_assumptions'] = module_assumptions(aup, pure, work)
report['module_specs'] = module_specs(aup, pure, work)
report['module_tier'] = module_tier(aup, pure, work)
- report['server_parity_requests'] = sum(server_parity(aup, pure, work, range(args.seeds), threads=threads)
- for threads in (1, 3))
+ report['server_documents_requests'] = sum(server_documents(aup, pure, work, range(args.seeds), threads=threads)
+ for threads in (1, 3))
report['server_pipelined_answers'] = server_pipelining(aup, pure, work)
- report['server_lent_read_bytes'] = server_lent_reads(aup, pure, work)
report['server_lent_budget_bytes'] = server_lent_budget(aup, pure, work)
report['server_lent_views'] = [server_lent_views(aup, pure, work, threads) for threads in (1, 3)]
report['server_bursts'] = server_bursts(aup, pure, work)