From fa9c695256de1c7fe007ba5d6bb9b28b1deb5a5a Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 20 Oct 2026 02:43:08 +0200 Subject: test(codes): compare with zxing-cpp reference rows and decode encoder output and screenshots Co-Authored-By: Claude Opus 5.5 --- Makefile | 5 +- docker/scripts/run-unit-tests.sh | 5 +- test/codes/LinearTest.mc | 146 ++++++++++++++++++++ test/data/CodeVectors.mc | 230 +++++++++++++++++++++++++++++++ test/golden/fenix7/code.png | Bin 0 -> 3294 bytes test/golden/fr255s/code.png | Bin 0 -> 2267 bytes test/golden/fr70/code.png | Bin 0 -> 13889 bytes test/golden/fr965/code.png | Bin 0 -> 12507 bytes test/golden/instinct3amoled45mm/code.png | Bin 0 -> 11749 bytes test/golden/venusq2/code.png | Bin 0 -> 3037 bytes test/golden/venux1/code.png | Bin 0 -> 11473 bytes tools/barcode_ref.py | 97 +++++++++++++ tools/check_codes.py | 60 ++++++++ tools/gen_code_vectors.py | 124 +++++++++++++++++ tools/tests/test_barcode_tools.py | 65 +++++++++ tools/ui_flow.py | 29 +++- 16 files changed, 757 insertions(+), 4 deletions(-) create mode 100644 test/codes/LinearTest.mc create mode 100644 test/data/CodeVectors.mc create mode 100644 test/golden/fenix7/code.png create mode 100644 test/golden/fr255s/code.png create mode 100644 test/golden/fr70/code.png create mode 100644 test/golden/fr965/code.png create mode 100644 test/golden/instinct3amoled45mm/code.png create mode 100644 test/golden/venusq2/code.png create mode 100644 test/golden/venux1/code.png create mode 100644 tools/barcode_ref.py create mode 100644 tools/check_codes.py create mode 100644 tools/gen_code_vectors.py create mode 100644 tools/tests/test_barcode_tools.py diff --git a/Makefile b/Makefile index 1f2fdba..61a2b46 100644 --- a/Makefile +++ b/Makefile @@ -72,7 +72,7 @@ DOCS_RUN = $(DOCKER) run --rm --init --label project=$(PROJECT) \ --read-only --tmpfs /tmp:rw,exec,nosuid,size=1g --shm-size 512m \ -v "$(ROOT)":/work -w /work --network none $(DOCS_IMAGE) -.PHONY: help image docs-image sdk key build test test-tools sim screenshots screenshots-approve perf memtest icons vectors corpus spike spike-sim \ +.PHONY: help image docs-image sdk key build test test-tools sim screenshots screenshots-approve perf memtest icons vectors corpus codes spike spike-sim \ lint package docs ci shell clean nuke lock products \ check-docker dirs need-image need-docs-image @@ -139,6 +139,9 @@ vectors: dirs need-image ## Regenerate test/data/Vectors.mc (cross-checked RFC/N corpus: dirs need-image ## Regenerate test/data/FuzzCorpus.mc (seeded fuzz inputs for the import parsers) $(DEV_OFFLINE) python3 tools/gen_fuzz_corpus.py +codes: dirs need-image ## Regenerate test/data/CodeVectors.mc (zxing-cpp reference barcodes) + $(DEV_OFFLINE) python3 tools/gen_code_vectors.py + spike: dirs need-image ## Build the M1 settings spike (debug .prg + beta .iq) into build/spike $(DEV_OFFLINE) build-spike.sh build diff --git a/docker/scripts/run-unit-tests.sh b/docker/scripts/run-unit-tests.sh index 60a8955..a935db3 100755 --- a/docker/scripts/run-unit-tests.sh +++ b/docker/scripts/run-unit-tests.sh @@ -8,7 +8,8 @@ # Every test build is compiled first, so a compile error stops the run before # the simulator starts. One simulator then runs every device in turn. # monkeydo exits with 1 even when all tests pass, so the verdict comes from -# tools/ciq_test_report.py; the script fails if any device fails. +# tools/ciq_test_report.py, and the barcodes the tests dumped are decoded +# with zxing-cpp (tools/check_codes.py); the script fails if any device fails. set -euo pipefail # shellcheck source=ciq-env.sh source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh" @@ -38,6 +39,8 @@ for device in "$@"; do >"${results}/${device}.log" 2>&1 || true python3 /work/tools/ciq_test_report.py "${results}/${device}.log" \ --device "${device}" --junit "${results}/${device}.xml" || failed=1 + # README 12.4 check 2: the barcodes the encoders produced must decode. + python3 /work/tools/check_codes.py "${results}/${device}.log" || failed=1 done if ((failed)); then diff --git a/test/codes/LinearTest.mc b/test/codes/LinearTest.mc new file mode 100644 index 0000000..ed42ccf --- /dev/null +++ b/test/codes/LinearTest.mc @@ -0,0 +1,146 @@ +// Author: cirakg & claude ai +// Summary: Tests for the linear barcode encoders: zxing-cpp reference rows, validation messages, and a dump for decoding. + +import Toybox.Lang; +import Toybox.Test; + +(:test) +module LinearFixtures { + + function moduleText(m as ByteArray) as String { + var s = ""; + for (var i = 0; i < m.size(); i++) { + s += m[i] == 1 ? "1" : "0"; + } + return s; + } + + // The error message for payload, or "" when it encodes. + function errorOf(symbology as Number, payload as String, options as Number) as String { + var r = Codes.encode(symbology, payload, options); + return r instanceof Codes.CodeError ? r.message : ""; + } + + function opts(check as Number) as Number { + return Codes.OPT_SHOW_TEXT | (check << Codes.OPT_CHECK_SHIFT) | 1; + } +} + +// README 12.4 check 1: identical modules to zxing-cpp for every row. +(:test) +function linearCodesMatchTheReference(logger as Test.Logger) as Boolean { + var rows = CodeVectors.LINEAR; + for (var i = 0; i < rows.size(); i++) { + var row = rows[i] as Array; + var symbology = row[0] as Number; + var payload = row[1] as String; + var result = Codes.encode(symbology, payload, row[2] as Number); + if (result instanceof Codes.CodeError) { + logger.error("row " + i + " (" + Codes.name(symbology) + " '" + payload + "'): " + result.message); + return false; + } + var got = LinearFixtures.moduleText(result.modules); + var expected = row[3] as String; + if (symbology == Codes.CODE128) { + // zxing-cpp 3 writes Code 128 through zint, whose optimal encoder + // breaks ties between equally short encodings differently (for + // example set C for a final digit pair). Both are optimal: the + // length must match, and the code is decoded by check_codes.py. + logger.debug("CODEDUMP " + symbology + " " + Bytes.toHex(Bytes.fromUtf8(result.text)) + " " + got); + if (got.length() != expected.length()) { + logger.error("row " + i + " (Code 128 '" + payload + "') has " + got.length() + " modules, zxing-cpp " + + expected.length()); + return false; + } + continue; + } + if (!got.equals(expected)) { + logger.error("row " + i + " (" + Codes.name(symbology) + " '" + payload + "') differs:\n got " + got + "\n exp " + expected); + return false; + } + } + logger.debug("CODES reference rows=" + rows.size()); + return true; +} + +(:test) +function linearCheckDigitModes(logger as Test.Logger) as Boolean { + var add = LinearFixtures.opts(Codes.CHECK_ADD_VERIFY); + var fix = LinearFixtures.opts(Codes.CHECK_FIX); + var off = LinearFixtures.opts(Codes.CHECK_OFF); + var added = Codes.encode(Codes.EAN13, "590123412345", add) as Codes.LinearCode; + var fixed = Codes.encode(Codes.EAN13, "5901234123450", fix) as Codes.LinearCode; + var kept = Codes.encode(Codes.EAN13, "5901234123450", off) as Codes.LinearCode; + var wrong = LinearFixtures.errorOf(Codes.EAN13, "5901234123450", add); + var offShort = LinearFixtures.errorOf(Codes.EAN13, "590123412345", off); + return added.text.equals("5901234123457") && added.note != null + && fixed.text.equals("5901234123457") && (fixed.note as String).find("was 0") != null + && kept.text.equals("5901234123450") + && wrong.find("check digit should be 7, got 0") != null && wrong.find("Fix if wrong") != null + && offShort.find("needs all 13 digits") != null; +} + +(:test) +function linearValidationMessages(logger as Test.Logger) as Boolean { + var o = LinearFixtures.opts(Codes.CHECK_ADD_VERIFY); + var off = LinearFixtures.opts(Codes.CHECK_OFF); + var cases = [ + [Codes.EAN13, "59012341234a", o, "digits only"], + [Codes.EAN13, "123", o, "needs 12 or 13 digits"], + [Codes.UPCA, "1234", o, "UPC-A needs 11 or 12"], + [Codes.EAN8, "12345678", o, "check digit should be"], + [Codes.UPCE, "2123456", o, "number system must be 0 or 1"], + [Codes.UPCE, "0502553", o, "not a valid zero suppression"], + [Codes.UPCE, "12345", o, "6, 7 or 8 digits"], + [Codes.ITF, "123", off, "even number of digits"], + [Codes.ITF, "1234", o, "odd number of digits"], + [Codes.CODE39, "abc", o, "isn't allowed"], + [Codes.CODE128, "Ünï", o, "printable ASCII"], + [Codes.CODE128, "", o, "needs some text"], + [Codes.CODABAR, "A123", o, "both ends or at neither"], + [Codes.CODABAR, "A1B2A", o, "isn't allowed there"], + [Codes.QR, "hello", o, "not supported yet"] + ]; + for (var i = 0; i < cases.size(); i++) { + var c = cases[i] as Array; + var message = LinearFixtures.errorOf(c[0] as Number, c[1] as String, c[2] as Number); + if (message.find(c[3] as String) == null) { + logger.error("case " + i + ": got '" + message + "'"); + return false; + } + } + var codabar = Codes.encode(Codes.CODABAR, "40156", o) as Codes.LinearCode; + return codabar.text.equals("A40156A") && codabar.note != null; +} + +// README 12.4 check 2: dumps encoder outputs for seeded random payloads; +// tools/check_codes.py decodes them with zxing-cpp after the test run. +(:test) +function linearDumpForDecoding(logger as Test.Logger) as Boolean { + var seed = 12345; + var count = 0; + var symbologies = [Codes.EAN13, Codes.EAN8, Codes.UPCA, Codes.UPCE, Codes.CODE128, Codes.CODE39, Codes.ITF, Codes.CODABAR]; + var o = LinearFixtures.opts(Codes.CHECK_ADD_VERIFY); + for (var i = 0; i < 160; i++) { + var symbology = symbologies[i % symbologies.size()] as Number; + var length = [12, 7, 11, 7, 10, 8, 10, 8][i % symbologies.size()] as Number; + var alphabet = ["0123456789", "0123456789", "0123456789", "0123456789", + "0123456789 AZaz!#$%&()*+,-./:;<=>?@", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%", "0123456789", "0123456789-$:/.+"][i % symbologies.size()] as String; + var text = ""; + for (var k = 0; k < length; k++) { + seed = (seed * 1103515245 + 12345) & 0x7FFFFFFF; + var at = (seed >> 8) % alphabet.length(); + text += alphabet.substring(at, at + 1); + } + if (symbology == Codes.UPCE) { + text = "0" + text.substring(1, 7); + } + var result = Codes.encode(symbology, text, o); + if (result instanceof Codes.CodeError) { + continue; // invalid random UPC-E suppressions are expected + } + logger.debug("CODEDUMP " + symbology + " " + Bytes.toHex(Bytes.fromUtf8(result.text)) + " " + LinearFixtures.moduleText(result.modules)); + count++; + } + return count > 120; +} diff --git a/test/data/CodeVectors.mc b/test/data/CodeVectors.mc new file mode 100644 index 0000000..64f8644 --- /dev/null +++ b/test/data/CodeVectors.mc @@ -0,0 +1,230 @@ +// Author: cirakg & claude ai +// Summary: zxing-cpp reference module rows for the linear barcode encoders (generated; do not edit). +// +// Generated by tools/gen_code_vectors.py, seed 1212. Rows: [symbology, payload, options, +// expected modules without quiet zones]. Fake payloads only. + +import Toybox.Lang; + +(:test) +module CodeVectors { + const LINEAR = [ + [3, "590123412345", 17, "10100010110100111011001100100110111101001110101010110011011011001000010101110010011101000100101"], + [3, "5901234123457", 17, "10100010110100111011001100100110111101001110101010110011011011001000010101110010011101000100101"], + [4, "1234567", 17, "1010011001001001101111010100011010101001110101000010001001110010101"], + [5, "01234567890", 17, "10100011010011001001001101111010100011011000101010101000010001001001000111010011100101001110101"], + [6, "0425261", 17, "101001110100100110111001001101101011110011001010101"], + [6, "1234565", 17, "101001001101000010011101011100101011110110001010101"], + [1, "1234", 17, "110100111001011001110010001011000100100111101100011101011"], + [1, "123456789", 17, "11010011100101100111001000101100011100010110110000101001011110111011100101100100011110101100011101011"], + [1, "AB12345678", 17, "1101001000010100011000100010110001011101111010110011100100010110001110001011011000010100111011010001100011101011"], + [1, "A1234B12345", 17, "1101001000010100011000101110111101011001110010001011000101111011101000101100010011100110101110111101110110111010111011000111010111101100011101011"], + [1, " ~", 17, "110100100001101100110010001011110111101001001100011101011"], + [2, "CODE 39", 25, "10010110110101101101001010110101101001010101100101101101011001010100110101101011011001010101011001011010100101101101"], + [2, "CODE-39", 17, "100101101101011011010010101101011010010101011001011011010110010101001010110110110110010101010110010110101011011010010100101101101"], + [7, "1234", 25, "101011101000101011100011101110100010100011101"], + [7, "123", 17, "101011101000101011100011101110001000101011101"], + [8, "A40156B", 17, "101100100101011010010101010011010101100101101010010100101011010010010110"], + [8, "31117013206375", 17, "101100100101100101010101011001010101100101010110010100101101010101001101010110010110010101010100101101010100110100101011011001010101001011010110101001010110010010"], + [3, "449628825640", 17, "10101000110010111010111100100110001001000100101010110110010011101010000101110011100101110010101"], + [4, "0695689", 17, "1010001101010111100010110110001010101010000100100011101001110100101"], + [5, "16055073910", 17, "10100110010101111000110101100010110001000110101010100010010000101110100110011011100101110100101"], + [6, "0786278", 17, "101001000101101110101111001001100100010001001010101"], + [1, "c3wd.2k069S5cU/86_th", 17, "110100100001000010110011001011100111100101001000010011010011001110110011100101100001001010011101100110011101001110010110011011101000110111001001000010110011011101110101110011001110100110011001110100101001100001001111010010011000010110111001001100011101011"], + [2, "9H.4KR-IMOC", 25, "100101101101010110010110101101010011010110010101101010100110101101101010100110110101011001010010101101101011010011010110110101001011010110100101101101001010100101101101"], + [7, "61702865119723", 25, "101010001110111010100010101000111000111010001110101000111010001110111000101011100010101011100010111010111000100010001110001010111011101"], + [8, "C/412A", 17, "101001001101101101011010110100101010110010101001011010110010010"], + [3, "035872054051", 17, "10101111010110001011011101110110010011000110101010100111010111001110010100111011001101101100101"], + [4, "9625582", 17, "1010001011010111100100110110001010101001110100100011011001000100101"], + [5, "49416692285", 17, "10101000110001011010001100110010101111010111101010111010011011001101100100100010011101011100101"], + [6, "1722686", 17, "101011101100110110010011000010100010010101111010101"], + [1, "9E S98s@[Z(r49w", 17, "11010010000111001011001000110100011011001100110111010001110010110011101001100101111001001100011011011100011010111011000101000110010010010011110110010011101110010110011110010100110100011101100011101011"], + [2, "TRO6", 17, "100101101101010101101100101101010110010110101101001010110011010101010011011010100101101101"], + [7, "6797684058", 25, "101010111011101000100010111010111000100010001110111010001010101110001000111011100010111010001011101"], + [8, "A24825B", 17, "101100100101010010110101101001010011010101010010110110101001010010010110"], + [3, "590759572895", 17, "10100010110100111001000101100010001011011100101010100010011011001001000111010010011101110100101"], + [4, "1019853", 17, "1010011001000110100110010001011010101001000100111010000101000100101"], + [5, "94874085816", 17, "10100010110100011011011101110110100011000110101010100100010011101001000110011010100001011100101"], + [6, "0484250", 17, "101001110100010010100011001001101100010100111010101"], + [1, "721P9675", 17, "1101001110010011000010101111011101001110011011101110110101110111101011110001011000010010110111011101100011101011"], + [2, "M/ZG9-P", 25, "10010110110101101101010010100100101001010011011010101010100110110101100101101010010101101101011011010010100101101101"], + [7, "213161", 25, "101010001110101011100011100011101010100010001110111010100011101"], + [8, "A:62C", 17, "10110010010110101101101001010110101001011010100100110"], + [3, "124018767336", 17, "10100100110100011010011100110010001001001000101010101000010001001000010100001010100001101100101"], + [4, "8370591", 17, "1010110111011110101110110001101010101001110111010011001101001110101"], + [5, "86048184512", 17, "10101101110101111000110101000110110111001100101010100100010111001001110110011011011001100110101"], + [6, "1432594", 17, "101010001101111010010011011100100101110011101010101"], + [1, "/8S/~60;95396", 17, "11010010000101110011001110100110011011101000101110011001000101111011001110100100111011001110110010011100101100101110111101101110111010111100010100011010001100011101011"], + [2, " .$2", 25, "10010110110101001101011010110010101101010010010010101011001010110100101101101"], + [7, "8603556071098627", 25, "101011101000100011101010001000111011101011100010111000101010111011100010001010001010111011100010100011101110001011101000100011101010111010100011100011101"], + [8, "A3/03-.589::.B", 17, "101100100101100101010110110101101010100110110010101010100110101101101101011010100101001101010110100101011010110110110101101101101101101010010010110"], + [3, "745070459602", 17, "10101000110111001000110100100010001101001110101010100111011101001010000111001011011001000100101"], + [4, "1035585", 17, "1010011001000110101111010110001010101001110100100010011101001110101"], + [5, "39401147040", 17, "10101111010001011010001100011010011001001100101010101110010001001110010101110011100101000010101"], + [6, "1939274", 17, "101000101101111010010111001101100100010100011010101"], + [1, "8b95FA7584q|1", 17, "1101001000011101001100100100001101110010110011011100100100011000101010001100010111011110110000100101001111010010111101110100101111001010111100010011100110111101000101100011101011"], + [2, "LWK5+", 25, "100101101101010110101001101100110101010110101010011011010011010101001010010010100101101101"], + [7, "4724634851241021", 25, "101010101110100011100010111010001011100010001110001110101010001011101000111011100010111010100010111010001011100011101010001000111010001110101011100011101"], + [8, "D/842$43A", 17, "101001100101101101011010011010101011010010101001011010110010101011010010110010101010110010010"], + [3, "631279390553", 17, "10101111010110011001101100100010001011011110101010111010011100101001110100111010000101001110101"], + [4, "4640106", 17, "1010100011010111101000110001101010101100110111001010100001110100101"], + [5, "36194005869", 17, "10101111010101111001100100010110100011000110101010111001010011101001000101000011101001110100101"], + [6, "1702344", 17, "101011101101001110010011010000101000110011101010101"], + [1, "9", 17, "1101001000011100101100111001001101100011101011"], + [2, "E", 17, "100101101101011010110010101101011001010100101101101"], + [7, "34286455759703", 25, "101011101110100010100010001110101000111010111011100010100011100010111000101010001010001110111010111010111000100010001000111011101011101"], + [8, "C6D", 17, "10100100110100101011010100110010"], + [3, "391662728089", 17, "10100010110011001000010100001010011011011101101010110110010010001110010100100011101001000010101"], + [4, "5936211", 17, "1010110001000101101111010101111010101101100110011011001101100110101"], + [5, "76968977434", 17, "10101110110101111000101101011110110111000101101010100010010001001011100100001010111001101100101"], + [6, "0934703", 17, "101001011101000010100011001000100011010111101010101"], + [1, "0U*85c11", 17, "110100100001001110110011011101110110010001001110100110011011100100100001011001011101111011000100100101111010001100011101011"], + [2, "QK.D/", 17, "1001011011010101010110011011010101001101100101011010101011001011010010010100101010010110110100101101101"], + [7, "501097", 25, "101011101011100010001011101010001000111010111010111000100011101"], + [8, "B51$9:7531787C", 17, "10010010110110101001010101100101011001010110100101011010110110100101101011010100101100101010101011001010010110101001101010100101101010100100110"], + [3, "856746516120", 17, "10101100010000101011101100111010000101011000101010110011010100001100110110110011100101110100101"], + [4, "8578033", 17, "1010110111011000101110110110111010101110010100001010000101110010101"], + [5, "67915446798", 17, "10101011110111011000101100110010110001010001101010101110010100001000100111010010010001010000101"], + [6, "0780039", 17, "101001000101101110001101010011101111010010111010101"], + [1, "686932&712633MB7*1B", 17, "1101001110010000100110101100100001100011011010111101110100110010001011101111010011010000111001001101010001100010111101110101110110001000101100011101101110110010001001001110011010001011000101100111001100011101011"], + [2, "ZECCK+0T", 17, "1001011011010100110110101011010110010101101101001010110110100101011010101001101001010010010101001101101010101101100101100101101010100101101101"], + [7, "108342", 25, "101011101010001000111011100010001011101010100011101011100011101"], + [8, "B-5+746+17A", 17, "100100101101010011010110101001010110110110100101101010110100101001010110101101101101010110010100101101010110010010"], + [3, "434270355934", 17, "10101111010011101001001101110110100111010000101010100111010011101110100100001010111001001110101"], + [4, "6164715", 17, "1010101111001100101011110100011010101000100110011010011101101100101"], + [5, "11306583166", 17, "10100110010011001011110100011010101111011000101010100100010000101100110101000010100001110010101"], + [6, "1042301", 17, "101000110100111010011011011110100011010110011010101"], + [1, "0|[}Kh0158Df;9", 17, "110100100001001110110010101111000111000110101010001111010110001110100110000101011101111011001101100111011000101011110111010110001000101100001001110110010011100101100111101000101100011101011"], + [2, "V8RE. ", 25, "1001011011010100110101011011010010110101101010110010110101100101011001010110101001101011010100101101101"], + [7, "0995829616", 25, "101010100011101110001010001110100011101011101000101110100010111000100011101011101000100010111011101"], + [8, "C7B", 17, "10100100110100101101010010010110"], + [3, "013645629332", 17, "10100110010111101010111101000110110001010111101010110110011101001000010100001011011001001000101"], + [4, "3321111", 17, "1010111101011110100100110011001010101100110110011011001101011100101"], + [5, "72563954209", 17, "10101110110010011011000101011110111101000101101010100111010111001101100111001011101001010000101"], + [6, "0248435", 17, "101001101101000110001001010001101111010111001010101"], + [1, "70i461!C6rX8", 17, "11010011100101100001001011110111010000110100110010011101100111010010011100110110011011001000100011011001110100100100111101110001011011101001100100010011001100011101011"], + [2, "M 3EF", 25, "100101101101011011010100101001101011010110110010101011010110010101011011001010100101101101"], + [7, "952022523820", 25, "101010001110100011101010111010001000111010111000101011100011101000111010100011100011101010001010111010001000111011101"], + [8, "D2/D", 17, "1010011001010100101101101101011010100110010"], + [3, "883500588448", 17, "10101101110100001011000101001110100111011000101010100100010010001011100101110010010001000010101"], + [4, "1230084", 17, "1010011001001001101111010001101010101110010100100010111001010000101"], + [5, "36281639231", 17, "10101111010101111001001101101110011001010111101010100001011101001101100100001011001101101100101"], + [6, "1280395", 17, "101001001100010010001101010000100101110110001010101"], + [1, "y62.r,>40034(64[x86", 17, "1101001000011011011110110011101001100111001010011001110100100111101011001110011011011000101110111101100010100010010011000101111011101100100111010001100100110011101001100100111011100011010111100100101011101111011110100100100010001101100011101011"], + [2, "G", 17, "100101101101010101001101101010100110110100101101101"], + [7, "6355553723020067", 25, "101010001110001110101011100010111000101011100010111000101011101110101000100010001110001010111010100011101110100010101110001110001010111011101000100011101"], + [8, "A4$9$-84+B", 17, "1011001001010110100101011001010110100101010110010101010011010100110101010110100101011011011010010010110"], + [3, "380795213128", 17, "10101101110001101001000100101110111001001001101010110011010000101100110110110010010001100110101"], + [4, "6764839", 17, "1010101111011101101011110100011010101001000100001011101001110100101"], + [5, "57862780506", 17, "10101100010111011011011101011110010011011101101010100100011100101001110111001010100001001000101"], + [6, "1284836", 17, "101001001101101110011101000100101111010000101010101"], + [1, "5BP41M1Q.3", 17, "1101001000011011100100100010110001110111011011001001110100111001101011101100010011100110110100011101001100111011001011100111010110001100011101011"], + [2, "/NF2", 17, "100101101101010010010100101010110100110101101100101010110010101101100101011010100101101101"], + [7, "845963608089", 25, "101011101010001110100011101000111010001010001110001110101010111011100010001011101010001110001011101000101110001011101"], + [8, "D6$88//41C", 17, "10100110010100101011010110010101001101010100110101011011010110110110101101011010010101011001010100100110"], + [3, "762561922265", 17, "10101011110011011011000100001010011001001011101010110110011011001101100101000010011101001110101"], + [4, "9615253", 17, "1010001011010111100110010110001010101101100100111010000101110100101"], + [5, "48264198556", 17, "10101000110110111001001101011110100011001100101010111010010010001001110100111010100001101100101"], + [6, "0286351", 17, "101001101101101110000101011110101110010011001010101"], + [1, "/80nYM", 17, "11010010000101110011001110100110010011101100110000101001110110100010111011000110111000101100011101011"], + [2, "NTZ", 17, "10010110110101010110100110101011011001010011011010101101001010110100101101101"], + [7, "0611286273", 25, "101010100011100011101011100010101011100010001110101000111010111000111010100010001000101110111011101"], + [8, "D24:4++3$:6:7B", 17, "101001100101010010110101101001011010110110101101001010110110110101101101101100101010101100101011010110110100101011011010110110100101101010010010110"], + [3, "867786950060", 17, "10101011110010001011101100010010000101000101101010100111011100101110010101000011100101110010101"], + [4, "4001759", 17, "1010100011000110100011010011001010101000100100111011101001011100101"], + [5, "02736375670", 17, "10100011010010011011101101111010101111011110101010100010010011101010000100010011100101101100101"], + [6, "0945268", 17, "101001011101000110111001001001101011110001001010101"], + [1, "80", 17, "1101001110010100111100100100111101100011101011"], + [2, "B22W-2", 25, "1001011011010101101001011010110010101101011001010110110011010101010010101101101011001010110100101101101"], + [7, "553661", 25, "101011100010111000101011101110001000101010001110111010100011101"], + [8, "B58429+-1$D", 17, "10010010110110101001010011010101011010010101001011011010010101011011011010100110101010110010101100101010100110010"], + [3, "078529030738", 17, "10101110110110111011000100100110001011000110101010100001011100101000100100001010010001110010101"], + [4, "7854460", 17, "1010111011011011101100010100011010101011100101000011100101011100101"], + [5, "04596111625", 17, "10100011010100011011000100010110101111001100101010110011011001101010000110110010011101011100101"], + [6, "1162246", 17, "101001100100001010010011001101100111010101111010101"], + [1, "DEE_55sR4P653910", 17, "110100100001011000100010001101000100011010001010011000011011100100110111001001011110010011000101110110010011101110111011010111011110100101100001101000100011001000100110111001001100011101011"], + [2, "MCQIMZ72VOB", 17, "1001011011010110110101001011011010010101010101100110101101001101011011010100101001101101010101001011011010110010101101001101010110110101101001010110100101101001101011010100101101101"], + [7, "63460187555009", 25, "101010001110001110101010100011100010111010001011101110100011101010111000100011100010111000101011101011100010001010100011101110001011101"], + [8, "C821D", 17, "1010010011010011010101010010110101011001010100110010"], + [3, "690596854636", 17, "10100010110100111011100100101110101111011011101010100111010111001010000100001010100001110100101"], + [4, "2575091", 17, "1010010011011000101110110110001010101110010111010011001101100110101"], + [5, "54909634786", 17, "10101100010100011000101100011010001011010111101010100001010111001000100100100010100001100110101"], + [6, "1301838", 17, "101011110100011010110011000100101111010001001010101"], + [1, "49(824(2_:){-9", 17, "110100111001101000111010111101110100011001001110100110011001110010110010011101000110010011001110010101001100001110010011011001001000111101101101001101110011100101100110011101001100011101011"], + [2, "7S%Z294C", 25, "100101101101010100101101101011010110010101001001001010011011010101011001010110101100101101010100110101101101101001010100101101101"], + [7, "70", 25, "101010101000111000111011101"], + [8, "B:99:9549//51C", 17, "10010010110110101101101101001010110100101011010110110110100101011010100101011010010110100101011011010110110110101101101010010101011001010100100110"], + [3, "789302830511", 17, "10101101110010111011110101001110010011000100101010100001011100101001110110011011001101110100101"], + [4, "4693088", 17, "1010100011010111100010110111101010101110010100100010010001110010101"], + [5, "42174970532", 17, "10101000110010011001100101110110100011000101101010100010011100101001110100001011011001110010101"], + [6, "1704761", 17, "101011101100011010011101001000101011110110011010101"], + [1, "9GE26f^44039V!0&", 17, "1101001000011100101100110100010001000110100011001110010110011101001011000010011110001010101110111101000110111010010011000101111011101110010110011101011000110011011001001110110010011001000101001111001100011101011"], + [2, "M", 25, "10010110110101101101010010100101101101"], + [7, "865176", 25, "101011101000100011101011100010111010100010100010001110111011101"], + [8, "A4D", 17, "10110010010101101001010100110010"], + [3, "548097674005", 17, "10101000110001001010011100010110111011000010101010100010010111001110010111001010011101110100101"], + [4, "6103085", 17, "1010101111001100100011010111101010101110010100100010011101001110101"], + [5, "79872221118", 17, "10101110110001011011011101110110010011001001101010110110011001101100110110011010010001010000101"], + [6, "0299335", 17, "101001101100010110001011010000101000010110001010101"], + [1, "0#~;35y7", 17, "110100100001001110110010010011000100010111101110110010011001011100110111001001101101111011101101110111101011101100011101011"], + [2, "4", 25, "10010110110101010011010110100101101101"], + [7, "331983305458", 25, "101011100011100010101011101000101000111011100010001011101011101110100010001011101011100010100011100010111010001011101"], + [8, "D+8+:3:3A", 17, "101001100101011011011010011010101011011011011010110110110010101011010110110110010101010110010010"], + [3, "557988048975", 17, "10101100010010001001011101101110110111010011101010101110010010001110100100010010011101001110101"], + [4, "3447860", 17, "1010111101010001101000110111011010101001000101000011100101001000101"], + [5, "62094742584", 17, "10101011110010011000110100010110100011011101101010101110011011001001110100100010111001000010101"], + [6, "0186399", 17, "101011001101101110000101010000100010110001011010101"], + [1, "a6298SMx@40I724", 17, "11010010000100101100001011101111011110001010111101000101011110111011011101000101110110001111001001011000110110110010011101001110110011000100010111011011101011101111011101001100101111011101100011101011"], + [2, "V4CZFET", 25, "10010110110101001101010110101001101011011011010010101001101101010101101100101011010110010101010110110010100101101101"], + [7, "79", 25, "101010100010111000111011101"], + [8, "A19A", 17, "101100100101010110010110100101010110010010"], + [3, "597417989799", 17, "10100010110010001001110100110010111011001011101010100100011101001000100111010011101001001000101"], + [4, "9196015", 17, "1010001011001100100010110101111010101110010110011010011101000010101"], + [5, "14477515835", 17, "10100110010100011010001101110110111011011000101010110011010011101001000100001010011101001000101"], + [6, "0826390", 17, "101000100100100110000101011110100010110100111010101"], + [1, ":J_65", 17, "110100100001110010011010110111000101001100001011101111010010110000100010111101100011101011"], + [2, "2V20LVP5U0", 17, "100101101101010110010101101001101010110101100101011010100110110101011010100110100110101011010110110100101101001101010110010101011010100110110101011010011010100101101101"], + [7, "8501", 25, "101011100010100011101010001011101110100011101"], + [8, "B1880419.5-4D", 17, "1001001011010101100101001101010100110101010101001101011010010101011001011010010101101101101011010100101010011010101101001010100110010"], + [3, "544278148264", 17, "10101000110011101001101101110110110111011001101010101110010010001101100101000010111001000100101"], + [4, "1872609", 17, "1010011001011011101110110010011010101010000111001011101001100110101"], + [5, "51082469544", 17, "10101100010011001000110101101110010011010001101010101000011101001001110101110010111001001000101"], + [6, "0120625", 17, "101011001100100110100111010111100100110111001010101"], + [1, "o30127I[LV83902GD", 17, "110100100001000111101010111011110110110110001011001110010111101110111011011101100010001011100011010100011011101110101100010111011110101111001001101111011010111101110110011100101101000100010110001000110111000101100011101011"], + [2, "D210 S", 17, "10010110110101010110010110101100101011011010010101101010011011010100110101101010110101100101001001001010100101101101"], + [7, "997053", 25, "101010111000101110001010101000111000111011100010001110101011101"], + [8, "B.$$-/8//5B", 17, "10010010110110110110101011001010101100101010100110101101101011010011010101101101011011011010110110101001010010010110"], + [3, "826391546219", 17, "10100100110000101011110100101110110011011000101010101110010100001101100110011011101001101100101"], + [4, "3897820", 17, "1010111101011011100010110111011010101001000110110011100101000010101"], + [5, "09462863478", 17, "10100011010001011010001101011110010011011011101010101000010000101011100100010010010001001110101"], + [6, "1937632", 17, "101000101101111010010001010111101000010011011010101"], + [1, "D5HJ4<295l5wi", 17, "1101001000010110001000110111001001100010100010110111000110010011101110011010011001110010111001011001101110010011001010000110111001001111001010010000110100100110011101100011101011"], + [2, "LFWD5QYI", 17, "1001011011010101101010011010110110010101100110101010101011001011011010011010101010101100110110010110101010110100110101001101101010100101101101"], + [7, "823127", 25, "101011101000101110100011100011101010100010111010100011100011101"], + [8, "A6-+5C", 17, "101100100101001010110101001101010110110110110101001010100100110"], + [3, "922372309656", 17, "10100100110011011010000101110110011011011110101010111001011101001010000100111010100001001000101"], + [4, "1455795", 17, "1010011001010001101100010110001010101000100111010010011101001000101"], + [5, "42634366629", 17, "10101000110010011010111101111010100011011110101010101000010100001010000110110011101001110100101"], + [6, "1258894", 17, "101001001101100010001001011011100101110011101010101"], + [1, "165672LWG9_Z380z6", 17, "11010011100100111011001110001011010011000010101111011101000110111011101000110110100010001110010110010100110000111011000101100101110011101001100100111011001101111011011001110100110010000101100011101011"], + [2, "/YYZ ", 17, "1001011011010100100101001011001011010101100101101010100110110101010011010110101011001011010100101101101"], + [7, "6716132785926531", 25, "101010111011101000100011101000100010111011100010001010111010111010100011100011100010100011101010111000101110100010001110111000101011100011101010100011101"], + [8, "B88$+603A", 17, "100100101101001101010100110101010110010101011011011010010101101010100110110010101010110010010"], + [3, "295961215368", 17, "10100010110110001001011100001010011001001101101010110011010011101000010101000010010001100110101"], + [4, "0767811", 17, "1010001101011101101011110111011010101001000110011011001101110010101"], + [5, "57010821457", 17, "10101100010111011000110100110010001101011011101010110110011001101011100100111010001001011100101"], + [6, "1172939", 17, "101001100100100010011011001011101111010001011010101"], + [1, "re584", 17, "110100100001001001111010110010000110111001001011101111010011110100101100001001100011101011"], + [2, "T9", 25, "100101101101010101101100101011001011010100101101101"], + [7, "399687558022", 25, "101011101110001010001010111000100011101011101010111000100011100010111000101011101010001110001010111000101011100011101"], + [8, "A4/D", 17, "1011001001010110100101101101011010100110010"], + [3, "267135345194", 17, "10101011110111011011001101000010110001010000101010101110010011101100110111010010111001001000101"], + [4, "5254450", 17, "1010110001001001101100010100011010101011100100111011100101000100101"], + [5, "02991573147", 17, "10100011010010011000101100010110011001011000101010100010010000101100110101110010001001101100101"], + [6, "0719345", 17, "101001000101100110001011011110101000110111001010101"], + [1, "DG014R3a>72z%7d&F4H", 17, "1101001000010110001000110100010001001110110010011100110110010011101100010111011001011100100101100001101101100011101101110110011100101101111011010001001100111011011101000010011010011001000100011000101100100111011000101000111011101101100011101011"], + [2, "3244BKWIS", 17, "10010110110101101100101010101100101011010100110101101010011010110101101001011011010101001101100110101010101101001101010110101100101001010110110100101101101"], + [7, "5620356163", 25, "101011101000111000101010111010001000111011100011101000101010001110111010100010001110001110101011101"], + [8, "B+2C", 17, "1001001011010110110110101001011010100100110"] + ]; +} diff --git a/test/golden/fenix7/code.png b/test/golden/fenix7/code.png new file mode 100644 index 0000000..d42686b Binary files /dev/null and b/test/golden/fenix7/code.png differ diff --git a/test/golden/fr255s/code.png b/test/golden/fr255s/code.png new file mode 100644 index 0000000..fc6989a Binary files /dev/null and b/test/golden/fr255s/code.png differ diff --git a/test/golden/fr70/code.png b/test/golden/fr70/code.png new file mode 100644 index 0000000..4353f5b Binary files /dev/null and b/test/golden/fr70/code.png differ diff --git a/test/golden/fr965/code.png b/test/golden/fr965/code.png new file mode 100644 index 0000000..35d817e Binary files /dev/null and b/test/golden/fr965/code.png differ diff --git a/test/golden/instinct3amoled45mm/code.png b/test/golden/instinct3amoled45mm/code.png new file mode 100644 index 0000000..6cabbac Binary files /dev/null and b/test/golden/instinct3amoled45mm/code.png differ diff --git a/test/golden/venusq2/code.png b/test/golden/venusq2/code.png new file mode 100644 index 0000000..42c16a7 Binary files /dev/null and b/test/golden/venusq2/code.png differ diff --git a/test/golden/venux1/code.png b/test/golden/venux1/code.png new file mode 100644 index 0000000..bcfd63e Binary files /dev/null and b/test/golden/venux1/code.png differ diff --git a/tools/barcode_ref.py b/tools/barcode_ref.py new file mode 100644 index 0000000..2e58b5e --- /dev/null +++ b/tools/barcode_ref.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# Author: cirakg & claude ai +# Summary: Shared barcode helpers for the reference tools: zxing-cpp module rows, rendering, decoding, check digits. +"""Helpers shared by gen_code_vectors.py and check_codes.py. + +Symbology numbers match source/codes/Codes.mc. zxing-cpp is both the +reference encoder (module rows) and the decoder (README 12.4). +""" + +from __future__ import annotations + +from typing import Any, cast + +import zxingcpp +from PIL import Image + +QR, CODE128, CODE39, EAN13, EAN8, UPCA, UPCE, ITF, CODABAR = range(9) +FORMATS = { + CODE128: zxingcpp.BarcodeFormat.Code128, + CODE39: zxingcpp.BarcodeFormat.Code39, + EAN13: zxingcpp.BarcodeFormat.EAN13, + EAN8: zxingcpp.BarcodeFormat.EAN8, + UPCA: zxingcpp.BarcodeFormat.UPCA, + UPCE: zxingcpp.BarcodeFormat.UPCE, + ITF: zxingcpp.BarcodeFormat.ITF, + CODABAR: zxingcpp.BarcodeFormat.Codabar, +} +CODE39_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%" +LINEAR_QUIET_MODULES = 10 # README 12.2 + + +def reference_modules(symbology: int, text: str) -> str: + """zxing-cpp's module row for text, without quiet zones, as '0'/'1'.""" + barcode = zxingcpp.create_barcode(text, FORMATS[symbology]) + image = barcode.to_image(scale=1, add_hrt=False, add_quiet_zones=False) + # zxing-cpp images support the buffer protocol; its type stubs don't say so. + row = bytes(memoryview(cast(Any, image)))[: image.shape[1]] + return "".join("1" if v < 128 else "0" for v in row) + + +def render_linear(modules: str, scale: int, height: int = 40) -> Image.Image: + """Black bars on white with the quiet zone, `scale` pixels per module.""" + quiet = LINEAR_QUIET_MODULES * scale + image = Image.new("L", (len(modules) * scale + 2 * quiet, height), 255) + for i, module in enumerate(modules): + if module == "1": + image.paste(0, (quiet + i * scale, 0, quiet + (i + 1) * scale, height)) + return image + + +# What to read each symbology as. Code 39 is read as standard Code 39: by +# default zxing-cpp also tries full-ASCII (extended) Code 39, which turns +# pairs like "+S" into "s" (the app doesn't encode extended Code 39). +READ_FORMATS = {**FORMATS, CODE39: zxingcpp.BarcodeFormat.Code39Std, UPCA: zxingcpp.BarcodeFormat.EAN13} + + +def decode(image: Image.Image, symbology: int | None = None) -> list[str]: + """Texts zxing-cpp reads in image (only as `symbology` when given).""" + if symbology is None: + return [result.text for result in zxingcpp.read_barcodes(image)] + # One format works as a format set at runtime; the stubs only allow the set type. + formats = cast(zxingcpp.BarcodeFormats, READ_FORMATS[symbology]) + return [result.text for result in zxingcpp.read_barcodes(image, formats=formats)] + + +def gs1_check(digits: str) -> int: + """GS1 mod-10 check digit of the data digits.""" + total = sum(int(d) * (3 if i % 2 == 0 else 1) for i, d in enumerate(reversed(digits))) + return (10 - total % 10) % 10 + + +def code39_check(text: str) -> str: + return CODE39_ALPHABET[sum(CODE39_ALPHABET.index(c) for c in text) % 43] + + +def upce_expand(code: str) -> str: + """UPC-E 'NS dddddd C' (8 digits) as the 13-digit EAN form zxing reads.""" + ns, d, check = code[0], code[1:7], code[7] + last = d[5] + if last in "012": + body = d[0:2] + last + "0000" + d[2:5] + elif last == "3": + body = d[0:3] + "00000" + d[3:5] + elif last == "4": + body = d[0:4] + "00000" + d[4] + else: + body = d[0:5] + "0000" + last + return "0" + ns + body + check + + +def expected_reading(symbology: int, readable: str) -> str: + """What zxing-cpp reports when it decodes a code whose human text is `readable`.""" + if symbology == UPCA: + return "0" + readable + if symbology == UPCE: + return upce_expand(readable) + return readable diff --git a/tools/check_codes.py b/tools/check_codes.py new file mode 100644 index 0000000..22d3033 --- /dev/null +++ b/tools/check_codes.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# Author: cirakg & claude ai +# Summary: Decode the barcodes the watch's encoders produced in a unit-test run with zxing-cpp (README 12.4 check 2). +"""Decode-check the CODEDUMP lines of a Monkey C test log. + +Each line is "CODEDUMP ". Every +code is rendered with its quiet zone at 1, 2 and 3 pixels per module and +must decode to the expected text at each size. + +Usage: check_codes.py LOG [LOG ...] Exit: 0 all decoded, 1 a failure, 2 no dump found. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +import barcode_ref as ref + +DUMP = re.compile(r"CODEDUMP (\d+) ([0-9a-f]*) ([01]+)") +SCALES = (1, 2, 3) + + +def check_log(path: Path) -> tuple[int, list[str]]: + """(codes checked, failure messages) for one log.""" + checked = 0 + failures: list[str] = [] + for match in DUMP.finditer(path.read_text(encoding="utf-8", errors="replace")): + symbology = int(match.group(1)) + text = bytes.fromhex(match.group(2)).decode("utf-8") + modules = match.group(3) + expected = ref.expected_reading(symbology, text) + for scale in SCALES: + got = ref.decode(ref.render_linear(modules, scale), symbology) + if expected not in got: + failures.append(f"symbology {symbology} '{text}' at {scale} px/module: read {got}") + checked += 1 + return checked, failures + + +def main(argv: list[str] | None = None) -> int: + paths = [Path(p) for p in (argv if argv is not None else sys.argv[1:])] + total = 0 + failed = False + for path in paths: + checked, failures = check_log(path) + total += checked + for failure in failures[:10]: + print(f"[{path.stem}] {failure}") + failed = failed or bool(failures) + print(f"[{path.stem}] decoded {checked} barcodes at {len(SCALES)} sizes: {'FAILED' if failures else 'ok'}") + if total == 0: + print("no CODEDUMP lines found") + return 2 + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/gen_code_vectors.py b/tools/gen_code_vectors.py new file mode 100644 index 0000000..970ca06 --- /dev/null +++ b/tools/gen_code_vectors.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +# Author: cirakg & claude ai +# Summary: Generate test/data/CodeVectors.mc: zxing-cpp reference module rows for the linear barcode encoders. +"""Reference module rows for README 12.4 check 1 (all fake payloads). + +For every linear symbology: fixed edge cases plus seeded random payloads, +each with the options the watch uses and zxing-cpp's module row. Where the +watch adds something zxing doesn't (a Code 39 or ITF check character, Codabar +start/stop characters), the reference is made from the completed text. + + gen_code_vectors.py [--seed 1212] [--count 25] [--out test/data/CodeVectors.mc] +""" + +from __future__ import annotations + +import argparse +import random +import string +from pathlib import Path + +import barcode_ref as ref + +CHECK_ADD, CHECK_FIX, CHECK_OFF = 0, 1, 2 +TEXT_ON = 0x10 + + +def options(check: int) -> int: + return TEXT_ON | (check << 2) | 1 + + +def digits(rng: random.Random, n: int) -> str: + return "".join(rng.choice(string.digits) for _ in range(n)) + + +def valid_upce(rng: random.Random) -> str: + """Six UPC-E digits that are a canonical zero suppression.""" + while True: + d = digits(rng, 6) + last = d[5] + if (last == "3" and d[2] in "012") or (last == "4" and d[3] == "0") or (last in "56789" and d[4] == "0"): + continue + return d + + +def cases(rng: random.Random, count: int) -> list[tuple[int, str, int, str]]: + """(symbology, payload, options, zxing text) tuples.""" + out: list[tuple[int, str, int, str]] = [] + fixed = [ + (ref.EAN13, "590123412345", options(CHECK_ADD), "590123412345"), + (ref.EAN13, "5901234123457", options(CHECK_ADD), "5901234123457"), + (ref.EAN8, "1234567", options(CHECK_ADD), "1234567"), + (ref.UPCA, "01234567890", options(CHECK_ADD), "01234567890"), + (ref.UPCE, "0425261", options(CHECK_ADD), "0425261"), + (ref.UPCE, "1234565", options(CHECK_ADD), "1234565"), + (ref.CODE128, "1234", options(CHECK_ADD), "1234"), + (ref.CODE128, "123456789", options(CHECK_ADD), "123456789"), + (ref.CODE128, "AB12345678", options(CHECK_ADD), "AB12345678"), + (ref.CODE128, "A1234B12345", options(CHECK_ADD), "A1234B12345"), + (ref.CODE128, " ~", options(CHECK_ADD), " ~"), + (ref.CODE39, "CODE 39", options(CHECK_OFF), "CODE 39"), + (ref.CODE39, "CODE-39", options(CHECK_ADD), "CODE-39" + ref.code39_check("CODE-39")), + (ref.ITF, "1234", options(CHECK_OFF), "1234"), + (ref.ITF, "123", options(CHECK_ADD), "123" + str(ref.gs1_check("123"))), + (ref.CODABAR, "A40156B", options(CHECK_ADD), "A40156B"), + (ref.CODABAR, "31117013206375", options(CHECK_ADD), "A31117013206375A"), + ] + out.extend(fixed) + printable = string.ascii_letters + string.digits + " !#$%&()*+,-./:;<=>?@[]^_{|}~" + for _ in range(count): + out.append((ref.EAN13, digits(rng, 12), options(CHECK_ADD), "")) + out.append((ref.EAN8, digits(rng, 7), options(CHECK_ADD), "")) + out.append((ref.UPCA, digits(rng, 11), options(CHECK_ADD), "")) + out.append((ref.UPCE, rng.choice("01") + valid_upce(rng), options(CHECK_ADD), "")) + n = rng.randint(1, 20) + mixed = "".join(rng.choice(string.digits) if rng.random() < 0.5 else rng.choice(printable) for _ in range(n)) + out.append((ref.CODE128, mixed, options(CHECK_ADD), mixed)) + c39 = "".join(rng.choice(ref.CODE39_ALPHABET) for _ in range(rng.randint(1, 12))) + with_check = rng.random() < 0.5 + out.append( + ( + ref.CODE39, + c39, + options(CHECK_ADD if with_check else CHECK_OFF), + c39 + ref.code39_check(c39) if with_check else c39, + ) + ) + itf = digits(rng, 2 * rng.randint(1, 8)) + out.append((ref.ITF, itf, options(CHECK_OFF), itf)) + inner = "".join(rng.choice("0123456789-$:/.+") for _ in range(rng.randint(1, 12))) + guard = rng.choice("ABCD") + inner + rng.choice("ABCD") + out.append((ref.CODABAR, guard, options(CHECK_ADD), guard)) + return out + + +def render(rows: list[tuple[int, str, int, str]], seed: int) -> str: + lines = [] + for symbology, payload, opts, text in rows: + modules = ref.reference_modules(symbology, text or payload) + escaped = payload.replace("\\", "\\\\").replace('"', '\\"') + lines.append(f' [{symbology}, "{escaped}", {opts}, "{modules}"]') + body = ",\n".join(lines) + return ( + "// Author: cirakg & claude ai\n" + "// Summary: zxing-cpp reference module rows for the linear barcode encoders (generated; do not edit).\n" + f"//\n// Generated by tools/gen_code_vectors.py, seed {seed}. Rows: [symbology, payload, options,\n" + "// expected modules without quiet zones]. Fake payloads only.\n\n" + "import Toybox.Lang;\n\n(:test)\nmodule CodeVectors {\n const LINEAR = [\n" + body + "\n ];\n}\n" + ) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--seed", type=int, default=1212) + parser.add_argument("--count", type=int, default=25) + parser.add_argument("--out", type=Path, default=Path("test/data/CodeVectors.mc")) + args = parser.parse_args(argv) + rows = cases(random.Random(args.seed), args.count) + args.out.write_text(render(rows, args.seed), encoding="utf-8") + print(f"wrote {len(rows)} reference rows to {args.out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/test_barcode_tools.py b/tools/tests/test_barcode_tools.py new file mode 100644 index 0000000..7be1f18 --- /dev/null +++ b/tools/tests/test_barcode_tools.py @@ -0,0 +1,65 @@ +# Author: cirakg & claude ai +# Summary: Tests for the barcode reference tools: zxing-cpp rows, rendering, decoding, check digits, generator, checker. +from __future__ import annotations + +import random +from pathlib import Path + +import barcode_ref as ref +import check_codes +import gen_code_vectors + +ROOT = Path(__file__).resolve().parents[2] + + +def test_reference_rows_have_the_standard_lengths() -> None: + assert ref.reference_modules(ref.EAN13, "590123412345") == ( + "10100010110100111011001100100110111101001110101010110011011011001000010101110010011101000100101" + ) + assert len(ref.reference_modules(ref.EAN8, "1234567")) == 67 + assert len(ref.reference_modules(ref.UPCE, "0425261")) == 51 + + +def test_rendered_codes_decode() -> None: + modules = ref.reference_modules(ref.CODE39, "ABC") + for scale in (1, 2, 3): + assert ref.decode(ref.render_linear(modules, scale), ref.CODE39) == ["ABC"] + + +def test_check_digits() -> None: + assert ref.gs1_check("590123412345") == 7 + assert ref.gs1_check("1234567") == 0 + assert ref.code39_check("CODE 39") == "R" # (12+24+13+14+38+3+9) % 43 = 27 + + +def test_expected_reading_normalises_upc() -> None: + assert ref.expected_reading(ref.UPCA, "012345678905") == "0012345678905" + assert ref.upce_expand("01234565") == "0012345000065" + + +def test_generator_only_makes_canonical_upce() -> None: + rng = random.Random(5) + for _ in range(200): + d = gen_code_vectors.valid_upce(rng) + ref.reference_modules(ref.UPCE, "0" + d) # zxing-cpp rejects non-canonical ones + + +def test_committed_vectors_match_the_generator() -> None: + rows = gen_code_vectors.cases(random.Random(1212), 25) + committed = (ROOT / "test" / "data" / "CodeVectors.mc").read_text(encoding="utf-8") + assert committed == gen_code_vectors.render(rows, 1212) + + +def test_check_codes_reads_dump_lines(tmp_path: Path) -> None: + good = ref.reference_modules(ref.EAN8, "1234567") + log = tmp_path / "dev.log" + log.write_text( + f"CODEDUMP {ref.EAN8} {b'12345670'.hex()} {good}\nCODEDUMP {ref.EAN8} {b'12345670'.hex()} {good[:-5]}00000\n", + encoding="utf-8", + ) + checked, failures = check_codes.check_log(log) + assert checked == 2 and len(failures) == 3 # the damaged code fails at all three sizes + assert check_codes.main([str(log)]) == 1 + empty = tmp_path / "empty.log" + empty.write_text("nothing\n", encoding="utf-8") + assert check_codes.main([str(empty)]) == 2 diff --git a/tools/ui_flow.py b/tools/ui_flow.py index 3e82231..82e5d65 100644 --- a/tools/ui_flow.py +++ b/tools/ui_flow.py @@ -23,7 +23,8 @@ The flow covers: the results of an Aegis import sent through the settings form (the fixture sets the form, as the phone would), the list, a TOTP entry, a HOTP entry and its next code, the PIN screen, a wrong PIN, a correct PIN opening a locked entry, auto-lock closing it, the menu, the About screen (watches with buttons; touch-only -menus have no focus to move) and the lockout after 5 wrong PINs. +menus have no focus to move), the lockout after 5 wrong PINs, and an EAN-13 +code whose screenshot must decode with zxing-cpp (README 12.4). Each screenshot is compared with test/golden//.png like sim_screenshot.py does (or saved there with --approve). @@ -43,6 +44,7 @@ from dataclasses import dataclass from pathlib import Path import sim_screenshot as shots +import zxingcpp EXIT_OK, EXIT_DIFFERENT, EXIT_FLOW = 0, 1, 2 SCREEN_TIMEOUT_S = 20.0 @@ -57,6 +59,7 @@ FIXTURE_PIN = "123456" # source/app/Fixtures.mc WRONG_PIN = "000000" FAILURES_UNTIL_WAIT = 5 # source/storage/Lockout.mc: the 5th failure starts a delay SELECTOR_OK = 10 # source/ui/PinView.mc: selector positions 0-9, then OK +GYM_CARD = "5901234123457" # source/app/Fixtures.mc: EAN-13 with its check digit class FlowError(Exception): @@ -295,7 +298,7 @@ class Flow: """Call before an input whose result `expect` should wait for.""" self.log.set_mark() - def shot(self, name: str) -> None: + def shot(self, name: str, expect_code: str | None = None) -> None: raw = self.sim.capture(name) out = self.out / f"{name}.png" label = f"{self.sim.device_dir.name}/{name}" @@ -305,6 +308,14 @@ class Flow: print(f"[{label}] {err}") self.results[name] = EXIT_DIFFERENT return + if expect_code is not None: + # README 12.4 check 3: the code on the screenshot must decode. + read = [r.text for r in zxingcpp.read_barcodes(screen)] + if expect_code not in read: + print(f"[{label}] the barcode didn't decode to {expect_code!r}: read {read}") + self.results[name] = EXIT_DIFFERENT + return + print(f"[{label}] barcode decodes to {expect_code!r}") golden = self.golden / f"{name}.png" if self.approve: shots.save_png(screen, golden, f"Approved simulator screen '{name}' of {self.sim.device_dir.name}") @@ -414,6 +425,19 @@ def flow_lockout(f: Flow) -> None: f.shot("pin_wait") +def flow_code(f: Flow) -> None: + """The EAN-13 gym card (index 6): its screenshot must decode.""" + f.act() + f.back() # leave the waiting PIN screen + f.expect("list") + for _ in range(4): # Bank (2) to the gym card (6) + f.next() + f.act() + f.open_focused_row() + f.expect("code") + f.shot("code", expect_code=GYM_CARD) + + def run_flow(f: Flow) -> None: """The scripted session; see the module docstring.""" flow_import(f) @@ -421,6 +445,7 @@ def run_flow(f: Flow) -> None: flow_locked_entry(f) flow_menu(f) flow_lockout(f) + flow_code(f) def find_key(data: object, key: str) -> object: -- cgit v1.2.3