// Author: cirakg & claude ai // Summary: Tests for the QR encoder: segno reference matrices (forced and automatic version and mask), decoding dumps, capacity, timing. import Toybox.Lang; import Toybox.System; import Toybox.Test; (:test) module QrFixtures { // Row-major modules packed 8 per byte, as hex (the CodeVectors format). function packedHex(code as Qr.MatrixCode) as String { var m = code.modules; var out = new [(m.size() + 7) / 8]b; for (var i = 0; i < m.size(); i++) { if (m[i] == 1) { out[i / 8] = out[i / 8] | (0x80 >> (i % 8)); } } return Bytes.toHex(out); } } // README 12.4 check 1 for QR: identical to segno with forced version and // mask, and the same automatic version and mask choice. (:test) function qrMatchesSegno(logger as Test.Logger) as Boolean { var rows = CodeVectors.QR; for (var i = 0; i < rows.size(); i++) { var row = rows[i] as Array; var payload = row[0] as String; var result = Qr.encode(payload, row[1] as Number, row[2] as Number, row[3] as Number); if (result instanceof Codes.CodeError) { logger.error("row " + i + ": " + result.message); return false; } var expectedVersion = row[4] as Number; var expectedMask = row[5] as Number; if (result.version != expectedVersion || result.mask != expectedMask) { logger.error("row " + i + " '" + Text.clip(payload, 20) + "': version " + result.version + " mask " + result.mask + ", segno " + expectedVersion + " / " + expectedMask); return false; } var got = QrFixtures.packedHex(result); var expected = row[6] as String; if (!got.equals(expected)) { logger.error("row " + i + " '" + Text.clip(payload, 20) + "' (version " + result.version + ") matrix differs"); logger.debug("QRGOT " + got); return false; } } logger.debug("CODES qr reference rows=" + rows.size()); return true; } // README 12.4 check 2: mixed-mode (segmented), Unicode and long payloads, // decoded by tools/check_codes.py with zxing-cpp. (:test) function qrDumpForDecoding(logger as Test.Logger) as Boolean { var payloads = [ "ABC12345678901234xyz", "Membership 0042-7731-55 · Zürich", "12345678901234567890HELLO WORLD hello world", "WIFI:T:WPA;S:FakeNet;P:not-a-real-password;;", "😀 emoji and Ünïcödé", "a" ]; var seed = 777; for (var i = 0; i < 24; i++) { var text = ""; var length = 1 + i * 13 % 90; for (var k = 0; k < length; k++) { seed = (seed * 1103515245 + 12345) & 0x7FFFFFFF; var pool = (seed >> 16) % 3 == 0 ? "0123456789" : ((seed >> 16) % 3 == 1 ? "ABCXYZ $%*+-./:" : "abcxyz,!?é"); var at = (seed >> 4) % pool.length(); text += pool.substring(at, at + 1); } payloads.add(text); } for (var i = 0; i < payloads.size(); i++) { var text = payloads[i] as String; var result = Qr.encode(text, i % 4, 0, -1); if (result instanceof Codes.CodeError) { logger.error("'" + text + "': " + result.message); return false; } logger.debug("QRDUMP " + result.size + " " + Bytes.toHex(Bytes.fromUtf8(text)) + " " + QrFixtures.packedHex(result)); } return true; } (:test) function qrReportsCapacity(logger as Test.Logger) as Boolean { var big = ""; for (var i = 0; i < 300; i++) { big += "abcdefghij"; } var tooLong = Qr.encode(big, 3, 0, -1); var forced = Qr.encode("hello world, too long for version 1", 3, 1, -1); var empty = Qr.encode("", 1, 0, -1); return tooLong instanceof Codes.CodeError && (tooLong.message.find("too long") != null) && forced instanceof Codes.CodeError && empty instanceof Codes.CodeError; } // Timing and step counts for PERFORMANCE.md (simulator). (:test) function qrBenchmark(logger as Test.Logger) as Boolean { var text = ""; for (var i = 0; i < 20; i++) { text += "abcdefghij"; } var inputs = ["https://example.com/member/12345", text, text + text + text + text + text]; for (var i = 0; i < inputs.size(); i++) { var job = new Qr.QrJob(inputs[i] as String, 1, 0, -1); var steps = 0; var start = System.getTimer(); while (!job.step(1)) { steps++; } var code = job.result as Qr.MatrixCode; logger.debug("BENCH qr_v" + code.version + " ms=" + (System.getTimer() - start) + " steps=" + steps); } return true; }