diff options
| -rw-r--r-- | docs/design/015-boundary-meaning.md | 24 | ||||
| -rw-r--r-- | tests/check.py | 96 |
2 files changed, 84 insertions, 36 deletions
diff --git a/docs/design/015-boundary-meaning.md b/docs/design/015-boundary-meaning.md index 08a4636..41c8c0b 100644 --- a/docs/design/015-boundary-meaning.md +++ b/docs/design/015-boundary-meaning.md @@ -6,8 +6,8 @@ 보일 때 보기의 글자가 되고, 보낼 때 링크나 HTTP의 기록이 되어 다른 노드의 입력으로 옮겨진다. 이 문서는 그 모든 경계에서 값의 뜻이 무엇이고, 무엇을 그대로 옮기고 무엇을 거절하는지를 정한다. -한 줄로는 이렇다. **받아들인 값은 어느 길로 나오든 같다. 수는 비트까지(0의 부호만 빼고), text는 바이트까지. -받아들일 수 없는 값은 모든 들어오는 길이 거절하고, 거절한 사건은 아무것도 남기지 않는다.** +한 줄로는 이렇다. **받아들인 값은 어느 길로 나오든 같다. 수는 비트까지(0의 부호만 빼고), text는 바이트까지, bool은 +`true`나 `false`로. 받아들일 수 없는 값은 모든 들어오는 길이 거절하고, 거절한 사건은 아무것도 남기지 않는다.** ## 수 @@ -30,6 +30,21 @@ - **보낼 때.** 출력의 u32 필드에 준 수는 범위 안의 정수여야 한다. 아니면 그 사건이 실패한다. 아무것도 남지 않고, 보내지도 않는다. +## bool + +[016](016-bool-and-i64.md)이 더한 참·거짓이다. 수가 아니다. + +- **들어올 때.** JSON의 `true`와 `false`만 받는다. `1`, `0`, `"true"`, `null`은 `json_type`으로, `True`는 JSON이 아니라서 + `json`으로 거절한다. 쿼리의 값(퍼센트 인코딩을 푼 뒤)도 `true`와 `false`만이고, 그 밖은 `query`로 거절한다. 메시지의 행도 + 같다. 페이로드에서는 접두부의 1바이트이고, 0이나 1이 아니면 그 메시지가 `limit`으로 실패한다. +- **저장.** 상태와 목록의 bool은 수의 칸에 0이나 1로 담는다. 목록의 이미지는 그 필드의 종류를 bool(3)로 적는다. +- **보일 때.** 보기의 글자는 `true`나 `false`이고(보간과 같다), 표의 JSON에서는 JSON의 `true`와 `false`다. 화면도 같은 + 글자다. +- **보낼 때.** 링크는 bool을 bool인 필드로만 옮긴다(수로 넓히지 않는다). 받는 입력의 필드가 bool이 아니면 그 기록을 받지 + 않고 기다린다. HTTP 전달의 JSON은 `true`와 `false`다. +- **모듈 교체와 다시 열기.** 수가 bool이 되면 0(−0도)과 1은 `false`와 `true`가 되고, 다른 수는 새 기본값이 된다. bool이 수가 + 되면 0과 1이다. 교체가 출력의 필드를 바꾸면 기다리던 보냄의 값도 같은 규칙으로 옮긴다. + ## text - **들어올 때.** JSON 문자열은 이스케이프를 풀고 서로게이트 짝을 합친다. 날 본문은 바이트 그대로다. 결과는 UTF-8이어야 @@ -59,10 +74,11 @@ [`tests/check.py`](../../tests/check.py)의 `server_meaning`이 한 번에 본다. - 값: 수 22개(0과 −0, 2^53 부근, 가장 큰 수, 정규와 비정규의 경계, 반쯤에서의 언더플로, 1e21 부근), u32 넷, text 12개(한글, - 4바이트 UTF-8, 이스케이프, NUL, U+2028, BOM, 비문자, 4 KiB). + 4바이트 UTF-8, 이스케이프, NUL, U+2028, BOM, 비문자, 4 KiB), 그리고 모든 값에 `true`나 `false`인 bool 필드 하나. - 들어오는 길: JSON(ASCII로 이스케이프한 것과 UTF-8), 행, 쿼리와 날 본문, 링크(다른 순서, 넓힌 필드), HTTP 전달. - 나오는 길: 보기, 질의, 답, 재시작, 체크포인트 뒤의 재시작, 모듈 교체, 두 번 중계한 노드. -- 거절할 값 39가지(수, u32, text, 행)가 모든 들어오는 길에서 거절되고, 어디에도 남지 않는다. +- 거절할 값 52가지(수, u32, text, bool, 행)가 모든 들어오는 길에서 거절되고, 어디에도 남지 않는다. bool은 JSON, 쿼리, 행에서 + 거절의 종류까지 본다. 전달이 한 번씩, 순서대로 적용되는 것은 링크의 검사들(`server_links_udp_lossy`, `server_links_moved`, `server_upgrade`)과 [012](012-link-protocol.md)이 지킨다. 필드를 이름으로 받는 규칙은 `server_fields`가 지킨다. diff --git a/tests/check.py b/tests/check.py index 0c52cf4..117beff 100644 --- a/tests/check.py +++ b/tests/check.py @@ -3305,6 +3305,7 @@ MEANING_KEPT = """record Kept { t: text x = 0 label: text + ok = false } state kept: list of Kept by id """ @@ -3315,6 +3316,7 @@ MEANING_ROWS = """view rows = each k in kept { t = k.t x = k.x label = k.label + ok = k.ok } """ # A: what comes in (a JSON body, its rows, or a query and a plain body) is kept, and sent on; a put is answered @@ -3324,12 +3326,14 @@ MEANING_ORIGIN = MEANING_KEPT + """record Part { n = 0 u = 0 t: text + ok = false } input put = message { id: u32 n: number u: u32 t: text + ok: bool at: { x: number label: text @@ -3340,6 +3344,7 @@ input put_plain = message { n: number u: u32 t: text + ok: bool } input put_rows = message { parts: list of Part @@ -3350,19 +3355,19 @@ input touch = message { } output passed = message like put on put { - kept.add(Kept { id: put.id, n: put.n, u: put.u, t: put.t, x: put.at.x, label: put.at.label }) + kept.add(Kept { id: put.id, n: put.n, u: put.u, t: put.t, x: put.at.x, label: put.at.label, ok: put.ok }) send passed { ..put } reply put.t } on put_rows { for p in put_rows.parts { - kept.add(Kept { id: p.id, n: p.n, u: p.u, t: p.t, x: 0, label: "rows" }) - send passed { id: p.id, n: p.n, u: p.u, t: p.t, at: { x: 0, label: "rows" } } + kept.add(Kept { id: p.id, n: p.n, u: p.u, t: p.t, x: 0, label: "rows", ok: p.ok }) + send passed { id: p.id, n: p.n, u: p.u, t: p.t, ok: p.ok, at: { x: 0, label: "rows" } } } } on put_plain { - kept.add(Kept { id: put_plain.id, n: put_plain.n, u: put_plain.u, t: put_plain.t, x: 0, label: "" }) - send passed { id: put_plain.id, n: put_plain.n, u: put_plain.u, t: put_plain.t, at: { x: 0, label: "" } } + kept.add(Kept { id: put_plain.id, n: put_plain.n, u: put_plain.u, t: put_plain.t, x: 0, label: "", ok: put_plain.ok }) + send passed { id: put_plain.id, n: put_plain.n, u: put_plain.u, t: put_plain.t, ok: put_plain.ok, at: { x: 0, label: "" } } } on touch { kept[touch.id].t = touch.t @@ -3370,6 +3375,7 @@ on touch { """ + MEANING_ROWS # B: A's sends over the link, taken by name (fields in another order, u a number), kept and sent on over HTTP. MEANING_MIRROR = MEANING_KEPT + """input passed = message { + ok: bool at: { label: text x: number @@ -3381,7 +3387,8 @@ MEANING_MIRROR = MEANING_KEPT + """input passed = message { } output onward = message like passed on passed { - kept.add(Kept { id: passed.id, n: passed.n, u: passed.u, t: passed.t, x: passed.at.x, label: passed.at.label }) + kept.add(Kept { id: passed.id, n: passed.n, u: passed.u, t: passed.t, x: passed.at.x, label: passed.at.label, + ok: passed.ok }) send onward { ..passed } } """ + MEANING_ROWS @@ -3395,9 +3402,11 @@ MEANING_END = MEANING_KEPT + """input onward = message { x: number label: text } + ok: bool } on onward { - kept.add(Kept { id: onward.id, n: onward.n, u: onward.u, t: onward.t, x: onward.at.x, label: onward.at.label }) + kept.add(Kept { id: onward.id, n: onward.n, u: onward.u, t: onward.t, x: onward.at.x, label: onward.at.label, + ok: onward.ok }) } """ + MEANING_ROWS # The value set: every number a JSON reader must round correctly, u32s to its ends, texts JSON escapes and UTF-8. @@ -3419,8 +3428,9 @@ def server_meaning(aup, pure, work): out the same at every exit: A's view, a query on A, B's and C's views, A after a restart and after a checkpoint, B after its module was replaced. A number keeps its bits (zero has no sign: -0 shows as 0 and counts as 0), - a text its bytes. What must be refused is refused on every way in, and - leaves nothing behind (no row, no send).""" + a text its bytes, a bool is true or false (never 1 or 0). What must be + refused is refused on every way in, and leaves nothing behind (no row, no + send).""" import socket import struct import time @@ -3428,8 +3438,8 @@ def server_meaning(aup, pure, work): from urllib.parse import quote modules = {} for name, text in (('origin', MEANING_ORIGIN), ('mirror', MEANING_MIRROR), ('end', MEANING_END), - ('mirror-2', MEANING_MIRROR.replace(' id = 0\n n = 0\n u = 0\n t: text\n x = 0\n label: text\n', - ' label: text\n x = 0\n t: text\n u = 0\n n = 0\n id = 0\n'))): + ('mirror-2', MEANING_MIRROR.replace(' id = 0\n n = 0\n u = 0\n t: text\n x = 0\n label: text\n ok = false\n', + ' ok = false\n label: text\n x = 0\n t: text\n u = 0\n n = 0\n id = 0\n'))): source = work / f'meaning-{name}.aup' source.write_text(text) compile_module(aup, source, source.with_suffix('.pmod')) @@ -3467,6 +3477,16 @@ def server_meaning(aup, pure, work): finally: connection.close() + def refused_as(port, path, body, kind): # the status and X-Pure-Error of a request that must fail + connection = HTTPConnection('127.0.0.1', port, timeout=30) + try: + connection.request('POST', path, body, {'Content-Length': str(len(body)), 'Content-Type': kind}) + response = connection.getresponse() + response.read() + return response.status, response.getheader('X-Pure-Error') + finally: + connection.close() + def rows(name, path='/views/rows'): status, _, _, body = http(hosts[name].port, 'GET', path) assert status == 200, (name, path, status, body[:200]) @@ -3476,9 +3496,10 @@ def server_meaning(aup, pure, work): return float(shown) == want == 0 or struct.pack('<d', float(shown)) == struct.pack('<d', want) def differing(found, expected): - wrong = [(key, found.get(key)) for key, (n, u, t, x, label) in expected.items() + wrong = [(key, found.get(key)) for key, (n, u, t, x, label, ok) in expected.items() if key not in found or not (same(found[key]['n'], n) and float(found[key]['u']) == u and - found[key]['t'] == t and same(found[key]['x'], x) and found[key]['label'] == label)] + found[key]['t'] == t and same(found[key]['x'], x) and found[key]['label'] == label + and found[key]['ok'] is ok)] return wrong + [(key, 'not sent') for key in set(found) - set(expected)] hosts, expected, refused = {}, {}, 0 @@ -3492,26 +3513,28 @@ def server_meaning(aup, pure, work): for i, n in enumerate(MEANING_NUMBERS): u, t = MEANING_U32[i % len(MEANING_U32)], MEANING_TEXTS[i % len(MEANING_TEXTS)] x, label = MEANING_NUMBERS[(i + 5) % count], MEANING_TEXTS[(i + 3) % len(MEANING_TEXTS)] - ascii_only = i % 2 == 0 - body = '{"id": %d, "n": %s, "u": %d, "t": %s, "at": {"x": %s, "label": %s}}' % ( - i + 1, n, u, json.dumps(t, ensure_ascii=ascii_only), x, json.dumps(label, ensure_ascii=ascii_only)) + ascii_only, ok = i % 2 == 0, i % 3 != 0 + body = '{"id": %d, "n": %s, "u": %d, "t": %s, "ok": %s, "at": {"x": %s, "label": %s}}' % ( + i + 1, n, u, json.dumps(t, ensure_ascii=ascii_only), json.dumps(ok), x, json.dumps(label, ensure_ascii=ascii_only)) assert send(a, '/inputs/put', body.encode(), 'application/json', t.encode()) == 200, body[:200] - expected[i + 1] = (float(n), u, t, float(x), label) + expected[i + 1] = (float(n), u, t, float(x), label, ok) # In as rows of one message: a JSON array, placed as its list's stored form. - parts = [(501 + i, n, MEANING_U32[(i + 2) % len(MEANING_U32)], MEANING_TEXTS[(i + 4) % len(MEANING_TEXTS)]) + parts = [(501 + i, n, MEANING_U32[(i + 2) % len(MEANING_U32)], MEANING_TEXTS[(i + 4) % len(MEANING_TEXTS)], i % 2 == 0) for i, n in enumerate(MEANING_NUMBERS)] - body = '{"parts": [%s]}' % ', '.join('{"id": %d, "n": %s, "u": %d, "t": %s}' % (key, n, u, json.dumps(t)) - for key, n, u, t in parts) + body = '{"parts": [%s]}' % ', '.join('{"id": %d, "n": %s, "u": %d, "t": %s, "ok": %s}' % (key, n, u, json.dumps(t), + json.dumps(ok)) + for key, n, u, t, ok in parts) assert send(a, '/inputs/put_rows', body.encode(), 'application/json') == 200, body[:200] - expected.update({key: (float(n), u, t, 0.0, 'rows') for key, n, u, t in parts}) + expected.update({key: (float(n), u, t, 0.0, 'rows', ok) for key, n, u, t, ok in parts}) # In as a query (%XX where a client would write it so) and a plain body. for i, n in enumerate(MEANING_NUMBERS): - u, t = MEANING_U32[(i + 1) % len(MEANING_U32)], MEANING_TEXTS[(i + 2) % len(MEANING_TEXTS)] - assert send(a, f'/inputs/put_plain?id={101 + i}&n={quote(n, safe="")}&u={u}', t.encode(), + u, t, ok = MEANING_U32[(i + 1) % len(MEANING_U32)], MEANING_TEXTS[(i + 2) % len(MEANING_TEXTS)], i % 2 == 1 + assert send(a, f'/inputs/put_plain?id={101 + i}&n={quote(n, safe="")}&u={u}&ok={json.dumps(ok)}', t.encode(), 'text/plain; charset=utf-8') == 200, n - expected[101 + i] = (float(n), u, t, 0.0, '') - # What must be refused: numbers past the finite, u32s out of range or not whole, texts that are no UTF-8. - good = '{"id": %d, "n": %s, "u": %s, "t": %s, "at": {"x": 1, "label": ""}}' + expected[101 + i] = (float(n), u, t, 0.0, '', ok) + # What must be refused: numbers past the finite, u32s out of range or not whole, texts that are no UTF-8, + # bools that are not true or false. + good = '{"id": %d, "n": %s, "u": %s, "t": %s, "ok": true, "at": {"x": 1, "label": ""}}' bad_json = [good % (201, '1e999', '1', '"t"'), good % (202, '-1e999', '1', '"t"'), good % (203, 'NaN', '1', '"t"'), good % (204, '00', '1', '"t"'), good % (205, '+1', '1', '"t"'), good % (206, '.5', '1', '"t"'), good % (207, '1', '4294967296', '"t"'), good % (208, '1', '-1', '"t"'), good % (209, '1', '1.5', '"t"'), @@ -3519,27 +3542,36 @@ def server_meaning(aup, pure, work): good % (213, '1', '1', '"a\\ud83d"'), good % (214, '1', '1', '"\\udc00\\ud800"')] for body in bad_json: assert send(a, '/inputs/put', body.encode(), 'application/json') == 400, body + not_bools = [('1', 'json_type'), ('0', 'json_type'), ('"true"', 'json_type'), ('null', 'json_type'), ('[]', 'json_type'), + ('True', 'json')] # no JSON at all + for i, (bad, why) in enumerate(not_bools): + body = '{"id": %d, "n": 1, "u": 1, "t": "t", "ok": %s, "at": {"x": 1, "label": ""}}' % (231 + i, bad) + assert refused_as(a, '/inputs/put', body.encode(), 'application/json') == (400, why), body no_utf8 = [b'\xff', b'\xc3', b'\xed\xa0\x80', b'\xc0\xaf', b'\xf4\x90\x80\x80'] # a surrogate, too long, past U+10FFFF for i, bad in enumerate(no_utf8): - body = b'{"id": %d, "n": 1, "u": 1, "t": "' % (215 + i) + bad + b'", "at": {"x": 1, "label": ""}}' + body = b'{"id": %d, "n": 1, "u": 1, "t": "' % (215 + i) + bad + b'", "ok": true, "at": {"x": 1, "label": ""}}' assert send(a, '/inputs/put', body, 'application/json') == 400, body bad_query = [('1e999', '1'), ('inf', '1'), ('nan', '1'), ('0x10', '1'), ('', '1'), ('1', '4294967296'), ('1', '-1'), ('1', '1.5'), ('1', '1e3'), ('1', '%2D0'), ('1', '%201'), ('1', '%zz')] for i, (n, u) in enumerate(bad_query): - assert send(a, f'/inputs/put_plain?id={301 + i}&n={n}&u={u}', b't', 'text/plain') == 400, (n, u) + assert send(a, f'/inputs/put_plain?id={301 + i}&n={n}&u={u}&ok=true', b't', 'text/plain') == 400, (n, u) + for i, bad in enumerate(['1', '0', 'True', 'TRUE', '%74rue%20', '']): + assert refused_as(a, f'/inputs/put_plain?id={331 + i}&n=1&u=1&ok={bad}', b't', 'text/plain') == (400, 'query'), bad for i, bad in enumerate(no_utf8 + [b'ok \xc3']): - assert send(a, f'/inputs/put_plain?id={401 + i}&n=1&u=1', bad, 'text/plain') == 422, bad + assert send(a, f'/inputs/put_plain?id={401 + i}&n=1&u=1&ok=false', bad, 'text/plain') == 422, bad # Rows go whole or not at all: one number past the finite refuses them all; a u that a row holds (a # number) but a u32 cannot fails the event where it is sent, and none is kept. assert send(a, '/inputs/put_rows', b'{"parts": [{"id": 601, "n": 1, "u": 1, "t": "a"}, ' b'{"id": 602, "n": 1e999, "u": 1, "t": "b"}]}', 'application/json') == 400 assert send(a, '/inputs/put_rows', b'{"parts": [{"id": 603, "n": 1, "u": 1, "t": "a"}, ' b'{"id": 604, "n": 1, "u": 1.5, "t": "b"}]}', 'application/json') != 200 - refused = len(bad_json) + len(no_utf8) + len(bad_query) + len(no_utf8) + 1 + 2 + assert refused_as(a, '/inputs/put_rows', b'{"parts": [{"id": 605, "ok": true}, {"id": 606, "ok": 1}]}', + 'application/json') == (400, 'json_type') + refused = len(bad_json) + len(not_bools) + len(no_utf8) + len(bad_query) + 6 + len(no_utf8) + 1 + 2 + 1 # A row to change often on A alone, until a checkpoint writes its list anew; it ends as it began. - assert send(a, '/inputs/put', json.dumps({'id': 900, 'n': 9, 'u': 9, 't': 'filler', + assert send(a, '/inputs/put', json.dumps({'id': 900, 'n': 9, 'u': 9, 't': 'filler', 'ok': True, 'at': {'x': 9, 'label': 'filler'}}).encode(), 'application/json') == 200 - expected[900] = (9.0, 9, 'filler', 9.0, 'filler') + expected[900] = (9.0, 9, 'filler', 9.0, 'filler', True) exits = {'a': rows('a')} deadline = time.time() + 30 while time.time() < deadline and (len(rows('b')) < len(expected) or len(rows('c')) < len(expected)): |
