| // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| library json_test; |
| |
| import "package:expect/expect.dart"; |
| import "dart:json"; |
| |
| bool badFormat(e) => e is FormatException; |
| |
| void testJson(json, expected) { |
| var value = parse(json); |
| compare(expected, actual, path) { |
| if (expected is List) { |
| Expect.isTrue(actual is List); |
| Expect.equals(expected.length, actual.length, "$path: List length"); |
| for (int i = 0; i < expected.length; i++) { |
| compare(expected[i], actual[i], "$path[$i]"); |
| } |
| } else if (expected is Map) { |
| Expect.isTrue(actual is Map); |
| Expect.equals(expected.length, actual.length, "$path: Map size"); |
| expected.forEach((key, value) { |
| Expect.isTrue(actual.containsKey(key)); |
| compare(value, actual[key], "$path[$key]"); |
| }); |
| } else if (expected is num) { |
| Expect.equals(expected is int, actual is int, "$path: same number type"); |
| Expect.isTrue(expected.compareTo(actual) == 0, |
| "$path: $expected vs. $actual"); |
| } else { |
| // String, bool, null. |
| Expect.equals(expected, actual, path); |
| } |
| } |
| compare(expected, value, "value"); |
| } |
| |
| String escape(String s) { |
| var sb = new StringBuffer(); |
| for (int i = 0; i < s.length; i++) { |
| int code = s.codeUnitAt(i); |
| if (code == '\\'.codeUnitAt(0)) sb.write(r'\\'); |
| else if (code == '\"'.codeUnitAt(0)) sb.write(r'\"'); |
| else if (code >= 32 && code < 127) sb.writeCharCode(code); |
| else { |
| String hex = '000${code.toRadixString(16)}'; |
| sb.write(r'\u' '${hex.substring(hex.length - 4)}'); |
| } |
| } |
| return '$sb'; |
| } |
| |
| void testThrows(json) { |
| Expect.throws(() => parse(json), badFormat, "json = '${escape(json)}'"); |
| } |
| |
| testNumbers() { |
| // Positive tests for number formats. |
| var integerList = ["0","9","9999"]; |
| var signList = ["", "-"]; |
| var fractionList = ["", ".0", ".1", ".99999"]; |
| var exponentList = [""]; |
| for (var exphead in ["e", "E", "e-", "E-", "e+", "E+"]) { |
| for (var expval in ["0", "1", "200"]) { |
| exponentList.add("$exphead$expval"); |
| } |
| } |
| |
| for (var integer in integerList) { |
| for (var sign in signList) { |
| for (var fraction in fractionList) { |
| for (var exp in exponentList) { |
| var literal = "$sign$integer$fraction$exp"; |
| var parseNumber = |
| ((fraction == "" && exp == "") ? (String x) => int.parse(x) |
| : (String x) => double.parse(x)); |
| var expectedValue = parseNumber(literal); |
| testJson(literal, expectedValue); |
| } |
| } |
| } |
| } |
| |
| // Negative tests (syntax error). |
| // testError thoroughly tests the given parts with a lot of valid |
| // values for the other parts. |
| testError({signs, integers, fractions, exponents}) { |
| def(value, defaultValue) { |
| if (value == null) return defaultValue; |
| if (value is List) return value; |
| return [value]; |
| } |
| signs = def(signs, signList); |
| integers = def(integers, integerList); |
| fractions = def(fractions, fractionList); |
| exponents = def(exponents, exponentList); |
| for (var integer in integers) { |
| for (var sign in signs) { |
| for (var fraction in fractions) { |
| for (var exponent in exponents) { |
| var literal = "$sign$integer$fraction$exponent"; |
| testThrows(literal); |
| } |
| } |
| } |
| } |
| } |
| // Doubles overflow to Infinity. |
| testJson("1e+400", double.INFINITY); |
| // (Integers do not, but we don't have those on dart2js). |
| |
| // Integer part cannot be omitted: |
| testError(integers: ""); |
| |
| // Test for "Initial zero only allowed for zero integer part" moved to |
| // json_strict_test.dart because IE's JSON.decode accepts additional initial |
| // zeros. |
| |
| // Only minus allowed as sign. |
| testError(signs: "+"); |
| // Requires digits after decimal point. |
| testError(fractions: "."); |
| // Requires exponent digts, and only digits. |
| testError(exponents: ["e", "e+", "e-", "e.0"]); |
| |
| // No whitespace inside numbers. |
| // Additional case "- 2.2e+2" in json_strict_test.dart. |
| testThrows("-2 .2e+2"); |
| testThrows("-2. 2e+2"); |
| testThrows("-2.2 e+2"); |
| testThrows("-2.2e +2"); |
| testThrows("-2.2e+ 2"); |
| |
| testThrows("[2.,2]"); |
| testThrows("{2.:2}"); |
| } |
| |
| testStrings() { |
| // String parser accepts and understands escapes. |
| var input = r'"\u0000\uffff\n\r\f\t\b\/\\\"' '\x20\ufffd\uffff"'; |
| var expected = "\u0000\uffff\n\r\f\t\b\/\\\"\x20\ufffd\uffff"; |
| testJson(input, expected); |
| // Empty string. |
| testJson(r'""', ""); |
| // Escape first. |
| testJson(r'"\"........"', "\"........"); |
| // Escape last. |
| testJson(r'"........\""', "........\""); |
| // Escape middle. |
| testJson(r'"....\"...."', "....\"...."); |
| |
| // Does not accept single quotes. |
| testThrows(r"''"); |
| // Throws on unterminated strings. |
| testThrows(r'"......\"'); |
| // Throws on unterminated escapes. |
| testThrows(r'"\'); // ' is not escaped. |
| testThrows(r'"\a"'); |
| testThrows(r'"\u"'); |
| testThrows(r'"\u1"'); |
| testThrows(r'"\u12"'); |
| testThrows(r'"\u123"'); |
| testThrows(r'"\ux"'); |
| testThrows(r'"\u1x"'); |
| testThrows(r'"\u12x"'); |
| testThrows(r'"\u123x"'); |
| // Throws on bad escapes. |
| testThrows(r'"\a"'); |
| testThrows(r'"\x00"'); |
| testThrows(r'"\c2"'); |
| testThrows(r'"\000"'); |
| testThrows(r'"\u{0}"'); |
| testThrows(r'"\%"'); |
| testThrows('"\\\x00"'); // Not raw string! |
| // Throws on control characters. |
| for (int i = 0; i < 32; i++) { |
| var string = new String.fromCharCodes([0x22,i,0x22]); // '"\x00"' etc. |
| testThrows(string); |
| } |
| } |
| |
| |
| testObjects() { |
| testJson(r'{}', {}); |
| testJson(r'{"x":42}', {"x":42}); |
| testJson(r'{"x":{"x":{"x":42}}}', {"x": {"x": {"x": 42}}}); |
| testJson(r'{"x":10,"x":42}', {"x": 42}); |
| testJson(r'{"":42}', {"": 42}); |
| |
| // Keys must be strings. |
| testThrows(r'{x:10}'); |
| testThrows(r'{true:10}'); |
| testThrows(r'{false:10}'); |
| testThrows(r'{null:10}'); |
| testThrows(r'{42:10}'); |
| testThrows(r'{42e1:10}'); |
| testThrows(r'{-42:10}'); |
| testThrows(r'{["text"]:10}'); |
| testThrows(r'{:10}'); |
| } |
| |
| testArrays() { |
| testJson(r'[]', []); |
| testJson(r'[1.1e1,"string",true,false,null,{}]', |
| [1.1e1, "string", true, false, null, {}]); |
| testJson(r'[[[[[[]]]],[[[]]],[[]]]]', [[[[[[]]]],[[[]]],[[]]]]); |
| testJson(r'[{},[{}],{"x":[]}]', [{},[{}],{"x":[]}]); |
| |
| testThrows(r'[1,,2]'); |
| testThrows(r'[1,2,]'); |
| testThrows(r'[,2]'); |
| } |
| |
| testWords() { |
| testJson(r'true', true); |
| testJson(r'false', false); |
| testJson(r'null', null); |
| testJson(r'[true]', [true]); |
| testJson(r'{"true":true}', {"true": true}); |
| |
| testThrows(r'truefalse'); |
| testThrows(r'trues'); |
| testThrows(r'nulll'); |
| testThrows(r'full'); |
| testThrows(r'nul'); |
| testThrows(r'tru'); |
| testThrows(r'fals'); |
| testThrows(r'\null'); |
| testThrows(r't\rue'); |
| testThrows(r't\rue'); |
| } |
| |
| testWhitespace() { |
| // Valid white-space characters. |
| var v = '\t\r\n\ '; |
| // Invalid white-space and non-recognized characters. |
| var invalids = ['\x00', '\f', '\x08', '\\', '\xa0','\u2028', '\u2029']; |
| |
| // Valid whitespace accepted "everywhere". |
| testJson('$v[${v}-2.2e2$v,$v{$v"key"$v:${v}true$v}$v,$v"ab"$v]$v', |
| [-2.2e2, {"key": true}, "ab"]); |
| |
| // IE9 accepts invalid characters at the end, so some of these tests have been |
| // moved to json_strict_test.dart. |
| for (var i in invalids) { |
| testThrows('${i}"s"'); |
| testThrows('42${i}'); |
| testThrows('$i[]'); |
| testThrows('[$i]'); |
| testThrows('[$i"s"]'); |
| testThrows('["s"$i]'); |
| testThrows('$i{"k":"v"}'); |
| testThrows('{$i"k":"v"}'); |
| testThrows('{"k"$i:"v"}'); |
| testThrows('{"k":$i"v"}'); |
| testThrows('{"k":"v"$i}'); |
| } |
| } |
| |
| main() { |
| testNumbers(); |
| testStrings(); |
| testWords(); |
| testObjects(); |
| testArrays(); |
| testWhitespace(); |
| } |