// Author: cirakg & claude ai // Summary: Tests for the QR encoder: segno reference matrices (forced and automatic version and mask), packed mask scores against a per-module reference, 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); } } // The mask penalty computed module by module, the way the encoder did // before M8 packed its lines: the reference for the packed scorer. It // scores a finished symbol (forced mask) with its format and version areas // made light again, as the encoder scores them. (:test) module QrReference { function penalty(code as Qr.MatrixCode) as Number { var n = code.size; var m = Bytes.copy(code.modules); for (var i = 0; i <= 8; i++) { if (i != 6) { m[i * n + 8] = 0; m[8 * n + i] = 0; } } for (var i = 0; i < 8; i++) { m[8 * n + n - 1 - i] = 0; m[(n - 1 - i) * n + 8] = 0; } m[(n - 8) * n + 8] = 0; if (code.version >= 7) { for (var i = 0; i < 18; i++) { var a = n - 11 + i % 3; var b = i / 3; m[b * n + a] = 0; m[a * n + b] = 0; } } var score = 0; var dark = 0; for (var y = 0; y < n; y++) { var row = m.slice(y * n, y * n + n); score += line(row); for (var x = 0; x < n; x++) { dark += row[x]; if (y > 0 && x > 0 && row[x] == row[x - 1] && row[x] == m[(y - 1) * n + x] && row[x] == m[(y - 1) * n + x - 1]) { score += 3; } } } for (var x = 0; x < n; x++) { var column = new [n]b; for (var y = 0; y < n; y++) { column[y] = m[y * n + x]; } score += line(column); } var total = n * n; return score + 10 * ((dark * 100 - 50 * total).abs() / (5 * total)); } function line(l as ByteArray) as Number { var n = l.size(); var score = 0; var run = 1; var window = l[0]; var nextAllowed = 0; for (var k = 1; k < n; k++) { var c = l[k]; if (c == l[k - 1]) { run++; } else { if (run >= 5) { score += run - 2; } run = 1; } window = ((window << 1) | c) & 0x7F; if (window == 0x5D && k >= 6) { var idx = k - 6; if (idx >= nextAllowed) { var after = idx + 7; if (idx == 0 || idx == n - 7 || light(l, idx - 4 < 0 ? 0 : idx - 4, idx) || light(l, after, after + 4 > n ? n : after + 4)) { score += 40; nextAllowed = idx + 7; } else { nextAllowed = idx + 4; } } } } return run >= 5 ? score + run - 2 : score; } function light(l as ByteArray, from as Number, to as Number) as Boolean { for (var i = from; i < to; i++) { if (l[i] != 0) { return false; } } return true; } } // The packed scorer gives every mask exactly the per-module penalty, for // small and large versions, every level, and versions with version areas. (:test) function qrPackedScoresMatchReference(logger as Test.Logger) as Boolean { var cases = [["https://example.com/member/12345", 1, 0], ["HELLO WORLD 1234567890", 3, 0], ["Ünïcödé · 2026", 0, 7], ["a", 2, 0], ["Version ten payload", 1, 10], ["0123456789", 0, 21]]; for (var c = 0; c < cases.size(); c++) { var row = cases[c] as Array; var job = new Qr.QrJob(row[0] as String, row[1] as Number, row[2] as Number, -1); Jobs.runToCompletion(job, 1); if (job.scores.size() != 8) { logger.error("case " + c + ": " + job.scores.size() + " scores"); return false; } for (var mask = 0; mask < 8; mask++) { var forced = Qr.encode(row[0] as String, row[1] as Number, (job.result as Qr.MatrixCode).version, mask); var expected = QrReference.penalty(forced as Qr.MatrixCode); if (job.scores[mask] != expected) { logger.error("case " + c + " mask " + mask + ": packed " + job.scores[mask] + ", reference " + expected); return false; } } } return true; } // 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; }