aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-30 06:44:35 +0100
committereunhhu <[email protected]>2026-09-30 06:44:35 +0100
commitb3c01b3a26a7b5fe60b70739a6fc7b5967e0d9c8 (patch)
treef47d4727d954b8260a5eaa8da30e1d28f0e2f47e /tests
parent7705831ebdf94186d1513f3e1b8d7d5ab8b3a2c0 (diff)
downloadvega-b3c01b3a26a7b5fe60b70739a6fc7b5967e0d9c8.tar.gz
vega-b3c01b3a26a7b5fe60b70739a6fc7b5967e0d9c8.zip
Add a board two nodes share
examples/shared-board.aup is the board grown in Pure's research 105: each node sends a changed card whole to the other (send changed { ..at }) and takes the other's whole when it is the later change (cards[k] = Card { ..remote }), by its time and then by the node's name, so both boards end the same whatever order changes come in. The input the other node sends to is the output's like (message like). A new field goes in the card and the output alone. tests/check.py shared_board runs Alice's and Bob's nodes linked both ways over pure://, sealed, each taking the other as its sender: cards added, moved and given on either agree; Bob cut off, both give one card (Bob first, Alice after) and Bob adds one; joined again, both boards take Alice's and Bob's card.
Diffstat (limited to 'tests')
-rw-r--r--tests/check.py109
1 files changed, 108 insertions, 1 deletions
diff --git a/tests/check.py b/tests/check.py
index b556b3a..7bb4c41 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -1701,6 +1701,112 @@ def width_of(character):
return width(character) or 0
+
+def shared_board(serve, module, work):
+ """Two nodes of examples/shared-board.aup, Alice's and Bob's, each with a
+ key of its own, linked both ways over pure:// (sealed, each taking the
+ other as its sender). Cards added, moved and given on either reach the
+ other, and both boards end the same. Then Bob's node is cut off (it
+ listens and sends where no one is) while both give one card to someone,
+ Bob first and Alice after, and Bob adds a card; joined again, both
+ boards take Alice's, the later change, and Bob's card."""
+ import http.client
+ import select
+ import signal
+ import socket
+ import time
+ nodes, keys = {}, {}
+
+ def udp_port():
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:
+ probe.bind(('127.0.0.1', 0))
+ return probe.getsockname()[1]
+
+ ports = {'alice': udp_port(), 'bob': udp_port()}
+
+ def launch(args):
+ process = subprocess.Popen([str(a) for a in args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ ready, _, _ = select.select([process.stdout], [], [], 10)
+ line = process.stdout.readline() if ready else b''
+ assert line, 'node did not start'
+ return process, json.loads(line)
+
+ def stop(name):
+ process = nodes.pop(name)[0]
+ process.send_signal(signal.SIGTERM)
+ _, error = process.communicate(timeout=30)
+ assert process.returncode == 0, error[-800:]
+
+ for name in ('alice', 'bob'): # the keys first
+ process, first = launch([serve, '--module', module, '--store', work / f'board-{name}', '--port', 0,
+ '--key', work / f'board-{name}.key'])
+ keys[name] = first['public_key']
+ nodes[name] = (process, 0)
+ stop(name)
+
+ def start(name, cut=False):
+ other = 'bob' if name == 'alice' else 'alice'
+ listen, to = (udp_port(), udp_port()) if cut else (ports[name], ports[other])
+ process, first = launch([serve, '--module', module, '--store', work / f'board-{name}', '--port', 0,
+ '--key', work / f'board-{name}.key', '--name', name, '--link-port', listen,
+ '--send', f'changed=pure://127.0.0.1:{to}/remote', '--seal', f'changed={keys[other]}',
+ '--sender', f'{other}.changed={keys[other]}'])
+ assert first['name'] == name, first
+ nodes[name] = (process, first['port'])
+
+ def call(name, path, body=None):
+ connection = http.client.HTTPConnection('127.0.0.1', nodes[name][1], timeout=10)
+ headers = {}
+ if body is not None:
+ data = body.encode() if isinstance(body, str) else json.dumps(body).encode()
+ headers = {'Content-Length': str(len(data)),
+ 'Content-Type': 'text/plain' if isinstance(body, str) else 'application/json'}
+ connection.request('POST' if body is not None else 'GET', path, data if body is not None else None, headers)
+ answer = connection.getresponse()
+ text = answer.read().decode()
+ connection.close()
+ return answer.status, text
+
+ def board(name):
+ return sorted((c['label'], c['badge']) for c in json.loads(call(name, '/views/cards_list')[1])['items'])
+
+ def agree(expected=None, seconds=20):
+ deadline = time.time() + seconds
+ while time.time() < deadline:
+ a, b = board('alice'), board('bob')
+ if a == b and (expected is None or a == expected):
+ return a
+ time.sleep(0.05)
+ raise AssertionError((board('alice'), board('bob'), expected))
+
+ try:
+ for name in ('alice', 'bob'):
+ start(name)
+ assert call(name, '/inputs/name', {'name': name})[0] == 200
+ for title in ('plan the week', 'write the tests'):
+ assert call('alice', '/inputs/add', title)[0] == 200
+ assert call('bob', '/inputs/add', 'fix the build')[0] == 200
+ assert call('alice', '/inputs/forward', '')[0] == 200 # write the tests: doing
+ agree(seconds=20)
+ assert call('bob', '/inputs/assign', {'title': 'plan the week', 'who': 'bob'})[0] == 200
+ together = agree([('fix the build', 'to do'), ('plan the week', 'bob, to do'), ('write the tests', 'doing')])
+ stop('bob')
+ start('bob', cut=True)
+ assert call('bob', '/inputs/assign', {'title': 'fix the build', 'who': 'bob'})[0] == 200
+ time.sleep(0.05) # Alice's is the later change
+ assert call('alice', '/inputs/assign', {'title': 'fix the build', 'who': 'alice'})[0] == 200
+ assert call('bob', '/inputs/add', 'made while cut off')[0] == 200
+ time.sleep(0.5)
+ assert ('fix the build', 'bob, to do') in board('bob') and ('fix the build', 'alice, to do') in board('alice')
+ stop('bob')
+ start('bob')
+ joined = agree([('fix the build', 'alice, to do'), ('made while cut off', 'to do'), ('plan the week', 'bob, to do'),
+ ('write the tests', 'doing')])
+ finally:
+ for name in list(nodes):
+ stop(name)
+ return {'cards': len(together), 'after_joining': len(joined)}
+
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--vega', type=Path, required=True)
@@ -1717,7 +1823,7 @@ def main():
work = Path(directory)
env = dict(os.environ, AUP_PATH=str(ROOT / 'lib'))
modules = {}
- for name in ('todo', 'scores', 'today', 'stopwatch', 'board', 'messenger'):
+ for name in ('todo', 'scores', 'today', 'stopwatch', 'board', 'messenger', 'shared-board'):
modules[name] = work / f'{name}.pmod'
built = run(aup, 'build', ROOT / 'examples' / f'{name}.aup', '-o', modules[name], env=env)
assert built.returncode == 0, built.stderr
@@ -1751,6 +1857,7 @@ def main():
report['live_display'] = live_display(vega, modules['todo'], work)
report['node_display'] = node_display(vega, args.serve.resolve(), modules['todo'], work)
report['messenger'] = messenger(vega, args.serve.resolve(), modules['messenger'], work)
+ report['shared_board'] = shared_board(args.serve.resolve(), modules['shared-board'], work)
print(json.dumps(report))