diff options
| author | eunhhu <[email protected]> | 2026-09-26 05:42:09 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-26 05:42:09 +0100 |
| commit | 8461d74f19828967c849fc6f08f405d47216987d (patch) | |
| tree | 7f171875537cc00472b9b7e6e8ef4cfab04f2cac /tests | |
| parent | bfee839d3a3a1d42ba4fe83ce39ba83a733988b9 (diff) | |
| download | aup-8461d74f19828967c849fc6f08f405d47216987d.tar.gz aup-8461d74f19828967c849fc6f08f405d47216987d.zip | |
Check pure-serve, pipelined connections included
The server checks run pure-serve, which replaced the module mode of the
old document server. A keep-alive client and pipelined requests (sent in
one write, and in writes that cut heads and bodies) must get the same
answers, in order, as separate connections.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/check.py | 71 |
1 files changed, 65 insertions, 6 deletions
diff --git a/tests/check.py b/tests/check.py index efe17ee..5b7cac5 100644 --- a/tests/check.py +++ b/tests/check.py @@ -15,8 +15,9 @@ 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; concurrent edits share - commits, and every acknowledged edit survives kill -9. + requests exactly as the server running editor.pure, on persistent and + pipelined connections too; 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. @@ -716,7 +717,7 @@ def server_parity(aup, pure, work, seeds, operations=150): rng = random.Random(seed + 13000) old = HttpHost([pure / 'build/tree/pure-connections', '--program', pure / 'examples/editor.pure', '--store', work / f'http-old-{seed}']) - new = HttpHost([pure / 'build/pure-connections-module', '--module', module, '--store', work / f'http-new-{seed}']) + new = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / f'http-new-{seed}']) try: revision, size = {}, {} for _ in range(operations): @@ -749,6 +750,63 @@ def server_parity(aup, pure, work, seeds, operations=150): 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_bursts(aup, pure, work, clients=8, edits=30): """Concurrent edits share commits, and every acknowledged edit survives a kill -9 of the server.""" @@ -780,7 +838,7 @@ def server_bursts(aup, pure, work, clients=8, edits=30): timings = {} for name, args in [('program', [pure / 'build/tree/pure-connections', '--program', pure / 'examples/editor.pure']), - ('module', [pure / 'build/pure-connections-module', '--module', module])]: + ('module', [pure / 'build/pure-serve', '--module', module])]: host = HttpHost(args + ['--store', work / f'bursts-{name}']) acknowledged = [(0, 0)] * clients threads, start = load(host, edits, acknowledged) @@ -794,7 +852,7 @@ def server_bursts(aup, pure, work, clients=8, edits=30): 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 / 'bursts-module' - host = HttpHost([pure / 'build/pure-connections-module', '--module', module, '--store', store]) + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store]) acknowledged = [(edits, edits)] * clients threads, _ = load(host, 1000, acknowledged) time.sleep(0.5) @@ -802,7 +860,7 @@ def server_bursts(aup, pure, work, clients=8, edits=30): host.process.wait() for thread in threads: thread.join() - host = HttpHost([pure / 'build/pure-connections-module', '--module', module, '--store', store]) + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store]) try: for number, (count, base) in enumerate(acknowledged): status, tag, _, body = host.request('GET', f'c{number}') @@ -948,6 +1006,7 @@ def main(): report['editor_parity_operations'], report['editor_parity_outcomes'] = \ editor_parity(aup, args.pure.resolve(), work, range(args.editor_seeds)) report['server_parity_requests'] = server_parity(aup, args.pure.resolve(), work, range(args.editor_seeds)) + report['server_pipelined_answers'] = server_pipelining(aup, args.pure.resolve(), work) report['server_bursts'] = server_bursts(aup, args.pure.resolve(), work) print(json.dumps(report)) |
