| /* Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include <string.h> |
| #define TESTLUCY_USE_SHORT_NAMES |
| #include "Lucy/Util/ToolSet.h" |
| |
| #include "Clownfish/Num.h" |
| #include "Clownfish/TestHarness/TestBatchRunner.h" |
| #include "Lucy/Test.h" |
| #include "Lucy/Test/Util/TestJson.h" |
| #include "Lucy/Util/Json.h" |
| #include "Lucy/Store/FileHandle.h" |
| #include "Lucy/Store/RAMFolder.h" |
| |
| TestJson* |
| TestJson_new() { |
| return (TestJson*)Class_Make_Obj(TESTJSON); |
| } |
| |
| // Create a test data structure including at least one each of Hash, Vector, |
| // and String. |
| static Obj* |
| S_make_dump() { |
| Hash *dump = Hash_new(0); |
| Hash_Store_Utf8(dump, "foo", 3, (Obj*)Str_newf("foo")); |
| Hash_Store_Utf8(dump, "stuff", 5, (Obj*)Vec_new(0)); |
| return (Obj*)dump; |
| } |
| |
| static void |
| test_tolerance(TestBatchRunner *runner) { |
| String *foo = Str_newf("foo"); |
| String *not_json = Json_to_json((Obj*)foo); |
| TEST_TRUE(runner, not_json == NULL, |
| "to_json returns NULL when fed invalid data type"); |
| TEST_TRUE(runner, Err_get_error() != NULL, |
| "to_json sets global error when fed invalid data type"); |
| DECREF(foo); |
| } |
| |
| // Test escapes for control characters ASCII 0-31. |
| static const char* control_escapes[] = { |
| "\\u0000", |
| "\\u0001", |
| "\\u0002", |
| "\\u0003", |
| "\\u0004", |
| "\\u0005", |
| "\\u0006", |
| "\\u0007", |
| "\\b", |
| "\\t", |
| "\\n", |
| "\\u000b", |
| "\\f", |
| "\\r", |
| "\\u000e", |
| "\\u000f", |
| "\\u0010", |
| "\\u0011", |
| "\\u0012", |
| "\\u0013", |
| "\\u0014", |
| "\\u0015", |
| "\\u0016", |
| "\\u0017", |
| "\\u0018", |
| "\\u0019", |
| "\\u001a", |
| "\\u001b", |
| "\\u001c", |
| "\\u001d", |
| "\\u001e", |
| "\\u001f", |
| NULL |
| }; |
| |
| // Test quote and backslash escape in isolation, then in context. |
| static const char* quote_escapes_source[] = { |
| "\"", |
| "\\", |
| "abc\"", |
| "abc\\", |
| "\"xyz", |
| "\\xyz", |
| "\\\"", |
| "\"\\", |
| NULL |
| }; |
| static const char* quote_escapes_json[] = { |
| "\\\"", |
| "\\\\", |
| "abc\\\"", |
| "abc\\\\", |
| "\\\"xyz", |
| "\\\\xyz", |
| "\\\\\\\"", |
| "\\\"\\\\", |
| NULL |
| }; |
| |
| static void |
| test_escapes(TestBatchRunner *runner) { |
| for (int i = 0; control_escapes[i] != NULL; i++) { |
| String *string = Str_new_from_char(i); |
| const char *escaped = control_escapes[i]; |
| String *json = Json_to_json((Obj*)string); |
| String *trimmed = Str_Trim(json); |
| String *decoded = (String*)Json_from_json(json); |
| |
| String *json_wanted = Str_newf("\"%s\"", escaped); |
| TEST_TRUE(runner, Str_Equals(json_wanted, (Obj*)trimmed), |
| "encode control escape: %s", escaped); |
| |
| TEST_TRUE(runner, decoded != NULL && Str_Equals(string, (Obj*)decoded), |
| "decode control escape: %s", escaped); |
| |
| DECREF(string); |
| DECREF(json); |
| DECREF(trimmed); |
| DECREF(decoded); |
| DECREF(json_wanted); |
| } |
| |
| for (int i = 0; quote_escapes_source[i] != NULL; i++) { |
| const char *source = quote_escapes_source[i]; |
| const char *escaped = quote_escapes_json[i]; |
| String *string = Str_new_from_utf8(source, strlen(source)); |
| String *json = Json_to_json((Obj*)string); |
| String *trimmed = Str_Trim(json); |
| String *decoded = (String*)Json_from_json(json); |
| |
| String *json_wanted = Str_newf("\"%s\"", escaped); |
| TEST_TRUE(runner, Str_Equals(json_wanted, (Obj*)trimmed), |
| "encode quote/backslash escapes: %s", source); |
| |
| TEST_TRUE(runner, decoded != NULL && Str_Equals(string, (Obj*)decoded), |
| "decode quote/backslash escapes: %s", source); |
| |
| DECREF(string); |
| DECREF(json); |
| DECREF(trimmed); |
| DECREF(decoded); |
| DECREF(json_wanted); |
| } |
| } |
| |
| static void |
| test_numbers(TestBatchRunner *runner) { |
| Integer *i64 = Int_new(33); |
| String *json = Json_to_json((Obj*)i64); |
| String *trimmed = Str_Trim(json); |
| TEST_TRUE(runner, Str_Equals_Utf8(trimmed, "33", 2), "Integer"); |
| DECREF(json); |
| DECREF(trimmed); |
| |
| Float *f64 = Float_new(33.33); |
| json = Json_to_json((Obj*)f64); |
| if (json) { |
| double value = Str_To_F64(json); |
| double diff = 33.33 - value; |
| if (diff < 0.0) { diff = 0.0 - diff; } |
| TEST_TRUE(runner, diff < 0.0001, "Float"); |
| DECREF(json); |
| } |
| else { |
| FAIL(runner, "Float conversion to json failed."); |
| } |
| |
| DECREF(i64); |
| DECREF(f64); |
| } |
| |
| static void |
| test_to_and_from(TestBatchRunner *runner) { |
| Obj *dump = S_make_dump(); |
| String *json = Json_to_json(dump); |
| Obj *got = Json_from_json(json); |
| TEST_TRUE(runner, got != NULL && Obj_Equals(dump, got), |
| "Round trip through to_json and from_json"); |
| DECREF(dump); |
| DECREF(json); |
| DECREF(got); |
| } |
| |
| static void |
| test_spew_and_slurp(TestBatchRunner *runner) { |
| Obj *dump = S_make_dump(); |
| Folder *folder = (Folder*)RAMFolder_new(NULL); |
| |
| String *foo = SSTR_WRAP_C("foo"); |
| bool result = Json_spew_json(dump, folder, foo); |
| TEST_TRUE(runner, result, "spew_json returns true on success"); |
| TEST_TRUE(runner, Folder_Exists(folder, foo), |
| "spew_json wrote file"); |
| |
| Obj *got = Json_slurp_json(folder, foo); |
| TEST_TRUE(runner, got && Obj_Equals(dump, got), |
| "Round trip through spew_json and slurp_json"); |
| DECREF(got); |
| |
| Err_set_error(NULL); |
| result = Json_spew_json(dump, folder, foo); |
| TEST_FALSE(runner, result, "Can't spew_json when file exists"); |
| TEST_TRUE(runner, Err_get_error() != NULL, |
| "Failed spew_json sets global error"); |
| |
| Err_set_error(NULL); |
| String *bar = SSTR_WRAP_C("bar"); |
| got = Json_slurp_json(folder, bar); |
| TEST_TRUE(runner, got == NULL, |
| "slurp_json returns NULL when file doesn't exist"); |
| TEST_TRUE(runner, Err_get_error() != NULL, |
| "Failed slurp_json sets global error"); |
| |
| String *boffo = SSTR_WRAP_C("boffo"); |
| |
| FileHandle *fh |
| = Folder_Open_FileHandle(folder, boffo, FH_CREATE | FH_WRITE_ONLY); |
| FH_Write(fh, "garbage", 7); |
| DECREF(fh); |
| |
| Err_set_error(NULL); |
| got = Json_slurp_json(folder, boffo); |
| TEST_TRUE(runner, got == NULL, |
| "slurp_json returns NULL when file doesn't contain valid JSON"); |
| TEST_TRUE(runner, Err_get_error() != NULL, |
| "Failed slurp_json sets global error"); |
| DECREF(got); |
| |
| DECREF(dump); |
| DECREF(folder); |
| } |
| |
| static void |
| S_verify_bad_syntax(TestBatchRunner *runner, const char *bad, const char *mess) { |
| String *has_errors = SSTR_WRAP_C(bad); |
| Err_set_error(NULL); |
| Obj *not_json = Json_from_json(has_errors); |
| TEST_TRUE(runner, not_json == NULL, "from_json returns NULL: %s", mess); |
| TEST_TRUE(runner, Err_get_error() != NULL, |
| "from_json sets global error: %s", mess); |
| } |
| |
| static void |
| test_syntax_errors(TestBatchRunner *runner) { |
| S_verify_bad_syntax(runner, "[", "unclosed left bracket"); |
| S_verify_bad_syntax(runner, "]", "unopened right bracket"); |
| S_verify_bad_syntax(runner, "{", "unclosed left curly"); |
| S_verify_bad_syntax(runner, "}", "unopened right curly"); |
| S_verify_bad_syntax(runner, "{}[]", "two top-level objects"); |
| S_verify_bad_syntax(runner, "[1 \"foo\"]", "missing comma in array"); |
| S_verify_bad_syntax(runner, "[1, \"foo\",]", "extra comma in array"); |
| S_verify_bad_syntax(runner, "{\"1\":1 \"2\":2}", "missing comma in hash"); |
| S_verify_bad_syntax(runner, "{\"1\":1,\"2\":2,}", "extra comma in hash"); |
| S_verify_bad_syntax(runner, "\"1", "unterminated string"); |
| // Tolerated by strtod(). |
| // S_verify_bad_syntax(runner, "1. ", "float missing fraction"); |
| // S_verify_bad_syntax(runner, "-.3 ", "Number missing integral part"); |
| S_verify_bad_syntax(runner, "-. ", "Number missing any digits"); |
| S_verify_bad_syntax(runner, "+1.0 ", "float with prepended plus"); |
| S_verify_bad_syntax(runner, "\"\\g\"", "invalid char escape"); |
| S_verify_bad_syntax(runner, "\"\\uAAAZ\"", "invalid \\u escape"); |
| } |
| |
| static void |
| S_round_trip_integer(TestBatchRunner *runner, int64_t value) { |
| Integer *num = Int_new(value); |
| Vector *array = Vec_new(1); |
| Vec_Store(array, 0, (Obj*)num); |
| String *json = Json_to_json((Obj*)array); |
| Obj *dump = Json_from_json(json); |
| TEST_TRUE(runner, Vec_Equals(array, dump), "Round trip integer %ld", |
| (long)value); |
| DECREF(dump); |
| DECREF(json); |
| DECREF(array); |
| } |
| |
| static void |
| test_integers(TestBatchRunner *runner) { |
| S_round_trip_integer(runner, 0); |
| S_round_trip_integer(runner, -1); |
| S_round_trip_integer(runner, -1000000); |
| S_round_trip_integer(runner, 1000000); |
| } |
| |
| static void |
| S_round_trip_float(TestBatchRunner *runner, double value, double max_diff) { |
| Float *num = Float_new(value); |
| Vector *array = Vec_new(1); |
| Vec_Store(array, 0, (Obj*)num); |
| String *json = Json_to_json((Obj*)array); |
| Obj *dump = CERTIFY(Json_from_json(json), VECTOR); |
| Float *got = (Float*)CERTIFY(Vec_Fetch((Vector*)dump, 0), FLOAT); |
| double diff = Float_Get_Value(num) - Float_Get_Value(got); |
| if (diff < 0) { diff = 0 - diff; } |
| TEST_TRUE(runner, diff <= max_diff, "Round trip float %f", value); |
| DECREF(dump); |
| DECREF(json); |
| DECREF(array); |
| } |
| |
| static void |
| test_floats(TestBatchRunner *runner) { |
| S_round_trip_float(runner, 0.0, 0.0); |
| S_round_trip_float(runner, 0.1, 0.00001); |
| S_round_trip_float(runner, -0.1, 0.00001); |
| S_round_trip_float(runner, 1000000.5, 1.0); |
| S_round_trip_float(runner, -1000000.5, 1.0); |
| } |
| |
| static void |
| test_max_depth(TestBatchRunner *runner) { |
| Hash *circular = Hash_new(0); |
| Hash_Store_Utf8(circular, "circular", 8, INCREF(circular)); |
| Err_set_error(NULL); |
| String *not_json = Json_to_json((Obj*)circular); |
| TEST_TRUE(runner, not_json == NULL, |
| "to_json returns NULL when fed recursing data"); |
| TEST_TRUE(runner, Err_get_error() != NULL, |
| "to_json sets global error when fed recursing data"); |
| DECREF(Hash_Delete_Utf8(circular, "circular", 8)); |
| DECREF(circular); |
| } |
| |
| void |
| TestJson_Run_IMP(TestJson *self, TestBatchRunner *runner) { |
| uint32_t num_tests = 105; |
| #ifndef LUCY_VALGRIND |
| num_tests += 28; // FIXME: syntax errors leak memory. |
| #endif |
| TestBatchRunner_Plan(runner, (TestBatch*)self, num_tests); |
| |
| // Test tolerance, then liberalize for testing. |
| test_tolerance(runner); |
| Json_set_tolerant(true); |
| |
| test_to_and_from(runner); |
| test_escapes(runner); |
| test_numbers(runner); |
| test_spew_and_slurp(runner); |
| test_integers(runner); |
| test_floats(runner); |
| test_max_depth(runner); |
| |
| #ifndef LUCY_VALGRIND |
| test_syntax_errors(runner); |
| #endif |
| } |
| |