aboutsummaryrefslogtreecommitdiffstats
path: root/tests/check.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/check.py')
-rw-r--r--tests/check.py158
1 files changed, 158 insertions, 0 deletions
diff --git a/tests/check.py b/tests/check.py
index d262b8e..cc33947 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -1023,6 +1023,163 @@ def server_surface(aup, pure, work):
return {'events': 5, 'lines': len(got)}
+SHOP = '''
+record Stock {
+ item = 0
+ left = 0
+}
+record Line {
+ item = 0
+ qty = 1
+ note: text
+}
+record Entry {
+ item = 0
+ qty = 0
+ note: text
+}
+state stock: list of Stock by item
+state log: list of Entry
+state orders = 0
+
+input restock = message {
+ rows: list of Stock
+}
+input order = message {
+ customer: u32
+ lines: list of Line
+}
+input clean = message {
+ below: number
+}
+
+on restock {
+ for r in restock.rows {
+ stock.add(Stock { item: r.item, left: r.left })
+ }
+}
+on order {
+ for l in order.lines {
+ stock[l.item].left = stock[l.item].left - l.qty
+ require stock[l.item].left >= 0
+ log.append(Entry { item: l.item, qty: l.qty, note: l.note })
+ }
+ orders = orders + order.lines.count
+}
+on clean {
+ for s in stock where s.left < clean.below {
+ stock.remove(s)
+ }
+}
+view items = stock.count
+view left = sum(s.left for s in stock)
+view logged = log.count
+view total = sum(e.qty for e in log)
+view lines = orders
+view notes = each e in log {
+ item = e.item
+ note = e.note
+}
+'''
+
+
+def server_rows(aup, pure, work):
+ """A message's rows (a list of a record: a JSON array of objects, or the
+ list's stored form as the body) walked with for, all or nothing: 30,000
+ stock rows in one request, then 20,000 as bytes, then random orders whose
+ rows take stock (an item not stocked or not enough of it fails the whole
+ order, none of its rows applied), a walk that removes, and a restart that
+ keeps every state and no rows. Checked against a model."""
+ import http.client
+ import random
+ import struct
+ import time
+ source, module = work / 'shop.aup', work / 'shop.pmod'
+ source.write_text(SHOP)
+ compile_module(aup, source, module)
+ store = work / 'shop-store'
+ host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store])
+
+ def call(method, path, body=None, kind='application/json'):
+ connection = http.client.HTTPConnection('127.0.0.1', host.port, timeout=60)
+ try:
+ connection.request(method, path, body, {'Content-Type': kind} if body is not None else {})
+ answer = connection.getresponse()
+ return answer.status, answer.getheader('X-Pure-Error'), answer.read()
+ finally:
+ connection.close()
+
+ def number(view):
+ status, _, data = call('GET', f'/views/{view}')
+ assert status == 200, (view, status)
+ return float(data)
+
+ stock, log = {}, []
+ try:
+ began = time.perf_counter()
+ rows = [{'item': i, 'left': i % 7} for i in range(30000)]
+ assert call('POST', '/inputs/restock', json.dumps({'rows': rows}).encode())[:2] == (200, 'none')
+ seconds = time.perf_counter() - began
+ stock.update({i: i % 7 for i in range(30000)})
+ packed = b''.join(struct.pack('<dd', i, 5) for i in range(30000, 50000))
+ assert call('POST', '/inputs/restock', packed, 'application/octet-stream')[:2] == (200, 'none')
+ stock.update({i: 5 for i in range(30000, 50000)})
+ assert number('items') == 50000 and number('left') == sum(stock.values())
+ # All or nothing: a row whose item is there already fails the whole message.
+ assert call('POST', '/inputs/restock', json.dumps({'rows': [{'item': 60000}, {'item': 7}]}).encode())[:2] == \
+ (409, 'exists')
+ assert call('POST', '/inputs/restock', packed[:40], 'application/octet-stream')[:2] == (422, 'limit')
+ assert call('POST', '/inputs/restock', json.dumps({'rows': [{'item': 1, 'lft': 2}]}).encode())[:2] == \
+ (400, 'json_field')
+ assert call('POST', '/inputs/restock', json.dumps({'rows': [{'item': 'one'}]}).encode())[:2] == (400, 'json_type')
+ assert number('items') == 50000
+ rng = random.Random(21)
+ notes = ['', 'fast', '빨리', 'a "quoted" \\ note', 'tab\there', '😀']
+ applied = refused = 0
+ for _ in range(400):
+ lines = [{'item': rng.randrange(50010), 'qty': rng.randrange(4), 'note': rng.choice(notes)}
+ for _ in range(rng.randrange(6))]
+ if lines and rng.random() < 0.3:
+ del lines[0]['qty'] # a field left out takes its default (1)
+ model = dict(stock)
+ ok = True
+ for line in lines:
+ qty = line.get('qty', 1)
+ if line['item'] not in model or model[line['item']] - qty < 0:
+ ok = False
+ break
+ model[line['item']] -= qty
+ status, error, _ = call('POST', '/inputs/order', json.dumps({'customer': 3, 'lines': lines}).encode())
+ assert (status == 200) == ok, (status, error, lines)
+ if ok:
+ stock = model
+ log.extend((line['item'], line.get('qty', 1), line['note']) for line in lines)
+ applied += 1
+ else:
+ refused += 1
+ assert number('logged') == len(log) and number('total') == sum(q for _, q, _ in log)
+ assert number('left') == sum(stock.values()) and number('lines') == len(log)
+ shown = json.loads(call('GET', f'/views/notes?from={max(0, len(log) - 50)}')[2])['items']
+ assert [(r['item'], r['note']) for r in shown] == [(i, n) for i, _, n in log[-50:]]
+ assert call('POST', '/inputs/clean', json.dumps({'below': 2}).encode())[:2] == (200, 'none')
+ stock = {item: left for item, left in stock.items() if left >= 2}
+ assert number('items') == len(stock) and number('left') == sum(stock.values())
+ finally:
+ host.stop()
+ # Restarted: the states as they were; rows are never kept, and come again.
+ host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store])
+ try:
+ assert number('items') == len(stock) and number('logged') == len(log) and number('lines') == len(log)
+ assert call('POST', '/inputs/restock', json.dumps({'rows': [{'item': 90000, 'left': 1}]}).encode())[:2] == \
+ (200, 'none')
+ assert number('items') == len(stock) + 1
+ finally:
+ host.stop()
+ assert applied > 50 and refused > 50
+ return {'bulk_rows_json': 30000, 'bulk_rows_bytes': 20000, 'bulk_rows_per_second': round(30000 / seconds),
+ 'orders_applied': applied, 'orders_refused': refused, 'logged': len(log)}
+
+
def server_queries(aup, pure, work):
"""Queries as verified programs over a snapshot (PUT and GET /queries/NAME):
aup query compiles a bank's declarations with views of its own; the node
@@ -1608,6 +1765,7 @@ def main():
report['server_sealed'] = server_sealed(aup, pure, work)
report['server_surface'] = server_surface(aup, pure, work)
report['server_queries'] = server_queries(aup, pure, work)
+ report['server_rows'] = server_rows(aup, pure, work)
report['server_answers'] = server_answers(aup, pure, work)
print(json.dumps(report))