aboutsummaryrefslogtreecommitdiffstats
path: root/tests/check.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/check.py')
-rw-r--r--tests/check.py123
1 files changed, 123 insertions, 0 deletions
diff --git a/tests/check.py b/tests/check.py
index ea4f9f6..b377e20 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -1317,6 +1317,128 @@ def server_windows(aup, pure, work):
return {'windows_full': stats['a']['windows_full'], 'wire_deliveries': stats['a']['delivered'],
'link_records': stats['a']['link_records']}
+
+UPGRADED_SENDER = """state n = 0
+input buy = message {
+ item: text
+ count: u32
+}
+output ordered = message {
+ note: text
+ item: text
+ count: u32
+}
+on buy {
+ n = n + 1
+ send ordered { note: "second", item: buy.item, count: buy.count }
+}
+view sent = n
+"""
+
+UPGRADED_RECEIVER = """state orders = 0
+state noted = 0
+state stock = 0
+input order = message {
+ note: text
+ item: text
+ count: u32
+}
+on order {
+ orders = orders + 1
+ stock = stock + order.count
+ if order.note == "second" { noted = noted + 1 }
+}
+view count = orders
+view noted_count = noted
+view total = stock
+"""
+
+
+def server_upgrade(aup, pure, work):
+ """A rolling upgrade of an output's fields between two linked nodes. The
+ sender, its receiver down, holds five sends of ordered {item, count,
+ to.city}; started again with ordered {note, item, count} it recasts them
+ by name (note empty, to.city gone) where it was refused before (review
+ 004's F3), and a module without the output is refused naming it. The old
+ receiver refuses the new shape, so the sends wait; the upgraded receiver
+ takes all eight, once each."""
+ import select
+ import socket
+ import time
+ modules = {}
+ for name, text in (('sender-1', LINK_SENDER), ('sender-2', UPGRADED_SENDER), ('receiver-1', LINK_RECEIVER),
+ ('receiver-2', UPGRADED_RECEIVER), ('sender-0', 'state n = 0\ninput tap = key KeyT\non tap.down { n = 1 }\n')):
+ source = work / f'upgrade-{name}.aup'
+ source.write_text(text)
+ modules[name] = compile_module(aup, source, source.with_suffix('.pmod')) and source.with_suffix('.pmod')
+
+ def free_port(kind=socket.SOCK_STREAM):
+ with socket.socket(socket.AF_INET, kind) 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)
+ line = process.stdout.readline() if ready else b''
+ assert line, f'host did not start: {process.stderr.read(2000) if process.poll() is not None else ""}'
+ return process, json.loads(line)
+
+ def stop(process):
+ process.terminate()
+ _, error = process.communicate(timeout=30)
+ assert process.returncode == 0, error[-1000:]
+ return [json.loads(line) for line in error.decode().splitlines() if line.startswith('{"active"')][-1]
+
+ def number(port, view):
+ return int(http(port, 'GET', f'/views/{view}')[3])
+
+ keys = {}
+ for name in ('sender', 'receiver'):
+ process, info = start([pure / 'build/pure-serve', '--module', modules['receiver-1'], '--store',
+ work / 'upgrade-key', '--key', work / f'upgrade-{name}.key'], 0)
+ keys[name] = info['public_key']
+ stop(process)
+ port_a, port_b, link_port = free_port(), free_port(), free_port(socket.SOCK_DGRAM)
+
+ def sender_args(module):
+ return [pure / 'build/pure-serve', '--module', modules[module], '--store', work / 'upgrade-a', '--key',
+ work / 'upgrade-sender.key', '--name', 'up', '--send', f'ordered=pure://127.0.0.1:{link_port}/order',
+ '--seal', f'ordered={keys["receiver"]}']
+
+ def receiver_args(module):
+ return [pure / 'build/pure-serve', '--module', modules[module], '--store', work / 'upgrade-b', '--key',
+ work / 'upgrade-receiver.key', '--link-port', link_port, '--sender', f'up.ordered={keys["sender"]}']
+
+ running, stats, said = {}, {}, ''
+ try:
+ running['a'] = start(sender_args('sender-1'), port_a)[0]
+ for i in range(5):
+ assert post(port_a, '/inputs/buy', {'item': f'first-{i}', 'count': 1})[0] == 200
+ stop(running.pop('a'))
+ refused = subprocess.run([str(a) for a in sender_args('sender-0')] + ['--port', str(port_a)],
+ capture_output=True, timeout=30)
+ assert refused.returncode != 0 and b"sends wait on the output 'ordered'" in refused.stderr, refused.stderr
+ running['a'] = start(sender_args('sender-2'), port_a)[0]
+ for i in range(3):
+ assert post(port_a, '/inputs/buy', {'item': f'second-{i}', 'count': 10})[0] == 200
+ running['b'] = start(receiver_args('receiver-1'), port_b)[0]
+ time.sleep(1.5)
+ assert number(port_b, 'count') == 0 # the old input takes another shape: the sends wait
+ stop(running.pop('b'))
+ running['b'] = start(receiver_args('receiver-2'), port_b)[0]
+ deadline = time.time() + 20
+ while number(port_b, 'count') < 8 and time.time() < deadline:
+ time.sleep(0.05)
+ got = (number(port_b, 'count'), number(port_b, 'noted_count'), number(port_b, 'total'))
+ assert got == (8, 3, 35), got
+ finally:
+ for name, process in running.items():
+ stats[name] = stop(process)
+ assert stats['a']['link_refused'] >= 1, stats['a']
+ return {'recast_waiting': 5, 'delivered': 8, 'refused_while_old': stats['a']['link_refused']}
+
def server_links_independent(aup, pure, work):
"""A receiver written from docs/design/012-link-protocol.md alone, in
Python with the cryptography package and no code of link/: a Pure node
@@ -3023,6 +3145,7 @@ def main():
report['server_links_refused'] = server_links_refused(aup, pure, work)
report['server_memory'] = server_memory(aup, pure, work)
report['server_windows'] = server_windows(aup, pure, work)
+ report['server_upgrade'] = server_upgrade(aup, pure, work)
report['server_links_independent'] = server_links_independent(aup, pure, work)
report['server_links_moved'] = server_links_moved(aup, pure, work)
report['server_webhook'] = server_webhook(aup, pure, work)