aboutsummaryrefslogtreecommitdiffstats
path: root/tests/check.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/check.py')
-rw-r--r--tests/check.py253
1 files changed, 253 insertions, 0 deletions
diff --git a/tests/check.py b/tests/check.py
index 91fa9b9..3876bf8 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -3980,6 +3980,258 @@ def server_apps_links(aup, pure, work):
return {'sent': sent, 'killed_at': killed_at, 'handed': near_stats['handed'], 'small_held': held}
+LIFE_SOURCE = """state n = 0
+state hits = 0
+input add = message { step: number }
+on add {
+ n = n + add.step
+ hits = hits + 1
+}
+view total = n
+view count = hits
+"""
+# Its next version: another input before add (add's number moves), and a state more.
+LIFE_NEXT = """state twice = 0
+state n = 0
+state hits = 0
+input reset = message { to: number }
+input add = message { step: number }
+on reset {
+ n = reset.to
+}
+on add {
+ n = n + add.step
+ hits = hits + 1
+ twice = twice + 2 * add.step
+}
+view total = n
+view count = hits
+view doubled = twice
+"""
+LIFE_SENDER = """state n = 0
+input put = message { k: u32 }
+output here = message { k: u32 }
+output far = message { k: u32 }
+on put {
+ n = n + 1
+ send here { k: put.k }
+ send far { k: put.k }
+}
+view count = n
+"""
+# A taker's next version: an input before take, which takes a field more (the sends by name).
+LIFE_TAKER_NEXT = LINKS_TAKER.replace('input take = message { k: u32 }', """state pokes = 0
+input poke = message { x: u32 }
+input take = message {
+ note: text
+ k: u32
+}
+on poke {
+ pokes = pokes + poke.x
+}""")
+
+
+def server_apps_lifecycle(aup, pure, work):
+ """Apps start, stop and change on a running host (docs/design/017,
+ stage 3): on SIGHUP it reads its --apps file again and says what it did
+ on a line. An app added answers; an app removed goes (404 no_app) and
+ another process opens its store; apps change module while requests,
+ another app's local sends and another node's link records come to them,
+ the new modules' inputs at other numbers and taking the sends by name:
+ a write answered 200 is applied once and one answered 503 stopped not at
+ all, every send arrives once and in order, and the app left alone
+ answers every request. An app that could not open its store starts at
+ the next reading once it can; a file with a wrong line changes nothing;
+ after a kill -9 the host starts as the file says, the states as saved."""
+ import os
+ import signal
+ import select
+ import socket
+ import threading
+ import time
+ for name, text in (('life', LIFE_SOURCE), ('life-next', LIFE_NEXT), ('life-sender', LIFE_SENDER),
+ ('life-taker', LINKS_TAKER), ('life-taker-next', LIFE_TAKER_NEXT)):
+ (work / f'{name}.aup').write_text(text)
+ compile_module(aup, work / f'{name}.aup', work / f'{name}.pmod')
+ serve = pure / 'build/pure-serve'
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:
+ probe.bind(('127.0.0.1', 0))
+ link_port = probe.getsockname()[1]
+ keys = {}
+ for side in ('host', 'far'):
+ (work / f'life-{side}-key.conf').write_text(f"x {work / 'life.pmod'} {work / f'life-keys-{side}'}\n")
+ host = HttpHost([serve, '--apps', work / f'life-{side}-key.conf', '--key', work / f'life-{side}.key'])
+ keys[side] = host.info['public_key']
+ host.stop()
+ module = {name: work / f'{name}.pmod' for name in ('life', 'life-next', 'life-sender', 'life-taker', 'life-taker-next')}
+ store = {name: work / f'life-store-{name}' for name in ('a', 'b', 'c', 'src', 'dst', 'dstl', 'e', 'srcf')}
+ (work / 'life-ro').mkdir()
+ store['e'] = work / 'life-ro' / 'e'
+ lines = {'a': f"a {module['life']} {store['a']}", 'b': f"b {module['life']} {store['b']}",
+ 'src': f"src {module['life-sender']} {store['src']} send=here=dst/take",
+ 'dst': f"dst {module['life-taker']} {store['dst']}", 'dstl': f"dstl {module['life-taker']} {store['dstl']}"}
+ conf = work / 'life.conf'
+
+ def write(names):
+ conf.write_text(''.join(lines[name] + '\n' for name in names))
+
+ write(['a', 'b', 'src', 'dst', 'dstl'])
+ args = [serve, '--apps', conf, '--key', work / 'life-host.key', '--link-port', link_port, '--threads', 2,
+ '--sender', f"srcf.far={keys['far']}"]
+ host = HttpHost(args)
+ (work / 'life-far.conf').write_text(f"srcf {module['life-sender']} {store['srcf']} "
+ f"send=far=pure://127.0.0.1:{link_port}/dstl/take seal=far={keys['host']}\n")
+ far = HttpHost([serve, '--apps', work / 'life-far.conf', '--key', work / 'life-far.key'])
+
+ def reading(names=None):
+ """The host reads its file again: what it said."""
+ if names is not None:
+ write(names)
+ host.process.send_signal(signal.SIGHUP)
+ ready, _, _ = select.select([host.process.stdout], [], [], 30)
+ assert ready, 'no reading'
+ said = json.loads(host.process.stdout.readline())
+ return said, {app['name']: app for app in said.get('apps', [])}
+
+ def number(port, path):
+ status, why, _, body = http(port, 'GET', path)
+ assert status == 200, (path, status, why)
+ return int(body) if body not in (b'true', b'false') else body == b'true'
+
+ sent = 600
+ try:
+ # Added: it answers. Removed: its routes go, and its store is let go.
+ assert http(host.port, 'POST', '/a/inputs/add?step=5')[0] == 200
+ assert http(host.port, 'POST', '/b/inputs/add?step=7')[0] == 200
+ lines['c'] = f"c {module['life']} {store['c']}"
+ _, did = reading(['a', 'b', 'src', 'dst', 'dstl', 'c'])
+ assert did['c'] == {'name': 'c', 'running': True, 'did': 'started'} and did['a']['did'] == 'kept', did
+ assert http(host.port, 'POST', '/c/inputs/add?step=2')[0] == 200 and number(host.port, '/c/views/total') == 2
+ _, did = reading(['a', 'src', 'dst', 'dstl', 'c'])
+ assert did['b'] == {'name': 'b', 'running': False, 'did': 'removed'}, did
+ assert http(host.port, 'GET', '/b/views/total')[:2] == (404, 'no_app')
+ other = HttpHost([serve, '--module', module['life'], '--store', store['b']])
+ try:
+ assert number(other.port, '/views/total') == 7
+ finally:
+ other.stop()
+ # Changed under load: a, dst (another app's local sends) and dstl (another node's link records) take
+ # modules whose inputs are at other numbers, the takers' by name; then the senders start again, src
+ # (its local outlet) and, on the far node, srcf (its link); c, left alone, answers every request. Each
+ # change comes while the sends go (as far as each putter got: more than none, less than all).
+ answered, refused, done = [0], [0], threading.Event()
+ progress, trouble = {'src': 0, 'srcf': 0}, []
+
+ def adder():
+ while not done.is_set():
+ status, why, _, _ = http(host.port, 'POST', '/a/inputs/add?step=1')
+ if status == 200:
+ answered[0] += 1
+ elif (status, why) == (503, 'stopped'):
+ refused[0] += 1
+ else:
+ trouble.append(('a', status, why))
+
+ def putter(port, app):
+ for k in range(1, sent + 1):
+ status, why, _, _ = http(port, 'POST', f'/{app}/inputs/put?k={k}')
+ while (status, why) == (503, 'stopped'): # its app starting again: not applied, sent again
+ time.sleep(0.005)
+ status, why, _, _ = http(port, 'POST', f'/{app}/inputs/put?k={k}')
+ if status != 200:
+ trouble.append((app, k, status, why))
+ return
+ progress[app] = k
+ time.sleep(0.002)
+
+ def watcher():
+ while not done.is_set():
+ status, why, _, _ = http(host.port, 'GET', '/c/views/total')
+ if status != 200:
+ trouble.append(('c', status, why))
+
+ def once(app, k):
+ deadline = time.time() + 30
+ while progress[app] < k and time.time() < deadline and not trouble:
+ time.sleep(0.005)
+ return dict(progress), answered[0]
+
+ workers = [threading.Thread(target=adder), threading.Thread(target=putter, args=(host.port, 'src')),
+ threading.Thread(target=putter, args=(far.port, 'srcf')), threading.Thread(target=watcher)]
+ for thread in workers:
+ thread.start()
+ during = []
+ during.append(once('src', 100))
+ lines['a'] = f"a {module['life-next']} {store['a']}"
+ lines['dst'] = f"dst {module['life-taker-next']} {store['dst']}"
+ lines['dstl'] = f"dstl {module['life-taker-next']} {store['dstl']}"
+ _, did = reading(['a', 'src', 'dst', 'dstl', 'c'])
+ assert [did[name]['did'] for name in ('a', 'dst', 'dstl', 'c', 'src')] == \
+ ['restarted', 'restarted', 'restarted', 'kept', 'kept'], did
+ during.append(once('src', 200))
+ lines['dst'] = f"dst {module['life-taker']} {store['dst']}" # and back: take's number moves again
+ _, did = reading(['a', 'src', 'dst', 'dstl', 'c'])
+ assert did['dst']['did'] == 'restarted' and did['a']['did'] == 'kept', did
+ during.append(once('src', 300))
+ lines['src'] += ' queue=100000' # the sender starts again (its module as it was), its outbox going on
+ _, did = reading(['a', 'src', 'dst', 'dstl', 'c'])
+ assert did['src']['did'] == 'restarted' and did['dst']['did'] == 'kept', did
+ during.append(once('srcf', 400))
+ (work / 'life-far.conf').write_text((work / 'life-far.conf').read_text().replace('\n', ' queue=100000\n'))
+ far.process.send_signal(signal.SIGHUP) # the far node's sender starts again, its link with it
+ ready, _, _ = select.select([far.process.stdout], [], [], 30)
+ assert ready and json.loads(far.process.stdout.readline())['apps'][0]['did'] == 'restarted'
+ for thread in workers[1:3]:
+ thread.join()
+ done.set()
+ for thread in (workers[0], workers[3]):
+ thread.join()
+ assert not trouble, trouble[:5]
+ for put, adds in during: # each change came while the sends went
+ assert 0 < put['src'] < sent and 0 < put['srcf'] < sent and adds > 0, during
+ whole = [sent, sent * (sent + 1) // 2, True]
+ deadline = time.time() + 30
+ while time.time() < deadline and ([number(host.port, f'/dst/views/{v}') for v in ('count', 'sum', 'in_order')] != whole or
+ [number(host.port, f'/dstl/views/{v}') for v in ('count', 'sum', 'in_order')] != whole):
+ time.sleep(0.05)
+ for app in ('dst', 'dstl'):
+ got = [number(host.port, f'/{app}/views/{v}') for v in ('count', 'sum', 'in_order')]
+ assert got == whole, (app, got)
+ total = 5 + answered[0]
+ assert number(host.port, '/a/views/total') == total and number(host.port, '/a/views/count') == 1 + answered[0], \
+ (number(host.port, '/a/views/total'), answered[0], refused[0])
+ # An app whose store cannot be made yet stops alone; once it can, the next reading starts it.
+ lines['e'] = f"e {module['life']} {store['e']}"
+ os.chmod(work / 'life-ro', 0o500)
+ try:
+ _, did = reading(['a', 'src', 'dst', 'dstl', 'c', 'e'])
+ finally:
+ os.chmod(work / 'life-ro', 0o700)
+ assert not did['e']['running'] and 'store' in did['e']['why'] and did['c']['did'] == 'kept', did
+ _, did = reading()
+ assert did['e'] == {'name': 'e', 'running': True, 'did': 'started'}, did
+ assert http(host.port, 'POST', '/e/inputs/add?step=3')[0] == 200
+ # A file with a wrong line changes nothing.
+ good = conf.read_text()
+ conf.write_text(good + 'two words\n') # NAME MODULE STORE at least
+ said, _ = reading()
+ assert 'error' in said and number(host.port, '/a/views/total') == total, said
+ conf.write_text(good)
+ # After a kill -9 the host starts as the file says, its states as saved.
+ host.process.send_signal(signal.SIGKILL)
+ host.process.wait()
+ host = HttpHost(args)
+ assert all(app['running'] for app in host.info['apps']), host.info
+ assert sorted(app['name'] for app in host.info['apps']) == ['a', 'c', 'dst', 'dstl', 'e', 'src'], host.info
+ assert number(host.port, '/a/views/total') == total and number(host.port, '/e/views/total') == 3
+ assert [number(host.port, f'/dst/views/{v}') for v in ('count', 'sum', 'in_order')] == whole
+ finally:
+ host.stop()
+ far.stop()
+ return {'readings': 8, 'far_readings': 1, 'sent': sent, 'answered_while_changing': answered[0],
+ 'refused_while_changing': refused[0], 'puts_at_changes': [put['src'] for put, _ in during]}
+
+
def main():
global EDITOR
parser = argparse.ArgumentParser()
@@ -4031,6 +4283,7 @@ def main():
report['server_work'] = server_work(aup, pure, work)
report['server_apps'] = server_apps(aup, pure, work)
report['server_apps_links'] = server_apps_links(aup, pure, work)
+ report['server_apps_lifecycle'] = server_apps_lifecycle(aup, pure, work)
print(json.dumps(report))