From bfee839d3a3a1d42ba4fe83ce39ba83a733988b9 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Sat, 26 Sep 2026 04:54:32 +0100 Subject: Check the module server and saving in bursts The HTTP document server running editor.aup must answer random requests exactly as the server running editor.pure (status, revision, error and body); concurrent edits must share commits; and every edit it acknowledged must survive kill -9. Every other stored run of the list app and the text editor saves in bursts and still matches its reference model across restarts. --- tests/check.py | 156 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 153 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/tests/check.py b/tests/check.py index 934a285..efe17ee 100644 --- a/tests/check.py +++ b/tests/check.py @@ -14,8 +14,12 @@ 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; 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, and still match their reference models. + restarting at random points (every other run saving in bursts), and still + match their reference models. 7. List model: examples/tasks.aup keeps its list, index, views (conditional ones and a table with a row per task) and failed events exactly as a reference Python model under random appends, moves, toggles, copies, @@ -653,9 +657,10 @@ def stored_runs(aup, pure_run, work, seeds, steps=240): expected.append(('none', *model.expected())) cuts = sorted(rng.sample(range(1, steps), 8)) lines = [] - for start, end in zip([0] + cuts, cuts + [steps]): + for number, (start, end) in enumerate(zip([0] + cuts, cuts + [steps])): chunk = '\n'.join(commands[start:end]) + '\n' - lines += run(pure_run, module, '--store', store, stdin=chunk, check=False).stdout.splitlines() + batch = ['--batch', str(rng.randint(2, 9))] if number % 2 else [] # deferred saving, in bursts + lines += run(pure_run, module, '--store', store, *batch, stdin=chunk, check=False).stdout.splitlines() restarts += 1 assert len(lines) == steps, (source, seed, len(lines)) for line, command, (fault, states, views) in zip(lines, commands, expected): @@ -667,6 +672,149 @@ 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): + """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 / '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}']) + 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('