aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-27 13:48:55 +0100
committereunhhu <[email protected]>2026-09-27 13:48:55 +0100
commitd03092f8dc3efe2231c817031be0a744ac6e60e1 (patch)
tree93fc1b7ee12d98977093fd7acdcafe4e5d3665f9
parent56b8202a23eb08167f27fea4782152959f8ea9ef (diff)
downloadaup-d03092f8dc3efe2231c817031be0a744ac6e60e1.tar.gz
aup-d03092f8dc3efe2231c817031be0a744ac6e60e1.zip
Leave pure-serve's checks to Pure
aup's check keeps what it owns: the language, the compiler, and apps written in aup matching their references on Pure's loader. The HTTP host's checks moved to Pure's tests/check.py, which builds its apps with this compiler.
-rw-r--r--Makefile4
-rw-r--r--tests/check.py1175
2 files changed, 4 insertions, 1175 deletions
diff --git a/Makefile b/Makefile
index 68f5378..1056019 100644
--- a/Makefile
+++ b/Makefile
@@ -16,6 +16,6 @@ $(BUILD)/aup: compiler/main.c compiler/compile.c compiler/aup.h | $(BUILD)
$(CC) $(CPPFLAGS) $(CFLAGS) compiler/main.c compiler/compile.c $(LDFLAGS) $(LDLIBS) -o $@
check: $(BUILD)/aup
- $(MAKE) -C $(PURE) build/pure-run build/pure-edit-module build/pure-serve
- $(MAKE) -C $(PURE)/prototypes/connections BUILD=build/tree STORE=store-tree.c build/tree/pure-edit build/tree/pure-connections
+ $(MAKE) -C $(PURE) build/pure-run build/pure-edit-module
+ $(MAKE) -C $(PURE)/prototypes/connections BUILD=build/tree STORE=store-tree.c build/tree/pure-edit
python3 tests/check.py --aup $(BUILD)/aup --pure-run $(PURE)/build/pure-run --pure $(PURE)
diff --git a/tests/check.py b/tests/check.py
index 1666ed7..98d6e37 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
-"""aup v0a checks.
+"""aup v0a checks. (Pure's HTTP host is checked in Pure: its tests/check.py
+builds its apps with this compiler.)
1. Every example compiles, and Pure's verifier accepts the module.
2. The same source always yields the same bytes.
@@ -14,12 +15,6 @@
mark exactly as a reference Python model under random typing (ASCII,
Korean, emoji, combining marks, NUL, newlines), moves and deletions; string
escapes and number text follow their stated rules.
-9. Server: the HTTP document server running editor.aup answers random
- requests exactly as the server running editor.pure, on persistent and
- pipelined connections too; a read lends the stored extents and still
- carries its version while the document changes, and lent answers count
- against the payload budget; concurrent edits share commits, and every
- acknowledged edit survives kill -9.
8. Stored state: the list app and the text editor run on a tree store,
restarting at random points (every other run saving in bursts), and still
match their reference models.
@@ -760,1155 +755,6 @@ def stored_runs(aup, pure_run, work, seeds, steps=240):
return compared, restarts
-class HttpHost:
- """A Pure HTTP host on a fresh port; stops with its statistics."""
-
- def __init__(self, args):
- import select
- self.process = subprocess.Popen([str(a) for a in args] + ['--port', '0'], stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- ready, _, _ = select.select([self.process.stdout], [], [], 10)
- assert ready, 'HTTP host did not start'
- line = self.process.stdout.readline()
- assert line, self.process.communicate(timeout=5)
- self.port = json.loads(line)['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)
- assert self.process.returncode == 0, error[-1000:]
- 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, ROOT / 'examples/editor.aup', module)
- compared = 0
- 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)])
- 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)
- if rng.random() < 0.35:
- request = ('GET', key, b'', None)
- 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)
- compared += 1
- finally:
- old.stop()
- new.stop()
- return compared
-
-
-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, ROOT / 'examples/editor.aup', module)
- host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'pipeline'])
- 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!')]
- 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'))
- 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, ROOT / 'examples/editor.aup', 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."""
- import socket
- import struct
- import time
- module = work / 'editor-budget.pmod'
- compile_module(aup, ROOT / 'examples/editor.aup', module)
- big = bytes(range(256)) * 4096
- 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
- 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)
- readers.append(sock)
- time.sleep(0.3)
- refused = host.request('GET', 'big')
- assert refused[0] == 503 and refused[2] == 'payload_budget', refused[:3]
- readers.pop().close()
- for _ in range(100):
- answered = host.request('GET', 'big')
- if answered[0] == 200:
- break
- time.sleep(0.05)
- assert answered[0] == 200 and answered[3] == big, answered[:3]
- finally:
- for sock in readers:
- sock.close()
- stats = host.stop()
- assert stats[0]['active'] == 0 and stats[0]['retained_payload_bytes'] == 0, stats
- return budget
-
-
-NOTE_SOURCE = """state note: text
-input write = message {
- text: text
-}
-on write { note = write.text }
-view page = note
-view size = note.bytes
-"""
-
-
-def server_lent_views(aup, pure, work, threads=1):
- """A view that shows a text state as it is lends the state's stored
- extents. Answers pipelined on one connection and left waiting hold the
- store mapping they were read from while the note is replaced: each comes
- out whole in one version. When a reader goes without reading, the
- mappings its answers held are unmapped and nothing stays charged. With
- several threads the owner lends and a connection thread gives back."""
- import socket
- import time
- module, source = work / 'note.pmod', work / 'note.aup'
- source.write_text(NOTE_SOURCE)
- compile_module(aup, source, module)
- store = work / f'lent-views-{threads}'
- host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', threads])
-
- def write(text):
- body = json.dumps({'text': text}).encode()
- import http.client
- connection = http.client.HTTPConnection('127.0.0.1', host.port, timeout=30)
- connection.request('POST', '/inputs/write', body, {'Content-Type': 'application/json'})
- assert connection.getresponse().status == 200
- connection.close()
-
- def mappings():
- # A compaction replaces the file: a mapping of the old one shows as "(deleted)".
- path = str((store / 'pure.tree').resolve())
- return sum(1 for line in open(f'/proc/{host.process.pid}/maps') if path in line)
-
- def settle(to):
- for _ in range(100): # an answer's ticket is released on its thread
- if mappings() <= to: return mappings()
- time.sleep(0.02)
- return mappings()
-
- def pipeline(count):
- # Far more than the kernel's buffers take (a few MiB): an answer waits, lent.
- sock = socket.create_connection(('127.0.0.1', host.port), timeout=30)
- sock.sendall(b'GET /views/page HTTP/1.1\r\nHost: x\r\n\r\n' * count)
- time.sleep(0.3)
- return sock
-
- size = 1 << 20
- try:
- write('x' * size)
- assert http(host.port, 'GET', '/views/size')[3] == str(size).encode()
- alone = mappings()
- # Whole versions, however the note changes while answers wait.
- sock = pipeline(32)
- for round_ in range(5):
- write(chr(ord('A') + round_) * size)
- held = mappings()
- data = b''
- while data.count(b'HTTP/1.1 200') < 32 or len(data) < 32 * size:
- chunk = sock.recv(1 << 20)
- assert chunk, len(data)
- data += chunk
- sock.close()
- answers = [part.split(b'\r\n\r\n', 1) for part in data.split(b'HTTP/1.1 200 OK\r\n')[1:]]
- bodies = [body[:size] for _, body in answers]
- assert all(b'text/plain' in head for head, _ in answers) and bodies[0] == b'x' * size, [b[:1] for b in bodies]
- assert all(len(set(body)) == 1 and len(body) == size for body in bodies), [len(b) for b in bodies]
- after_reading = settle(alone)
- # A reader that goes without reading gives back what its answers held.
- sock = pipeline(32)
- for round_ in range(3):
- write(chr(ord('a') + round_) * size)
- held_again = mappings()
- sock.close()
- after_leaving = settle(alone)
- assert held > alone and held_again > alone and after_reading <= alone and after_leaving <= alone, \
- (alone, held, after_reading, held_again, after_leaving)
- assert http(host.port, 'GET', '/views/page')[3] == b'c' * size
- finally:
- stats = host.stop()
- assert stats[0]['active'] == 0 and stats[0]['retained_payload_bytes'] == 0, stats
- return {'alone': alone, 'while_lent': max(held, held_again), 'after': after_leaving}
-
-
-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, ROOT / 'examples/editor.aup', module)
-
- 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
- return
- if status != 200:
- return
- base = int(tag.strip('"'))
- acknowledged[number] = (acknowledged[number][0] + 1, base)
- threads = [threading.Thread(target=client, args=(n,)) for n in range(clients)]
- start = time.perf_counter()
- for thread in threads:
- 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 = [(edits, edits)] * clients
- threads, _ = load(host, 1000, acknowledged)
- time.sleep(0.5)
- host.process.send_signal(signal.SIGKILL)
- host.process.wait()
- for thread in threads:
- thread.join()
- 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)
- finally:
- host.stop()
- return {'writes': clients * edits, 'bursts': bursts, 'program_seconds': round(timings['program'], 3),
- 'module_seconds': round(timings['module'], 3),
- 'acknowledged_before_kill': sum(count for count, _ in acknowledged) - clients * edits}
-
-
-SERVED_SOURCE = """record Entry {
- name: text
- points = 0
-}
-state board: list of Entry by name
-state at: index of board
-input score = message {
- points: u32
- name: bytes
-}
-input join = text
-input drop = key Delete
-on score {
- if !board.has(text(score.name)) { board.add(Entry { name: text(score.name) }) }
- board[text(score.name)].points = board[text(score.name)].points + score.points
- at = board[text(score.name)]
- reply score.name
-}
-on join {
- board.add(Entry { name: join })
- at = board[join]
-}
-on drop.down { board.remove(at) }
-view total = sum(e.points for e in board)
-view current = at.name
-view leaders = each e in board {
- name = e.name
- points = e.points
-}
-"""
-
-
-def http(port, method, path, body=b''):
- """One request on its own connection: status, X-Pure-Error, content type, body."""
- import http.client
- connection = http.client.HTTPConnection('127.0.0.1', port, timeout=30)
- try:
- connection.request(method, path, body, {'Content-Length': str(len(body))} if method != 'GET' else {})
- response = connection.getresponse()
- return response.status, response.getheader('X-Pure-Error'), response.getheader('Content-Type'), response.read()
- finally:
- connection.close()
-
-
-JSON_SOURCE = """record Note {
- title: text
- body: text
- points = 0
-}
-state notes: list of Note by title
-state counted = 0
-input add = message {
- title: text
- points: number
- body: text
-}
-input count = message {
- n: u32
-}
-record Shipment {
- title: text
- city: text
- note: text
- zip = 0
-}
-state shipped: list of Shipment
-input ship = message {
- title: text
- to: {
- city: text
- zip: u32
- door: {
- note: text
- }
- }
-}
-on add {
- notes.add(Note { title: add.title, body: add.body, points: add.points })
- reply add.title
-}
-on count { counted = counted + count.n }
-on ship {
- shipped.append(Shipment { title: ship.title, city: ship.to.city, note: ship.to.door.note, zip: ship.to.zip })
- reply ship.to.door.note
-}
-view counted_view = counted
-view all = each n in notes {
- title = n.title
- body = n.body
- points = n.points
-}
-view shipments = each s in shipped {
- title = s.title
- city = s.city
- note = s.note
- zip = s.zip
-}
-"""
-
-
-def server_json(aup, pure, work, rounds=300):
- """Messages of text and number fields posted as JSON objects: strings
- come out as Python's json module reads them (escapes, control bytes,
- Korean, surrogate pairs), numbers as it reads them; a missing, unknown,
- doubled or mistyped field, broken JSON and a lone surrogate answer 400."""
- module, source = work / 'json.pmod', work / 'json.aup'
- source.write_text(JSON_SOURCE)
- compile_module(aup, source, module)
- host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'json-store'])
- rng = random.Random(77)
- pieces = ['a', 'Z', ' ', '"', '\\', '/', '\n', '\t', '\x01', '\x1f', 'é', '가', '😀', '\u2028', 'x' * 20]
- notes = {}
-
- def post(path, body, json_body=True):
- import http.client
- connection = http.client.HTTPConnection('127.0.0.1', host.port, timeout=30)
- try:
- headers = {'Content-Length': str(len(body))}
- if json_body: headers['Content-Type'] = 'application/json'
- connection.request('POST', path, body, headers)
- response = connection.getresponse()
- return response.status, response.getheader('X-Pure-Error'), response.read()
- finally:
- connection.close()
-
- try:
- for _ in range(rounds):
- title = ''.join(rng.choice(pieces) for _ in range(rng.randint(0, 6)))
- body = ''.join(rng.choice(pieces) for _ in range(rng.randint(0, 4)))
- points = rng.choice([0, 1, -2.5, 1e3, 3.25e-2, 12345678.5])
- members = [('title', title), ('points', points), ('body', body)]
- rng.shuffle(members)
- text = json.dumps(dict(members), ensure_ascii=rng.random() < 0.5)
- text = text.replace(': ', ':' + ' ' * rng.randint(0, 2)).replace(', ', ',\n' if rng.random() < 0.3 else ', ')
- got = post('/inputs/add', text.encode())
- if title in notes:
- assert got[:2] == (409, 'exists'), (text, got)
- continue
- assert got == (200, 'none', title.encode()), (text, got)
- notes[title] = (body, points)
- rows = json.loads(http(host.port, 'GET', '/views/all')[3])['items']
- want = [{'title': t, 'body': notes[t][0], 'points': notes[t][1]} for t in sorted(notes, key=lambda t: t.encode())]
- assert rows == want, [(a, b) for a, b in zip(rows, want) if a != b][:2]
- # The query gives fixed fields; a body without JSON fills a message's one variable field only.
- assert post('/inputs/count?n=3', b'', json_body=False)[:2] == (200, 'none')
- assert post('/inputs/count', b'{"n": 4}')[:2] == (200, 'none')
- assert http(host.port, 'GET', '/views/counted_view')[3] == b'7'
- assert post('/inputs/add?points=2', b'{"title": "q-from-query", "body": ""}')[:2] == (200, 'none')
- wrong = [
- (b'{"title": "m", "points": 1}', 'missing_field'),
- (b'{"title": "m", "points": 1, "body": "", "extra": 1}', 'json_field'),
- (b'{"title": "m", "title": "n", "points": 1, "body": ""}', 'json_field'),
- (b'{"title": 1, "points": 1, "body": ""}', 'json_type'),
- (b'{"title": "m", "points": "1", "body": ""}', 'json_type'),
- (b'{"title": "m", "points": 1e999, "body": ""}', 'json_type'),
- (b'{"title": "m", "points": 01, "body": ""}', 'json'),
- (b'{"title": "m", "points": NaN, "body": ""}', 'json'),
- (b'{"title": "m", "points": 1, "body": "",}', 'json'),
- (b'{"title": "\\ud800", "points": 1, "body": ""}', 'json'),
- (b'{"title": "\xff", "points": 1, "body": ""}', 'json'),
- (b'{"title": "m", "points": 1, "body": ""} x', 'json'),
- (b'[1]', 'json'),
- (b'{"title": "m\n", "points": 1, "body": ""}', 'json'), # a raw newline in a string
- ]
- for body, error in wrong:
- assert post('/inputs/add', body)[:2] == (400, error), (body, post('/inputs/add', body))
- assert post('/inputs/add?points=2', b'{"title": "m", "points": 1, "body": ""}')[:2] == (400, 'json_field')
- assert post('/inputs/add', b'plain', json_body=False)[:2] == (400, 'json') # two text fields need JSON
- assert post('/inputs/count', b'{"n": -1}')[:2] == (400, 'json_type')
- assert post('/inputs/count', b'{"n": 4294967296}')[:2] == (400, 'json_type')
- # Groups: objects inside the body, members in any order, strings
- # read where they are (escaped ones unescaped over themselves).
- shipments = []
- for _ in range(rounds // 3):
- title, city, note = (''.join(rng.choice(pieces) for _ in range(rng.randint(0, 5))) for _ in range(3))
- zip_code = rng.randint(0, 99999)
- door = [('note', note)]
- to = [('city', city), ('zip', zip_code), ('door', dict(door))]
- rng.shuffle(to)
- members = [('title', title), ('to', dict(to))]
- rng.shuffle(members)
- text = json.dumps(dict(members), ensure_ascii=rng.random() < 0.5)
- got = post('/inputs/ship', text.encode())
- assert got == (200, 'none', note.encode()), (text, got)
- shipments.append({'title': title, 'city': city, 'note': note, 'zip': zip_code})
- # A group's fixed field from the query, by its path.
- assert post('/inputs/ship?to.zip=7', b'{"title": "q", "to": {"city": "c", "door": {"note": "n"}}}')[:2] == \
- (200, 'none')
- shipments.append({'title': 'q', 'city': 'c', 'note': 'n', 'zip': 7})
- rows = json.loads(http(host.port, 'GET', f'/views/shipments?count={len(shipments)}')[3])['items']
- assert rows == shipments, [(a, b) for a, b in zip(rows, shipments) if a != b][:2]
- grouped = '{"title": "t", "to": {"city": "c", "zip": 1, "door": {"note": "n"}}}'
- wrong_groups = [
- (grouped.replace('"zip": 1', '"zip": 1, "lat": 2'), 'json_field'), # unknown member of a group
- (grouped.replace('"note": "n"', '"note": "n", "bell": {"x": 1}'), 'json_field'),
- (grouped.replace('"door": {"note": "n"}', '"door": "n"'), 'json_type'), # a group as a value
- (grouped.replace('"title": "t"', '"title": {"a": "t"}'), 'json_type'), # a field as a group
- (grouped.replace('"door": {"note": "n"}', '"door": {}'), 'missing_field'),
- (grouped.replace('"zip": 1', '"zip": 1, "zip": 2'), 'json_field'),
- (grouped.replace('"zip": 1', '"zip": [1]'), 'json_type'),
- (grouped.replace('"zip": 1', '"zip": null'), 'json_type'),
- (grouped.replace('"note": "n"', '"note": "n\\ud800"'), 'json'),
- (grouped.replace('"door": {"note": "n"}}', '"door": {"note": "n"}'), 'json'), # unclosed
- ]
- for body, error in wrong_groups:
- assert post('/inputs/ship', body.encode())[:2] == (400, error), (body, post('/inputs/ship', body.encode()))
- assert post('/inputs/ship?to.zip=7', grouped.encode())[:2] == (400, 'json_field') # given twice
- finally:
- host.stop()
- return rounds + len(wrong) + rounds // 3 + len(wrong_groups)
-
-
-LINK_SENDER = """state n = 0
-state total = 0
-input buy = message {
- item: text
- count: u32
-}
-output ordered = message {
- item: text
- count: u32
- to: {
- city: text
- }
-}
-on buy {
- n = n + 1
- total = total + buy.count
- send ordered { item: buy.item, count: buy.count, to: { city: "Seoul" } }
-}
-view sent = n
-view sent_total = total
-"""
-
-LINK_RECEIVER = """state stock = 0
-state orders = 0
-input order = message {
- item: text
- count: u32
- to: {
- city: text
- }
-}
-on order {
- require order.count < 100
- stock = stock + order.count
- orders = orders + 1
-}
-view total = stock
-view count = orders
-"""
-
-
-def server_links(aup, pure, work):
- """Two served apps, one's output wired to the other's input over HTTP
- (--send). While four clients buy from the sender, the receiver and then
- the sender are killed (-9) and started again on their stores. In the end
- the receiver has applied every order the sender saved exactly once (its
- total and count equal the sender's), a refused order (count 150) was
- delivered once and did not hold back the order after it, and every
- acknowledged order is among the saved ones."""
- import select
- import signal
- import socket
- import threading
- import time
- sender_module, receiver_module = work / 'link-sender.pmod', work / 'link-receiver.pmod'
- for module, text in ((sender_module, LINK_SENDER), (receiver_module, LINK_RECEIVER)):
- source = module.with_suffix('.aup')
- source.write_text(text)
- compile_module(aup, source, module)
-
- def free_port():
- with socket.socket() as probe:
- probe.bind(('127.0.0.1', 0))
- return probe.getsockname()[1]
-
- def start(args, port):
- process = subprocess.Popen([str(a) for a in args] + ['--port', str(port)], stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- ready, _, _ = select.select([process.stdout], [], [], 10)
- assert ready and process.stdout.readline(), 'host did not start'
- return process
-
- def post(port, path, body):
- import http.client
- connection = http.client.HTTPConnection('127.0.0.1', port, timeout=10)
- try:
- data = json.dumps(body).encode()
- connection.request('POST', path, data, {'Content-Length': str(len(data)), 'Content-Type': 'application/json'})
- response = connection.getresponse()
- response.read()
- return response.status
- finally:
- connection.close()
-
- def number(port, view):
- return int(http(port, 'GET', f'/views/{view}')[3])
-
- port_a, port_b = free_port(), free_port()
- receiver = [pure / 'build/pure-serve', '--module', receiver_module, '--store', work / 'link-b', '--threads', '2']
- sender = [pure / 'build/pure-serve', '--module', sender_module, '--store', work / 'link-a',
- '--send', f'ordered=http://127.0.0.1:{port_b}/inputs/order']
- hosts = {'b': start(receiver, port_b), 'a': start(sender, port_a)}
- acknowledged = [0] * 4
- stop = threading.Event()
-
- def client(which):
- rng = random.Random(which + 7700)
- while not stop.is_set():
- count = rng.randint(1, 20)
- try:
- status = post(port_a, '/inputs/buy', {'item': f'c{which}', 'count': count})
- except OSError:
- time.sleep(0.02)
- continue
- if status == 200:
- acknowledged[which] += count
- else:
- time.sleep(0.02)
-
- clients = [threading.Thread(target=client, args=(n,)) for n in range(len(acknowledged))]
- for thread in clients:
- thread.start()
- try:
- for name, args, port in (('b', receiver, port_b), ('a', sender, port_a)):
- time.sleep(0.6)
- hosts[name].send_signal(signal.SIGKILL)
- hosts[name].wait()
- time.sleep(0.3)
- hosts[name] = start(args, port)
- time.sleep(0.6)
- finally:
- stop.set()
- for thread in clients:
- thread.join()
- try:
- assert post(port_a, '/inputs/buy', {'item': 'big', 'count': 150}) == 200
- assert post(port_a, '/inputs/buy', {'item': 'after', 'count': 7}) == 200
- deadline = time.time() + 30
- while True:
- sent, sent_total = number(port_a, 'sent'), number(port_a, 'sent_total')
- count, total = number(port_b, 'count'), number(port_b, 'total')
- if (count, total) == (sent - 1, sent_total - 150) or time.time() > deadline:
- break
- time.sleep(0.1)
- assert (count, total) == (sent - 1, sent_total - 150), (count, total, sent, sent_total)
- assert sent_total >= sum(acknowledged) + 157 and sum(acknowledged) > 0, (sent_total, acknowledged)
- finally:
- stats = {}
- for name, process in hosts.items():
- process.terminate()
- _, error = process.communicate(timeout=30)
- assert process.returncode == 0, error[-1000:]
- stats[name] = [json.loads(line) for line in error.decode().splitlines() if line.startswith('{"active"')][-1]
- return {'orders': sent, 'acknowledged_total': sum(acknowledged), 'delivered_after_restart': stats['a']['delivered'],
- 'attempts_after_restart': stats['a']['delivery_attempts']}
-
-
-def server_webhook(aup, pure, work):
- """An output wired to a third-party HTTPS endpoint (a Python server with
- a certificate of its own): five orders arrive in order as JSON with the
- sender's headers, one answered 503 at first arrives again, and none goes
- to a receiver whose certificate the sender does not trust."""
- from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
- import ssl
- import threading
- import time
- module = work / 'hook-sender.pmod'
- module.with_suffix('.aup').write_text(LINK_SENDER)
- compile_module(aup, module.with_suffix('.aup'), module)
- for name in ('trusted', 'other'):
- subprocess.run(['openssl', 'req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256', '-nodes',
- '-keyout', work / f'{name}.key', '-out', work / f'{name}.pem', '-days', '1',
- '-subj', '/CN=127.0.0.1', '-addext', 'subjectAltName=IP:127.0.0.1'],
- check=True, capture_output=True)
- received, refused_once = [], set()
-
- class Hook(BaseHTTPRequestHandler):
- protocol_version = 'HTTP/1.1'
-
- def do_POST(self):
- body = self.rfile.read(int(self.headers['Content-Length']))
- received.append((dict(self.headers), json.loads(body)))
- status = 200
- if self.headers['X-Pure-Sequence'] == '2' and '2' not in refused_once:
- refused_once.add('2')
- status = 503
- self.send_response(status)
- self.send_header('Content-Length', '0')
- self.end_headers()
-
- def log_message(self, *args):
- pass
-
- server = ThreadingHTTPServer(('127.0.0.1', 0), Hook)
- context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
- context.load_cert_chain(work / 'trusted.pem', work / 'trusted.key')
- server.socket = context.wrap_socket(server.socket, server_side=True)
- threading.Thread(target=server.serve_forever, daemon=True).start()
- url = f'ordered=https://127.0.0.1:{server.server_address[1]}/hook'
- try:
- def buy(host, count):
- import http.client
- connection = http.client.HTTPConnection('127.0.0.1', host.port, timeout=10)
- data = json.dumps({'item': f'i{count}', 'count': count}).encode()
- connection.request('POST', '/inputs/buy', data, {'Content-Length': str(len(data)), 'Content-Type': 'application/json'})
- assert connection.getresponse().status == 200
- connection.close()
-
- host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'hook-a', '--send', url,
- '--send-ca', work / 'trusted.pem'])
- try:
- for count in range(1, 6):
- buy(host, count)
- deadline = time.time() + 20
- while len({h['X-Pure-Sequence'] for h, _ in received}) < 5 and time.time() < deadline:
- time.sleep(0.05)
- finally:
- stats = host.stop()[-1]
- firsts = []
- for headers, body in received:
- seq = int(headers['X-Pure-Sequence'])
- assert headers['X-Pure-Source'] == 'hook-sender.ordered' and headers['Content-Type'] == 'application/json'
- assert headers['Idempotency-Key'] == f'hook-sender.ordered-{seq}' and 'X-Pure-Count' not in headers
- assert body == {'item': f'i{seq}', 'count': seq, 'to': {'city': 'Seoul'}}, body
- if seq not in firsts:
- firsts.append(seq)
- assert firsts == [1, 2, 3, 4, 5] and len(received) == 6 and stats['delivered'] == 5, (firsts, len(received), stats)
- # A receiver the sender cannot trust gets nothing; the send waits in the outbox.
- before = len(received)
- host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'hook-b', '--send', url,
- '--send-ca', work / 'other.pem'])
- try:
- buy(host, 1)
- time.sleep(1)
- finally:
- host.process.terminate()
- _, error = host.process.communicate(timeout=30)
- assert len(received) == before and b'not trusted' in error, error[-500:]
- finally:
- server.shutdown()
- return {'hooks': len(received), 'delivered': stats['delivered']}
-
-
-PAY_SOURCE = """state sent = 0
-state paid = 0
-state declined = 0
-state paid_total = 0
-input buy = message {
- order: text
- amount: u32
-}
-output charge = message {
- order: text
- amount: u32
-}
-input charged = message {
- order: text
- amount: u32
- status: text
- http_status: u32
-}
-on buy {
- sent = sent + 1
- send charge { order: buy.order, amount: buy.amount }
-}
-on charged {
- if charged.http_status == 200 {
- paid = paid + 1
- paid_total = paid_total + charged.amount
- } else {
- declined = declined + 1
- }
-}
-view sent_view = sent
-view paid_view = paid
-view declined_view = declined
-view paid_total_view = paid_total
-"""
-
-
-def server_answers(aup, pure, work):
- """An output wired to a payments API whose answers come back as events
- (--reply): the API answers 200 or 402 and gives a key's first answer to
- every request with that Idempotency-Key. While clients buy, the sender is
- killed (-9) and started again; in the end every saved order was answered
- exactly once: the app's paid and declined counts and paid total match the
- API's answers, one per key."""
- from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
- import select
- import signal
- import socket
- import threading
- import time
- module = work / 'pay.pmod'
- module.with_suffix('.aup').write_text(PAY_SOURCE)
- compile_module(aup, module.with_suffix('.aup'), module)
- answers, lock = {}, threading.Lock()
-
- class Payments(BaseHTTPRequestHandler):
- protocol_version = 'HTTP/1.1'
-
- def do_POST(self):
- body = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
- with lock:
- key = self.headers['Idempotency-Key']
- if key not in answers:
- ok = body['amount'] <= 1000
- answers[key] = (body, 200 if ok else 402,
- {'status': 'succeeded' if ok else 'declined', 'id': f'ch_{len(answers)}', 'x': [1, {}]})
- _, status, answer = answers[key]
- data = json.dumps(answer).encode()
- self.send_response(status)
- self.send_header('Content-Type', 'application/json')
- self.send_header('Content-Length', str(len(data)))
- self.end_headers()
- self.wfile.write(data)
-
- def log_message(self, *args):
- pass
-
- server = ThreadingHTTPServer(('127.0.0.1', 0), Payments)
- server.handle_error = lambda request, address: None # a sender killed mid-request
- threading.Thread(target=server.serve_forever, daemon=True).start()
- with socket.socket() as probe:
- probe.bind(('127.0.0.1', 0))
- port = probe.getsockname()[1]
- args = [pure / 'build/pure-serve', '--module', module, '--store', work / 'pay-store', '--port', port,
- '--send', f'charge=http://127.0.0.1:{server.server_address[1]}/charge', '--reply', 'charge=charged']
-
- def start():
- process = subprocess.Popen([str(a) for a in args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- ready, _, _ = select.select([process.stdout], [], [], 10)
- assert ready and process.stdout.readline(), 'host did not start'
- return process
-
- def number(view):
- return int(http(port, 'GET', f'/views/{view}')[3])
-
- host = start()
- stop = threading.Event()
-
- def client(which):
- import http.client
- rng = random.Random(which + 8800)
- while not stop.is_set():
- data = json.dumps({'order': f'o{which}-{rng.randrange(10**6)}', 'amount': rng.randint(1, 1500)}).encode()
- try:
- connection = http.client.HTTPConnection('127.0.0.1', port, timeout=10)
- connection.request('POST', '/inputs/buy', data, {'Content-Length': str(len(data)), 'Content-Type': 'application/json'})
- connection.getresponse().read()
- connection.close()
- except OSError:
- time.sleep(0.02)
-
- clients = [threading.Thread(target=client, args=(n,)) for n in range(3)]
- for thread in clients:
- thread.start()
- try:
- time.sleep(0.8)
- host.send_signal(signal.SIGKILL)
- host.wait()
- host = start()
- time.sleep(0.8)
- finally:
- stop.set()
- for thread in clients:
- thread.join()
- try:
- deadline = time.time() + 30
- while number('paid_view') + number('declined_view') < number('sent_view') and time.time() < deadline:
- time.sleep(0.1)
- sent, paid, declined, total = (number(v) for v in ('sent_view', 'paid_view', 'declined_view', 'paid_total_view'))
- with lock:
- keys = list(answers.values())
- assert paid + declined == sent == len(keys), (paid, declined, sent, len(keys))
- assert paid == sum(1 for _, status, _ in keys if status == 200), (paid, len(keys))
- assert total == sum(body['amount'] for body, status, _ in keys if status == 200)
- finally:
- host.terminate()
- _, error = host.communicate(timeout=30)
- server.shutdown()
- stats = [json.loads(line) for line in error.decode().splitlines() if line.startswith('{"active"')][-1]
- return {'orders': sent, 'paid': paid, 'declined': declined, 'answers_after_restart': stats['answers']}
-
-
-BEAT_SOURCE = """state beats = 0
-state last = 0
-state stamped = 0
-input beat = every 50 ms
-input stamp = message {
- note: text
-}
-on beat {
- beats = beats + 1
- last = now
-}
-on stamp { stamped = now }
-view beats_view = beats
-view last_view = last
-view stamped_view = stamped
-"""
-
-
-def server_timers(aup, pure, work):
- """A served app's timers tick on the server's clock and its events see
- the time they were applied, on one thread and with an owner; the ticks
- are saved, so a restarted server goes on from them."""
- import time
- module, source = work / 'beat.pmod', work / 'beat.aup'
- source.write_text(BEAT_SOURCE)
- compile_module(aup, source, module)
- seen = {}
- for threads in (1, 3):
- store = work / f'beat-{threads}'
- beats = 0
- for run_number in range(2):
- host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', threads])
- try:
- posted = time.time() * 1000
- assert http(host.port, 'POST', '/inputs/stamp', b'x')[0] == 200
- time.sleep(0.6)
- before = time.time() * 1000
- read = lambda view: float(http(host.port, 'GET', f'/views/{view}')[3])
- count, last, stamped = read('beats_view'), read('last_view'), read('stamped_view')
- after = time.time() * 1000 + 1 # the server's clock reads whole milliseconds
- assert count >= beats + 5, (threads, run_number, count, beats)
- assert before - 1000 < last <= after and posted - 50 <= stamped <= before, (last, stamped, posted, before)
- beats = count
- finally:
- stats = host.stop()
- assert stats[0]['ticks'] >= 5, stats
- seen[threads] = beats
- return seen
-
-
-def server_app(aup, pure, work, seeds, operations=200, threads=1):
- """An app served by its views and inputs, no routes written: random
- POSTs (a message with query fields and a body, a text, a key) and GETs
- (a number, a text, a table's pages) answer exactly as a Python model
- says, failures with their fault; what was acknowledged is there after
- the server stops, and after it is killed while clients post. The app's
- owner thread applies the inputs; connection threads (one or several)
- show views from the snapshot of its last save."""
- import signal
- import threading
- import time
- module = work / 'served.pmod'
- source = work / 'served.aup'
- source.write_text(SERVED_SOURCE)
- compile_module(aup, source, module)
- names = ['ann', 'bob', 'zed', '가', 'a"b', 'back\\slash', 'tab\t', '', 'bob2']
- compared = 0
- for seed in seeds:
- rng, board, at = random.Random(seed + 9100), {}, 0
- store = work / f'served-{seed}-{threads}'
- serve = [pure / 'build/pure-serve', '--module', module, '--threads', threads]
- host = HttpHost(serve + ['--store', store])
- order = lambda: sorted(board, key=lambda name: name.encode())
-
- def place(name):
- nonlocal at
- keys = order()
- if name not in board:
- position = sum(1 for k in keys if k.encode() < name.encode())
- board[name] = 0
- if len(board) > 1 and at >= position: at += 1
-
- try:
- for _ in range(operations):
- roll, keys = rng.random(), order()
- if roll < 0.35:
- name, points = rng.choice(names), rng.randint(0, 9)
- if rng.random() < 0.05:
- request = ('POST', '/inputs/score', b'x') # no points
- want = (400, 'missing_field', b'')
- elif rng.random() < 0.05:
- request = ('POST', f'/inputs/score?points={points}', b'\xff')
- want = (422, 'encoding', b'')
- else:
- request = ('POST', f'/inputs/score?points={points}', name.encode())
- place(name)
- board[name] += points
- at = order().index(name)
- want = (200, 'none', name.encode())
- elif roll < 0.5:
- name = rng.choice(names)
- request = ('POST', '/inputs/join', name.encode())
- if name in board: want = (409, 'exists', b'')
- else:
- place(name)
- at = order().index(name)
- want = (200, 'none', b'')
- elif roll < 0.58:
- request = ('POST', '/inputs/drop', b'')
- if not board: want = (404, 'not_found', b'')
- else:
- del board[keys[at]]
- at = min(at, max(len(board) - 1, 0))
- want = (200, 'none', b'')
- elif roll < 0.68:
- request = ('GET', '/views/total', b'')
- want = (200, 'none', str(sum(board.values())).encode())
- elif roll < 0.78:
- request = ('GET', '/views/current', b'')
- want = (200, 'none', keys[at].encode()) if board else (404, 'no_value', b'')
- elif roll < 0.96:
- first, count = rng.randint(0, 4), rng.choice([1, 2, 5, 100])
- request = ('GET', f'/views/leaders?from={first}&count={count}', b'')
- shown = min(first, len(keys))
- rows = [{'name': k, 'points': board[k]} for k in keys[shown:shown + count]]
- want = (200, 'none', {'rows': len(keys), 'from': shown, 'items': rows})
- else:
- request, want = rng.choice([(('GET', '/views/nope', b''), (404, 'no_view', b'')),
- (('POST', '/inputs/nope', b''), (404, 'no_input', b'')),
- (('POST', '/inputs/join?x=1', b'q'), (400, 'query', b'')),
- (('GET', '/views/total?from=1', b''), (400, 'query', b''))])
- status, error, kind, body = http(host.port, *request)
- if isinstance(want[2], dict):
- assert kind == 'application/json', kind
- body = json.loads(body)
- assert (status, error, body) == want, (seed, request, (status, error, body), want)
- compared += 1
- finally:
- stats = host.stop()
- assert stats[0]['bursts'] <= stats[0]['requests'] and stats[0]['owner'], stats
- assert stats[0]['views_from_snapshots'] > 0, stats
- # A graceful stop kept every acknowledged event.
- host = HttpHost(serve + ['--store', store])
- try:
- got = json.loads(http(host.port, 'GET', '/views/leaders')[3])
- assert got['items'] == [{'name': k, 'points': board[k]} for k in order()], (seed, got)
- finally:
- host.stop()
-
- # Clients post while the server is killed: each acknowledged point is
- # stored. Readers on other connections meanwhile see every point
- # acknowledged before they asked, never a total going back, and never one
- # that was not saved: the total after the kill is at least any they saw.
- store = work / f'served-kill-{threads}'
- serve = [pure / 'build/pure-serve', '--module', module, '--threads', threads]
- host = HttpHost(serve + ['--store', store])
- acknowledged = [0] * 6
- seen = [0, 0]
-
- def client(number):
- for _ in range(10000):
- try:
- status, _, _, _ = http(host.port, 'POST', '/inputs/score?points=1', f'c{number}'.encode())
- except OSError:
- return
- if status != 200:
- return
- acknowledged[number] += 1
-
- def reader(number):
- for _ in range(100000):
- floor = sum(acknowledged)
- try:
- status, _, _, body = http(host.port, 'GET', '/views/total')
- except OSError:
- return
- if status != 200:
- return
- total = int(body)
- assert floor <= total and seen[number] <= total, (floor, seen[number], total)
- seen[number] = total
-
- clients = [threading.Thread(target=client, args=(n,)) for n in range(len(acknowledged))]
- clients += [threading.Thread(target=reader, args=(n,)) for n in range(len(seen))]
- for thread in clients:
- thread.start()
- time.sleep(0.8)
- host.process.send_signal(signal.SIGKILL)
- host.process.wait()
- for thread in clients:
- thread.join()
- host = HttpHost(serve + ['--store', store])
- try:
- stored = {row['name']: row['points'] for row in json.loads(http(host.port, 'GET', '/views/leaders')[3])['items']}
- for number, count in enumerate(acknowledged):
- assert count > 0 and stored.get(f'c{number}', 0) in (count, count + 1), (number, count, stored)
- assert min(seen) > 0 and max(seen) <= sum(stored.values()), (seen, stored)
- finally:
- host.stop()
- return compared, sum(acknowledged)
-
-
FOLDS_SOURCE = """record Task {
title: text
done = 0
@@ -2397,23 +1243,6 @@ def main():
report['library_programs'], report['library_diagnostics'] = libraries(aup, pure_run, work)
report['editor_parity_operations'], report['editor_parity_outcomes'] = \
editor_parity(aup, args.pure.resolve(), work, range(args.editor_seeds))
- report['server_parity_requests'] = sum(server_parity(aup, args.pure.resolve(), work, range(args.editor_seeds),
- threads=threads) for threads in (1, 3))
- report['server_pipelined_answers'] = server_pipelining(aup, args.pure.resolve(), work)
- report['server_lent_read_bytes'] = server_lent_reads(aup, args.pure.resolve(), work)
- report['server_lent_budget_bytes'] = server_lent_budget(aup, args.pure.resolve(), work)
- report['server_lent_views'] = [server_lent_views(aup, args.pure.resolve(), work, threads) for threads in (1, 3)]
- report['server_bursts'] = server_bursts(aup, args.pure.resolve(), work)
- report['server_bursts_three_threads'] = server_bursts(aup, args.pure.resolve(), work, shards=3)
- report['server_app_requests'], report['server_app_acknowledged_before_kill'] = \
- server_app(aup, args.pure.resolve(), work, range(args.editor_seeds))
- report['server_app_owner_requests'], report['server_app_owner_acknowledged_before_kill'] = \
- server_app(aup, args.pure.resolve(), work, range(args.editor_seeds), threads=3)
- report['server_json_requests'] = server_json(aup, args.pure.resolve(), work)
- report['server_timer_beats'] = server_timers(aup, args.pure.resolve(), work)
- report['server_links'] = server_links(aup, args.pure.resolve(), work)
- report['server_webhook'] = server_webhook(aup, args.pure.resolve(), work)
- report['server_answers'] = server_answers(aup, args.pure.resolve(), work)
print(json.dumps(report))