Files
kokkoja-rel/tests/test_qr_code.py
T
ackandClaude Fable 5.1 89b4a41b2a Character codes in typed text; the site and relay at app.kokkoja.com
With the Text tool, Klammertext's codes give letters a keyboard lacks:
^o" is ö, ^s^ ß, ^a1 ā, ^130ED^ a code point of 4 to 6 digits,
^9053 53EF^ a sequence, ^^ a caret, replaced as each code is completed.
The public site and relay moved to app.kokkoja.com; the old name
redirects.  The Markdown documentation is withdrawn from the release
while it is converted to Klammertext; the README's links to it are
stale until then.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 20:40:09 +02:00

86 lines
3.4 KiB
Python

"""The client's QR encoder against Nayuki's reference encoder (qrcodegen).
Every version from 1 to 40 at every level, and the invitation link's
shape, must give the same modules, mask choice included. Needs node
(the type checker's) and qrcodegen (tests/requirements.txt). Two other
encoders were tried as the reference: python-qrcode agrees with ours at
a given mask but chooses masks by its own reading of the penalty rules,
and segno's modules differ even at the same mask.
"""
import json
import os
import subprocess
import unittest
from qrcodegen import QrCode, QrSegment
CLIENT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "client", "js")
LEVELS = {"L": QrCode.Ecc.LOW, "M": QrCode.Ecc.MEDIUM, "Q": QrCode.Ecc.QUARTILE, "H": QrCode.Ecc.HIGH}
HARNESS = """
import { qrModules } from %s;
const texts = JSON.parse(await new Promise((resolve) => {
let data = '';
process.stdin.on('data', (chunk) => { data += chunk; });
process.stdin.on('end', () => resolve(data));
}));
console.log(JSON.stringify(texts.map(([text, level]) =>
qrModules(text, level).map((row) => row.map((dark) => dark ? 1 : 0)))));
"""
def encode_in_client(texts):
"""[(text, level)] -> list of module matrices (lists of 0/1 rows)."""
harness = HARNESS % json.dumps(os.path.join(CLIENT, "qr_code.js"))
result = subprocess.run(["node", "--input-type=module", "-e", harness],
input=json.dumps(texts), capture_output=True, text=True, check=True)
return json.loads(result.stdout)
def reference(data, level):
"""Nayuki's code for those bytes in byte mode: (version, mask, matrix)."""
code = QrCode.encode_segments([QrSegment.make_bytes(data)], LEVELS[level], boostecl=False)
size = code.get_size()
matrix = [[1 if code.get_module(x, y) else 0 for x in range(size)] for y in range(size)]
return code.get_version(), code.get_mask(), matrix
def longest_text(version, level):
"""The longest text of that version at that level in byte mode."""
bits = QrCode._get_num_data_codewords(version, LEVELS[level]) * 8
length = (bits - 4 - (8 if version < 10 else 16)) // 8
assert reference(b"x" * length, level)[0] == version
return "".join(chr(33 + (i * 7) % 90) for i in range(length))
class QrCodeTest(unittest.TestCase):
def check(self, texts):
matrices = encode_in_client(texts)
for (text, level), matrix in zip(texts, matrices):
version, mask, expected = reference(text.encode("utf-8"), level)
with self.subTest(level=level, length=len(text.encode("utf-8")), version=version, mask=mask):
self.assertEqual(matrix, expected)
def test_invitation_link(self):
link = ("https://app.kokkoja.com/#join=wss://app.kokkoja.com/relay%23"
"AAAAAAAAAAAAAAAAAAAAAA.BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
self.check([(link, "M"), ("", "M"), ("Kokkoja", "M"), ("Mahendra & Artha", "L"),
("Übung 1: äöü", "M")])
def test_every_version(self):
texts = []
for level in "LMQH":
for version in range(1, 41):
# The longest text of that version, so that the block
# structure is filled, and a short one with much padding.
full = longest_text(version, level)
texts.append((full, level))
texts.append((full[: len(full) // 2 + 1], level))
self.check(texts)
if __name__ == "__main__":
unittest.main()