summaryrefslogtreecommitdiff
path: root/test/import/JsonTest.mc
blob: c98aaa427ac0afa69e4d83082943ac5caf0395bf (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
121
122
123
124
125
126
127
128
// Author:  cirakg & claude ai
// Summary: Tests for the streaming JSON tokenizer: tokens, escapes, numbers, errors, incomplete input, limits.

import Toybox.Lang;
import Toybox.Test;

(:test)
module JsonFixtures {

    // All tokens of text, as "kind:value" strings (values for keys, strings
    // and numbers), ending with the final token.
    function tokens(text as String) as Array<String> {
        var tok = new Json.Tokenizer(Bytes.fromUtf8(text));
        var out = [] as Array<String>;
        for (var i = 0; i < 200; i++) {
            var t = tok.next();
            if (t == Json.T_KEY) {
                out.add("K:" + tok.text);
            } else if (t == Json.T_STRING) {
                out.add("S:" + tok.text);
            } else if (t == Json.T_NUMBER) {
                out.add(tok.numberIsInteger ? "N:" + tok.number : "N:float");
            } else {
                out.add("T" + t);
            }
            if (t == Json.T_END || t == Json.T_INCOMPLETE || t == Json.T_ERROR) {
                return out;
            }
        }
        return out;
    }

    function last(text as String) as Number {
        var tok = new Json.Tokenizer(Bytes.fromUtf8(text));
        for (var i = 0; i < 500; i++) {
            var t = tok.next();
            if (t == Json.T_END || t == Json.T_INCOMPLETE || t == Json.T_ERROR) {
                return t;
            }
        }
        return -1;
    }
}

(:test)
function jsonTokenizesAllValueKinds(logger as Test.Logger) as Boolean {
    var got = JsonFixtures.tokens("{\"a\": [1, -20, true, false, null, \"x\\\"y\\n\\u00e9\\ud83d\\ude00\"], \"b\": {}}");
    var expected = ["T1", "K:a", "T3", "N:1", "N:-20", "T8", "T9", "T10", "S:x\"y\né😀", "T4", "K:b", "T1", "T2", "T2", "T0"];
    if (!got.toString().equals(expected.toString())) {
        logger.error("got " + got.toString());
        return false;
    }
    return true;
}

(:test)
function jsonNumbers(logger as Test.Logger) as Boolean {
    var got = JsonFixtures.tokens("[0, 1.5, 1e3, -0, 123456789012345678901, 999999999999999999]");
    var expected = ["T3", "N:0", "N:float", "N:float", "N:0", "N:float", "N:999999999999999999", "T4", "T0"];
    if (!got.toString().equals(expected.toString())) {
        logger.error("got " + got.toString());
        return false;
    }
    return true;
}

(:test)
function jsonRejectsInvalidDocuments(logger as Test.Logger) as Boolean {
    var bad = [
        "[1,]", "{\"a\" 1}", "[01]", "[\"\\x\"]", "[\"\\ud800\"]", "[\"\\udc00\"]", "{a: 1}", "[1] [2]",
        "[\"\\u0000\"]", "[1.]", "[-]", "[tru]", "{\"a\":1,}", "[\"a\tb\"]", "[1 2]", "]"
    ];
    for (var i = 0; i < bad.size(); i++) {
        if (JsonFixtures.last(bad[i] as String) != Json.T_ERROR) {
            logger.error("accepted invalid JSON " + bad[i]);
            return false;
        }
    }
    return true;
}

// Input that stops inside the document is "incomplete", not an error, so
// a multi-part import can wait for the rest.
(:test)
function jsonReportsIncompleteInput(logger as Test.Logger) as Boolean {
    var partial = ["{", "{\"a\"", "{\"a\":", "{\"a\": \"abc", "{\"a\": 12", "[tr", "[1, {\"b\": [", "{\"a\": \"x\\"];
    for (var i = 0; i < partial.size(); i++) {
        if (JsonFixtures.last(partial[i] as String) != Json.T_INCOMPLETE) {
            logger.error("not incomplete: " + partial[i]);
            return false;
        }
    }
    return JsonFixtures.last("") == Json.T_INCOMPLETE && JsonFixtures.last("  7  ") == Json.T_END;
}

(:test)
function jsonEnforcesTheDepthLimit(logger as Test.Logger) as Boolean {
    var ok = "";
    var tooDeep = "";
    for (var i = 0; i < Json.MAX_DEPTH; i++) {
        ok += "[";
    }
    tooDeep = ok + "[";
    var close = "";
    for (var i = 0; i < Json.MAX_DEPTH; i++) {
        close += "]";
    }
    return JsonFixtures.last(ok + close) == Json.T_END && JsonFixtures.last(tooDeep + close + "]") == Json.T_ERROR;
}

// Long strings (icons) are skipped without decoding, including escaped
// quotes inside them; long keys are errors.
(:test)
function jsonSkipsLongStrings(logger as Test.Logger) as Boolean {
    var big = "";
    for (var i = 0; i < 300; i++) {
        big += "ABCDEFGHIJ";
    }
    var withQuote = big + "\\\"" + big;
    var got = JsonFixtures.tokens("{\"icon\": \"" + big + "\", \"q\": \"" + withQuote + "\", \"n\": \"ok\"}");
    var expected = ["T1", "K:icon", "T11", "K:q", "T11", "K:n", "S:ok", "T2", "T0"];
    if (!got.toString().equals(expected.toString())) {
        logger.error("got " + got.toString());
        return false;
    }
    return JsonFixtures.last("{\"" + big + "\": 1}") == Json.T_ERROR
        && JsonFixtures.last("{\"icon\": \"" + big) == Json.T_INCOMPLETE;
}