// 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 { var tok = new Json.Tokenizer(Bytes.fromUtf8(text)); var out = [] as Array; 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; }