aboutsummaryrefslogtreecommitdiff
path: root/test/codes/QrTest.mc
blob: 50cb35a304bdc2f9123d2b346b5184501817817c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// 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;
}