aboutsummaryrefslogtreecommitdiff
path: root/test/codes
diff options
context:
space:
mode:
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)