diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-20 01:51:05 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-20 01:51:05 +0200 |
| commit | 42f9331e693f5058fcfcd7aef5a92cabc04c20ee (patch) | |
| tree | f3dab2657eb592ee468e6cebc9b47d9da7e08683 /test/import/JsonTest.mc | |
| parent | d537f4e154a40075405b0ab2fed14fbe2eeaac81 (diff) | |
test(import): cover parsers, importer and a seeded fuzz corpus; import 100 entries in the UI flow
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'test/import/JsonTest.mc')
| -rw-r--r-- | test/import/JsonTest.mc | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/test/import/JsonTest.mc b/test/import/JsonTest.mc new file mode 100644 index 0000000..c98aaa4 --- /dev/null +++ b/test/import/JsonTest.mc @@ -0,0 +1,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; +} |
