summaryrefslogtreecommitdiff
path: root/test/import/JsonTest.mc
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-20 01:51:05 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-20 01:51:05 +0200
commit42f9331e693f5058fcfcd7aef5a92cabc04c20ee (patch)
treef3dab2657eb592ee468e6cebc9b47d9da7e08683 /test/import/JsonTest.mc
parentd537f4e154a40075405b0ab2fed14fbe2eeaac81 (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.mc128
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;
+}