aboutsummaryrefslogtreecommitdiffstats
path: root/tests/check.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/check.py')
-rw-r--r--tests/check.py148
1 files changed, 148 insertions, 0 deletions
diff --git a/tests/check.py b/tests/check.py
index 024388a..500ffc0 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -1332,6 +1332,153 @@ def server_json(aup, pure, work, rounds=300):
return rounds + len(wrong) + rounds // 3 + len(wrong_groups)
+LINK_SENDER = """state n = 0
+state total = 0
+input buy = message {
+ item: text
+ count: u32
+}
+output ordered = message {
+ item: text
+ count: u32
+ to: {
+ city: text
+ }
+}
+on buy {
+ n = n + 1
+ total = total + buy.count
+ send ordered { item: buy.item, count: buy.count, to: { city: "Seoul" } }
+}
+view sent = n
+view sent_total = total
+"""
+
+LINK_RECEIVER = """state stock = 0
+state orders = 0
+input order = message {
+ item: text
+ count: u32
+ to: {
+ city: text
+ }
+}
+on order {
+ require order.count < 100
+ stock = stock + order.count
+ orders = orders + 1
+}
+view total = stock
+view count = orders
+"""
+
+
+def server_links(aup, pure, work):
+ """Two served apps, one's output wired to the other's input over HTTP
+ (--send). While four clients buy from the sender, the receiver and then
+ the sender are killed (-9) and started again on their stores. In the end
+ the receiver has applied every order the sender saved exactly once (its
+ total and count equal the sender's), a refused order (count 150) was
+ delivered once and did not hold back the order after it, and every
+ acknowledged order is among the saved ones."""
+ import select
+ import signal
+ import socket
+ import threading
+ import time
+ sender_module, receiver_module = work / 'link-sender.pmod', work / 'link-receiver.pmod'
+ for module, text in ((sender_module, LINK_SENDER), (receiver_module, LINK_RECEIVER)):
+ source = module.with_suffix('.aup')
+ source.write_text(text)
+ compile_module(aup, source, module)
+
+ def free_port():
+ with socket.socket() 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)
+ assert ready and process.stdout.readline(), 'host did not start'
+ return process
+
+ def post(port, path, body):
+ import http.client
+ connection = http.client.HTTPConnection('127.0.0.1', port, timeout=10)
+ try:
+ data = json.dumps(body).encode()
+ connection.request('POST', path, data, {'Content-Length': str(len(data)), 'Content-Type': 'application/json'})
+ response = connection.getresponse()
+ response.read()
+ return response.status
+ finally:
+ connection.close()
+
+ def number(port, view):
+ return int(http(port, 'GET', f'/views/{view}')[3])
+
+ port_a, port_b = free_port(), free_port()
+ receiver = [pure / 'build/pure-serve', '--module', receiver_module, '--store', work / 'link-b', '--threads', '2']
+ sender = [pure / 'build/pure-serve', '--module', sender_module, '--store', work / 'link-a',
+ '--send', f'ordered=http://127.0.0.1:{port_b}/inputs/order']
+ hosts = {'b': start(receiver, port_b), 'a': start(sender, port_a)}
+ acknowledged = [0] * 4
+ stop = threading.Event()
+
+ def client(which):
+ rng = random.Random(which + 7700)
+ while not stop.is_set():
+ count = rng.randint(1, 20)
+ try:
+ status = post(port_a, '/inputs/buy', {'item': f'c{which}', 'count': count})
+ except OSError:
+ time.sleep(0.02)
+ continue
+ if status == 200:
+ acknowledged[which] += count
+ else:
+ time.sleep(0.02)
+
+ clients = [threading.Thread(target=client, args=(n,)) for n in range(len(acknowledged))]
+ for thread in clients:
+ thread.start()
+ try:
+ for name, args, port in (('b', receiver, port_b), ('a', sender, port_a)):
+ time.sleep(0.6)
+ hosts[name].send_signal(signal.SIGKILL)
+ hosts[name].wait()
+ time.sleep(0.3)
+ hosts[name] = start(args, port)
+ time.sleep(0.6)
+ finally:
+ stop.set()
+ for thread in clients:
+ thread.join()
+ try:
+ assert post(port_a, '/inputs/buy', {'item': 'big', 'count': 150}) == 200
+ assert post(port_a, '/inputs/buy', {'item': 'after', 'count': 7}) == 200
+ deadline = time.time() + 30
+ while True:
+ sent, sent_total = number(port_a, 'sent'), number(port_a, 'sent_total')
+ count, total = number(port_b, 'count'), number(port_b, 'total')
+ if (count, total) == (sent - 1, sent_total - 150) or time.time() > deadline:
+ break
+ time.sleep(0.1)
+ assert (count, total) == (sent - 1, sent_total - 150), (count, total, sent, sent_total)
+ assert sent_total >= sum(acknowledged) + 157 and sum(acknowledged) > 0, (sent_total, acknowledged)
+ finally:
+ stats = {}
+ for name, process in hosts.items():
+ process.terminate()
+ _, error = process.communicate(timeout=30)
+ assert process.returncode == 0, error[-1000:]
+ stats[name] = [json.loads(line) for line in error.decode().splitlines() if line.startswith('{"active"')][-1]
+ return {'orders': sent, 'acknowledged_total': sum(acknowledged), 'delivered_after_restart': stats['a']['delivered'],
+ 'attempts_after_restart': stats['a']['delivery_attempts']}
+
+
BEAT_SOURCE = """state beats = 0
state last = 0
state stamped = 0
@@ -2036,6 +2183,7 @@ def main():
server_app(aup, args.pure.resolve(), work, range(args.editor_seeds), threads=3)
report['server_json_requests'] = server_json(aup, args.pure.resolve(), work)
report['server_timer_beats'] = server_timers(aup, args.pure.resolve(), work)
+ report['server_links'] = server_links(aup, args.pure.resolve(), work)
print(json.dumps(report))