blob: c67e29232a83efb69046b760637b7449f3476e99 [file] [log] [blame]
/* 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
}