aboutsummaryrefslogtreecommitdiff
path: root/test/codes
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-21 03:55:01 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-21 03:55:01 +0200
commit88f1cac4fa530fc30fd6ef01970af11c7a959b8e (patch)
tree4e2e52b36a6faa2dd50d3b6b07fbfe46fb5dc48c /test/codes
parenta69e23fb5266dd942d125239e7dcbe8a7c8ab956 (diff)
fix(codes): bound every 2D encoding step under the watchdog; draw large codes from runs
M6's step sizes were estimates, only exercised with small codes. With the largest inputs the app accepts, every 2D encoder tripped the watchdog on the Forerunner 255S (and the biggest steps were far over the 768 KB watches' limit), so a large code added from the phone would have stopped the app during settings processing at every start. - QR: resumable segmentation, Reed-Solomon, interleaving, function patterns and final mask; mask scoring on rows and columns packed 16 modules per word (same penalties, checked against the per-module rules). - Reed-Solomon: resumable field tables and generator, encoder shifting with native slice. - Data Matrix, Aztec, PDF417: resumable encodation, stuffing, compaction, placement and build; Data Matrix's grid as a byte array (90 KB -> 35 KB); PDF417 refuses text that can't fit the screen before compacting. - Cache format 2 with 16-bit sizes (wide PDF417 was cached wrong); packing in steps; codes drawn as runs, and from a bitmap painted in steps when a frame can't draw all runs. - The job runner does two steps per tick on 768 KB watches; gen_products --check verifies each product's watchdogCount fits that. - CodeView renews full brightness (AMOLED) or the backlight (MIP) for up to a minute from its single ticker; a tap restarts the minute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'test/codes')
-rw-r--r--test/codes/CodeCacheTest.mc58
-rw-r--r--test/codes/QrTest.mc124
2 files changed, 179 insertions, 3 deletions
diff --git a/test/codes/CodeCacheTest.mc b/test/codes/CodeCacheTest.mc
index 812acc9..2ad7f05 100644
--- a/test/codes/CodeCacheTest.mc
+++ b/test/codes/CodeCacheTest.mc
@@ -1,5 +1,5 @@
// Author: cirakg & claude ai
-// Summary: Tests for the encrypted barcode matrix cache: round trip, keys of locked entries, invalidation and tampering.
+// Summary: Tests for the encrypted barcode matrix cache: round trip, packed format, runs, keys of locked entries, invalidation and tampering.
import Toybox.Lang;
import Toybox.Test;
@@ -53,6 +53,60 @@ function codeCacheOfLockedEntriesNeedsTheSession(logger as Test.Logger) as Boole
pdf.rowHeight = 3;
var round = Codes.Matrix.fromBytes(pdf.toBytes());
return code == VaultError.LOCKED && back != null && Bytes.equal(back, [3, 1, 4, 1, 0xE0]b)
- && Codes.Matrix.fromBytes([3, 1, 4, 1, 0xE0]b) != null && Codes.Matrix.fromBytes([3, 1, 4]b) == null
+ && Codes.Matrix.fromBytes([2, 0, 3, 0, 1, 4, 1, 0xE0]b) != null && Codes.Matrix.fromBytes([2, 0, 3, 0, 1, 4]b) == null
&& round != null && round.rowHeight == 3 && Bytes.equal(round.modules, [1, 0, 1, 0, 1, 1]b);
}
+
+// Format 2 holds widths over 255 (wide PDF417); a 0.7 cache (one width
+// byte) is refused, so the code is encoded again.
+(:test)
+function codeCachePackedFormat(logger as Test.Logger) as Boolean {
+ var bits = new [300 * 2]b;
+ for (var i = 0; i < bits.size(); i++) {
+ bits[i] = (i * 7 / 3) % 2;
+ }
+ var wide = new Codes.Matrix(300, 2, 2, bits);
+ var bytes = wide.toBytes();
+ var back = Codes.Matrix.fromBytes(bytes);
+ var oldFormat = Codes.Packed.parse([3, 1, 4, 1, 0xE0]b);
+ var wrongSize = Codes.Packed.parse(bytes.slice(0, bytes.size() - 1));
+ return bytes[0] == 2 && back != null && back.width == 300 && back.height == 2 && Bytes.equal(back.modules, bits)
+ && oldFormat == null && wrongSize == null && Codes.Packed.parse(null) == null;
+}
+
+// The runs drawn on screen are exactly the dark modules, row by row,
+// including runs across byte boundaries and whole dark or light bytes.
+(:test)
+function codeRunsCoverExactlyTheDarkModules(logger as Test.Logger) as Boolean {
+ var codes = [Qr.encode("runs, runs, runs", 1, 0, -1) as Qr.MatrixCode];
+ var matrices = [new Codes.Matrix(codes[0].size, codes[0].size, 4, codes[0].modules)] as Array<Codes.Matrix>;
+ var stripes = new [37 * 5]b;
+ for (var i = 0; i < stripes.size(); i++) {
+ stripes[i] = (i % 37) < 20 || (i % 37) > 33 ? 1 : 0;
+ }
+ matrices.add(new Codes.Matrix(37, 5, 1, stripes));
+ for (var c = 0; c < matrices.size(); c++) {
+ var m = matrices[c];
+ var finder = new Codes.RunFinder(Codes.Packed.parse(m.toBytes()) as Codes.Packed);
+ Jobs.runToCompletion(finder, 1);
+ var drawn = new [m.width * m.height]b;
+ var runs = finder.result.runs;
+ for (var i = 0; i < runs.size(); i++) {
+ var r = runs[i];
+ var y = r >> 20;
+ var x = (r >> 10) & 0x3FF;
+ for (var k = 0; k < (r & 0x3FF); k++) {
+ if (drawn[y * m.width + x + k] == 1) {
+ logger.error("matrix " + c + ": run " + i + " overlaps");
+ return false;
+ }
+ drawn[y * m.width + x + k] = 1;
+ }
+ }
+ if (!Bytes.equal(drawn, m.modules)) {
+ logger.error("matrix " + c + ": runs differ from the modules");
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/test/codes/QrTest.mc b/test/codes/QrTest.mc
index 50cb35a..2f81887 100644
--- a/test/codes/QrTest.mc
+++ b/test/codes/QrTest.mc
@@ -1,5 +1,5 @@
// Author: cirakg & claude ai
-// Summary: Tests for the QR encoder: segno reference matrices (forced and automatic version and mask), decoding dumps, capacity, timing.
+// 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;
@@ -21,6 +21,128 @@ module QrFixtures {
}
}
+// 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)