Fri, 23 May 2025 12:44:24 +0200
make test-compile depend on both static and shared
the shared lib is not needed for the tests,
but when run with coverage, gcov will be confused
when outdated line information is available from
a previous shared build
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2024 Mike Becker, Olaf Wintermann All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "util_allocator.h" #include "cx/test.h" #include "cx/json.h" #include "cx/compare.h" CX_TEST(test_json_init_default) { CxJson json; CX_TEST_DO { cxJsonInit(&json, NULL); CX_TEST_ASSERT(json.states == json.states_internal); CX_TEST_ASSERT(json.states_size == 1); CX_TEST_ASSERT(json.states_capacity >= 8); CX_TEST_ASSERT(json.vbuf == json.vbuf_internal); CX_TEST_ASSERT(json.vbuf_size == 0); CX_TEST_ASSERT(json.vbuf_capacity >= 8); cxJsonDestroy(&json); } } CX_TEST(test_json_simple_object) { cxstring text = cx_str( "{\n" "\t\"message\":\"success\",\n" "\t\"position\":{\n" "\t\t\"longitude\":-94.7099,\n" "\t\t\"latitude\":51.5539\n" "\t},\n" "\t\"timestamp\":1729348561,\n" "\t\"alive\":true\n" "}" ); CX_TEST_DO { CxJsonStatus result; CxJson json; cxJsonInit(&json, NULL); cxJsonFill(&json, text); // parse the big fat object CxJsonValue *obj; result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); // check the contents CX_TEST_ASSERT(cxJsonIsObject(obj)); CxJsonValue *message = cxJsonObjGet(obj, "message"); CX_TEST_ASSERT(cxJsonIsString(message)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(message), cx_str("success")) ); CxJsonValue *position = cxJsonObjGet(obj, "position"); CX_TEST_ASSERT(cxJsonIsObject(position)); CxJsonValue *longitude = cxJsonObjGet(position, "longitude"); CX_TEST_ASSERT(cxJsonIsNumber(longitude)); CX_TEST_ASSERT(!cxJsonIsInteger(longitude)); CX_TEST_ASSERT(0 == cx_vcmp_double(cxJsonAsDouble(longitude), -94.7099)); CX_TEST_ASSERT(cxJsonAsInteger(longitude) == -94); CxJsonValue *latitude = cxJsonObjGet(position, "latitude"); CX_TEST_ASSERT(cxJsonIsNumber(latitude)); CX_TEST_ASSERT(!cxJsonIsInteger(latitude)); CX_TEST_ASSERT(0 == cx_vcmp_double(cxJsonAsDouble(latitude), 51.5539)); CX_TEST_ASSERT(cxJsonAsInteger(latitude) == 51); CxJsonValue *timestamp = cxJsonObjGet(obj, "timestamp"); CX_TEST_ASSERT(cxJsonIsInteger(timestamp)); CX_TEST_ASSERT(cxJsonIsNumber(timestamp)); CX_TEST_ASSERT(cxJsonAsInteger(timestamp) == 1729348561); CX_TEST_ASSERT(cxJsonAsDouble(timestamp) == 1729348561.0); CxJsonValue *alive = cxJsonObjGet(obj, "alive"); CX_TEST_ASSERT(cxJsonIsBool(alive)); CX_TEST_ASSERT(cxJsonIsTrue(alive)); CX_TEST_ASSERT(!cxJsonIsFalse(alive)); CX_TEST_ASSERT(cxJsonAsBool(alive)); // this recursively frees everything else cxJsonValueFree(obj); // we only have one object that already contained all the data result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_DATA); cxJsonDestroy(&json); } } CX_TEST(test_json_escaped_strings) { cxstring text = cx_str( "{\n" "\t\"object\":\"{\\n\\t\\\"object\\\":null\\n}\",\n" "\t\"ctrl-chars\":\"\\\\foo\\r\\nbar\\f*ring\\/ring*\\b\"\n" "}" ); CxJson json; cxJsonInit(&json, NULL); CX_TEST_DO { cxJsonFill(&json, text); CxJsonValue *obj; CxJsonStatus result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsObject(obj)); CxJsonValue *object = cxJsonObjGet(obj, "object"); CX_TEST_ASSERT(cxJsonIsString(object)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(object), CX_STR("{\n\t\"object\":null\n}")) ); CxJsonValue *ctrl = cxJsonObjGet(obj, "ctrl-chars"); CX_TEST_ASSERT(cxJsonIsString(ctrl)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(ctrl), CX_STR("\\foo\r\nbar\f*ring/ring*\b")) ); cxJsonValueFree(obj); } cxJsonDestroy(&json); } CX_TEST(test_json_escaped_unicode_strings) { cxstring text = cx_str( "{\n" "\"ascii\":\"\\u0041\\u0053\\u0043\\u0049\\u0049\",\n" "\"unicode\":\"\\u00df\\u00DF\",\n" "\"mixed\":\"mixed ä ö \\u00e4 \\u00f6\",\n" "\"wide\":\"\\u03a3\\u29b0\",\n" "\"surrogatepair1\":\"\\ud83e\\udff5\",\n" "\"surrogatepair2\":\"test\\ud83e\\udff1AA\"\n," "\"mixed2\":\"123\\u03a3\\ud83e\\udfc5\\u00df\"" "}" ); CxJson json; cxJsonInit(&json, NULL); CX_TEST_DO { cxJsonFill(&json, text); CxJsonValue *obj; CxJsonStatus result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsObject(obj)); CxJsonValue *ascii = cxJsonObjGet(obj, "ascii"); CX_TEST_ASSERT(cxJsonIsString(ascii)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(ascii), CX_STR("ASCII")) ); CxJsonValue *unicode = cxJsonObjGet(obj, "unicode"); CX_TEST_ASSERT(cxJsonIsString(unicode)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(unicode), CX_STR("ßß")) ); CxJsonValue *mixed = cxJsonObjGet(obj, "mixed"); CX_TEST_ASSERT(cxJsonIsString(mixed)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(mixed), CX_STR("mixed ä ö ä ö")) ); CxJsonValue *wide = cxJsonObjGet(obj, "wide"); CX_TEST_ASSERT(cxJsonIsString(wide)); CX_TEST_ASSERT(0 == cx_strcmp(cxJsonAsCxString(wide), CX_STR("Σ⦰"))); CxJsonValue *surrogatepair1 = cxJsonObjGet(obj, "surrogatepair1"); CX_TEST_ASSERT(cxJsonIsString(surrogatepair1)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(surrogatepair1), CX_STR("\xf0\x9f\xaf\xb5")) ); CxJsonValue *surrogatepair2 = cxJsonObjGet(obj, "surrogatepair2"); CX_TEST_ASSERT(cxJsonIsString(surrogatepair2)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(surrogatepair2), CX_STR("test\xf0\x9f\xaf\xb1" "AA")) ); CxJsonValue *mixed2 = cxJsonObjGet(obj, "mixed2"); char test[16]; strncpy(test, mixed2->value.string.ptr, 15); CX_TEST_ASSERT(cxJsonIsString(mixed2)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(mixed2), CX_STR("123\xce\xa3\xf0\x9f\xaf\x85ß")) ); cxJsonValueFree(obj); } cxJsonDestroy(&json); } CX_TEST(test_json_escaped_unicode_malformed) { CxJson json; cxJsonInit(&json, NULL); CxJsonValue *obj; CxJsonStatus result; CX_TEST_DO { cxJsonFill(&json, "\"too few \\u123 digits\""); result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsString(obj)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(obj), CX_STR("too few \\u123 digits") )); cxJsonValueFree(obj); cxJsonFill(&json, "\"too many \\u00E456 digits\""); result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsString(obj)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(obj), CX_STR("too many ä56 digits") )); cxJsonValueFree(obj); cxJsonFill(&json, "\"only high \\uD800 surrogate\""); result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsString(obj)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(obj), CX_STR("only high \\uD800 surrogate") )); cxJsonValueFree(obj); cxJsonFill(&json, "\"only low \\uDC00 surrogate\""); result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsString(obj)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(obj), CX_STR("only low \\uDC00 surrogate") )); cxJsonValueFree(obj); cxJsonFill(&json, "\"two high \\uD800\\uD800 surrogates\""); result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsString(obj)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(obj), CX_STR("two high \\uD800\\uD800 surrogates") )); cxJsonValueFree(obj); cxJsonFill(&json, "\"high plus bullshit \\uD800\\u567 foo\""); result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsString(obj)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(obj), CX_STR("high plus bullshit \\uD800\\u567 foo") )); cxJsonValueFree(obj); } cxJsonDestroy(&json); } CX_TEST(test_json_escaped_end_of_string) { CxJson json; cxJsonInit(&json, NULL); CX_TEST_DO { // first test, normal scenario cxJsonFill(&json, "\"a \\\"test\\\" string\""); CxJsonValue *val; CxJsonStatus result = cxJsonNext(&json, &val); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsString(val)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(val), cx_str("a \"test\" string")) ); cxJsonValueFree(val); // second test - uncompleted token with hanging escape char cxJsonFill(&json, "\"a \\\"test\\"); result = cxJsonNext(&json, &val); CX_TEST_ASSERT(result == CX_JSON_INCOMPLETE_DATA); cxJsonFill(&json, "\" string\""); result = cxJsonNext(&json, &val); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsString(val)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(val), cx_str("a \"test\" string")) ); cxJsonValueFree(val); } cxJsonDestroy(&json); } CX_TEST(test_json_object_incomplete_token) { cxstring text = cx_str( "{\"message\":\"success\" , \"__timestamp\":1729348561}"); cxstring parts[16]; size_t nparts = 0; // split the json text into mulple parts for(size_t i=0;i<text.length;i+=4) { parts[nparts++] = cx_strsubsl(text, i, 4); } CX_TEST_DO { CxJsonStatus result; CxJson json; cxJsonInit(&json, NULL); CxJsonValue *obj; size_t part = 0; while(part < nparts - 1) { cxJsonFill(&json, parts[part]); part++; result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_INCOMPLETE_DATA); } cxJsonFill(&json, parts[nparts - 1]); result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsObject(obj)); CxJsonValue *message = cxJsonObjGet(obj, "message"); CX_TEST_ASSERT(cxJsonIsString(message)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(message), cx_str("success")) ); CxJsonValue *timestamp = cxJsonObjGet(obj, "__timestamp"); CX_TEST_ASSERT(message->type == CX_JSON_STRING); CX_TEST_ASSERT(cxJsonIsInteger(timestamp)); CX_TEST_ASSERT(cxJsonAsInteger(timestamp) == 1729348561); // this recursively frees everything else cxJsonValueFree(obj); // now there is everything read result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_DATA); cxJsonDestroy(&json); } } CX_TEST(test_json_token_wrongly_completed) { CxTestingAllocator talloc; cx_testing_allocator_init(&talloc); const CxAllocator *alloc = &talloc.base; cxstring text = cx_str("{\"number\": 47110815!}"); cxstring part1 = cx_strsubsl(text, 0, 16); cxstring part2 = cx_strsubs(text, 16); CX_TEST_DO { CxJson json; cxJsonInit(&json, alloc); CxJsonStatus result; CxJsonValue *obj; cxJsonFill(&json, part1); result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_INCOMPLETE_DATA); cxJsonFill(&json, part2); result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_FORMAT_ERROR_NUMBER); CX_TEST_ASSERT(obj->type == CX_JSON_NOTHING); cxJsonDestroy(&json); CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); } cx_testing_allocator_destroy(&talloc); } CX_TEST(test_json_subsequent_fill) { cxstring text = cx_str( "{\"message\":\"success\" , \"__timestamp\":1729348561}"); cxstring part1 = cx_strsubsl(text, 0, 25); cxstring part2 = cx_strsubs(text, 25); CX_TEST_DO { CxJson json; cxJsonInit(&json, NULL); CxJsonValue *obj; cxJsonFill(&json, part1); cxJsonFill(&json, part2); CxJsonStatus result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsObject(obj)); CxJsonValue *message = cxJsonObjGet(obj, "message"); CX_TEST_ASSERT(cxJsonIsString(message)); CX_TEST_ASSERT(0 == cx_strcmp( cxJsonAsCxString(message), cx_str("success")) ); CxJsonValue *timestamp = cxJsonObjGet(obj, "__timestamp"); CX_TEST_ASSERT(message->type == CX_JSON_STRING); CX_TEST_ASSERT(cxJsonIsInteger(timestamp)); CX_TEST_ASSERT(cxJsonAsInteger(timestamp) == 1729348561); cxJsonValueFree(obj); result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_DATA); cxJsonDestroy(&json); } } CX_TEST(test_json_object_error) { cxstring text0 = cx_str( "{\n" "\t\"message\":\"success\",\n" "\t\"data\":{\n" "\t\t\"obj\":{\n" "\t\t\t\"array\": [1, 2, 3, ?syntaxerror? ]\n" "\t\t\"}\n" "\t},\n" "\t\"timestamp\":1729348561,\n" "}" ); cxstring text1 = cx_str("{ \"string\" }"); cxstring text2 = cx_str("{ \"a\" : }"); cxstring text3 = cx_str("{ \"a\" : \"b\" ]"); cxstring text4 = cx_str("{ \"name\": \"value\" ]"); cxstring tests[] = { text0, text1, text2, text3, text4 }; CxJsonStatus errors[] = { CX_JSON_FORMAT_ERROR_NUMBER, CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN, CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN, CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN, CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN }; CX_TEST_DO { CxJsonStatus result; CxJson json; CxJsonValue *obj = NULL; for(int i=0;i<5;i++) { cxJsonInit(&json, NULL); cxJsonFill(&json, tests[i]); result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == errors[i]); CX_TEST_ASSERT(obj != NULL && obj->type == CX_JSON_NOTHING); cxJsonDestroy(&json); } } } CX_TEST(test_json_large_nesting_depth) { CxJson json; CxJsonValue *d1; cxstring text = cx_str("{\"test\": [{},{\"foo\": [[{\"bar\":[4, 2, [null, {\"key\": 47}]]}]]}]}"); CX_TEST_DO { cxJsonInit(&json, NULL); cxJsonFill(&json, text); cxJsonNext(&json, &d1); CX_TEST_ASSERT(d1 != NULL); CX_TEST_ASSERT(cxJsonIsObject(d1)); CxJsonValue *d2 = cxJsonObjGet(d1, "test"); CX_TEST_ASSERT(cxJsonIsArray(d2)); CX_TEST_ASSERT(cxJsonArrSize(d2) == 2); CxJsonValue *d3 = cxJsonArrGet(d2, 1); CX_TEST_ASSERT(cxJsonIsObject(d3)); CxJsonValue *d4 = cxJsonObjGet(d3, "foo"); CX_TEST_ASSERT(cxJsonIsArray(d4)); CX_TEST_ASSERT(cxJsonArrSize(d4) == 1); CxJsonValue *d5 = cxJsonArrGet(d4, 0); CX_TEST_ASSERT(cxJsonIsArray(d5)); CX_TEST_ASSERT(cxJsonArrSize(d5) == 1); CxJsonValue *d6 = cxJsonArrGet(d5, 0); CX_TEST_ASSERT(cxJsonIsObject(d6)); CxJsonValue *d7 = cxJsonObjGet(d6, "bar"); CX_TEST_ASSERT(cxJsonIsArray(d7)); CX_TEST_ASSERT(cxJsonArrSize(d7) == 3); CxJsonValue *d8 = cxJsonArrGet(d7, 2); CX_TEST_ASSERT(cxJsonIsArray(d8)); CX_TEST_ASSERT(cxJsonArrSize(d8) == 2); CxJsonValue *d9a = cxJsonArrGet(d8, 0); CX_TEST_ASSERT(cxJsonIsNull(d9a)); CxJsonValue *d9b = cxJsonArrGet(d8, 1); CX_TEST_ASSERT(cxJsonIsObject(d9b)); CxJsonValue *d10 = cxJsonObjGet(d9b, "key"); CX_TEST_ASSERT(cxJsonIsInteger(d10)); CX_TEST_ASSERT(cxJsonAsInteger(d10) == 47); CX_TEST_ASSERT(json.states != json.states_internal); CX_TEST_ASSERT(json.states_capacity > cx_nmemb(json.states_internal)); cxJsonValueFree(d1); cxJsonDestroy(&json); } } CX_TEST(test_json_number) { CxJson json; cxJsonInit(&json, NULL); CX_TEST_DO { CxJsonValue *v; CxJsonStatus result; cxJsonFill(&json, "3.1415 "); result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsNumber(v)); CX_TEST_ASSERT(0 == cx_vcmp_double(cxJsonAsDouble(v), 3.1415)); cxJsonValueFree(v); cxJsonFill(&json, "-47.11e2 "); result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsNumber(v)); CX_TEST_ASSERT(0 == cx_vcmp_double(cxJsonAsDouble(v), -4711.0)); cxJsonValueFree(v); cxJsonFill(&json, "0.815e-3 "); result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsNumber(v)); CX_TEST_ASSERT(0 == cx_vcmp_double(cxJsonAsDouble(v), 0.000815)); cxJsonValueFree(v); cxJsonFill(&json, "1.23E4 "); result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsNumber(v)); CX_TEST_ASSERT(cxJsonAsInteger(v) == 12300); CX_TEST_ASSERT(cxJsonAsDouble(v) == 12300.0); cxJsonValueFree(v); cxJsonFill(&json, "18446744073709551615.0123456789 "); result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsNumber(v)); // be as precise as possible // TODO: this might produce format error / out of range in future implementations CX_TEST_ASSERT(0 == cx_vcmp_double(cxJsonAsDouble(v), 1.8446744073709552e+19)); cxJsonValueFree(v); } cxJsonDestroy(&json); } CX_TEST(test_json_number_format_errors) { CxJson json; cxJsonInit(&json, NULL); CX_TEST_DO { CxJsonValue *v; CxJsonStatus result; cxJsonFill(&json, "+3.1415 "); result = cxJsonNext(&json, &v); CX_TEST_ASSERTM(result == CX_JSON_FORMAT_ERROR_NUMBER, "leading plus is not RFC-8259 compliant"); CX_TEST_ASSERT(v->type == CX_JSON_NOTHING); cxJsonReset(&json); cxJsonFill(&json, "0.815e-3.0 "); result = cxJsonNext(&json, &v); CX_TEST_ASSERTM(result == CX_JSON_FORMAT_ERROR_NUMBER, "exponent must be an integer"); CX_TEST_ASSERT(v->type == CX_JSON_NOTHING); cxJsonReset(&json); cxJsonFill(&json, "3.14e "); result = cxJsonNext(&json, &v); CX_TEST_ASSERTM(result == CX_JSON_FORMAT_ERROR_NUMBER, "exponent cannot be empty"); CX_TEST_ASSERT(v->type == CX_JSON_NOTHING); cxJsonReset(&json); cxJsonFill(&json, "3.14e~7 "); result = cxJsonNext(&json, &v); CX_TEST_ASSERTM(result == CX_JSON_FORMAT_ERROR_NUMBER, "exponent cannot start with bullshit"); CX_TEST_ASSERT(v->type == CX_JSON_NOTHING); cxJsonReset(&json); cxJsonFill(&json, "1.23e4f "); result = cxJsonNext(&json, &v); CX_TEST_ASSERTM(result == CX_JSON_FORMAT_ERROR_NUMBER, "non-digits in exponent"); CX_TEST_ASSERT(v->type == CX_JSON_NOTHING); cxJsonReset(&json); cxJsonFill(&json, "1.23f "); result = cxJsonNext(&json, &v); CX_TEST_ASSERTM(result == CX_JSON_FORMAT_ERROR_NUMBER, "non-digits in value"); CX_TEST_ASSERT(v->type == CX_JSON_NOTHING); cxJsonReset(&json); cxJsonFill(&json, "1.23.45 "); result = cxJsonNext(&json, &v); CX_TEST_ASSERTM(result == CX_JSON_FORMAT_ERROR_NUMBER, "multiple decimal separators"); CX_TEST_ASSERT(v->type == CX_JSON_NOTHING); cxJsonReset(&json); cxJsonFill(&json, "184467440737095516150123456789 "); result = cxJsonNext(&json, &v); CX_TEST_ASSERTM(result == CX_JSON_FORMAT_ERROR_NUMBER, "30 digit int does not fit into 64-bit int"); CX_TEST_ASSERT(v->type == CX_JSON_NOTHING); cxJsonReset(&json); } cxJsonDestroy(&json); } CX_TEST(test_json_multiple_values) { CxJson json; cxJsonInit(&json, NULL); CX_TEST_DO { CxJsonValue *v; CxJsonStatus result; // read number cxJsonFill(&json, "10\n"); result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsNumber(v)); CX_TEST_ASSERT(cxJsonAsInteger(v) == 10); cxJsonValueFree(v); // read remaining '\n' result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_INCOMPLETE_DATA); // read string cxJsonFill(&json, "\"hello world\"\n"); result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsString(v)); CX_TEST_ASSERT(!cx_strcmp(cxJsonAsCxString(v), CX_STR("hello world"))); cxJsonValueFree(v); // don't process the remaining newline this time // read obj cxJsonFill(&json, "{ \"value\": \"test\" }\n"); result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsObject(v)); CxJsonValue *value = cxJsonObjGet(v, "value"); CX_TEST_ASSERT(cxJsonAsString(value)); cxJsonValueFree(v); // read array cxJsonFill(&json, "[ 0, 1, 2, 3, 4, 5 ]\n"); result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsArray(v)); CxJsonValue *a0 = cxJsonArrGet(v, 0); CxJsonValue *a3 = cxJsonArrGet(v, 3); CX_TEST_ASSERT(cxJsonIsNumber(a0)); CX_TEST_ASSERT(cxJsonAsInteger(a0) == 0); CX_TEST_ASSERT(cxJsonIsNumber(a3)); CX_TEST_ASSERT(cxJsonAsInteger(a3) == 3); cxJsonValueFree(v); // read literal cxJsonFill(&json, "true\n"); result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsLiteral(v)); CX_TEST_ASSERT(cxJsonIsBool(v)); CX_TEST_ASSERT(cxJsonIsTrue(v)); CX_TEST_ASSERT(cxJsonAsBool(v)); cxJsonValueFree(v); cxJsonFill(&json, "false\n"); result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsLiteral(v)); CX_TEST_ASSERT(cxJsonIsBool(v)); CX_TEST_ASSERT(cxJsonIsFalse(v)); CX_TEST_ASSERT(!cxJsonAsBool(v)); cxJsonValueFree(v); cxJsonFill(&json, "null\n"); result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsLiteral(v)); CX_TEST_ASSERT(!cxJsonIsBool(v)); CX_TEST_ASSERT(cxJsonIsNull(v)); cxJsonValueFree(v); } cxJsonDestroy(&json); } CX_TEST(test_json_array_iterator) { CxJson json; cxJsonInit(&json, NULL); CX_TEST_DO { CxJsonValue *v; CxJsonStatus result; cxJsonFill(&json, "[ 0, 3, 6, 9, 12, 15 ]\n"); result = cxJsonNext(&json, &v); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(cxJsonIsArray(v)); CxIterator iter = cxJsonArrIter(v); unsigned i = 0; cx_foreach(CxJsonValue*, elem, iter) { CX_TEST_ASSERT(cxJsonIsNumber(elem)); CX_TEST_ASSERT(i == cxJsonAsInteger(elem)); i += 3; } cxJsonValueFree(v); } cxJsonDestroy(&json); } CX_TEST(test_json_allocator) { CxTestingAllocator talloc; cx_testing_allocator_init(&talloc); CxAllocator *allocator = &talloc.base; cxstring text = cx_str( "{\n" "\t\"message\":\"success\",\n" "\t\"data\":[\"value1\",{\"x\":123, \"y\":523 }]\n" "}" ); CX_TEST_DO { CxJson json; cxJsonInit(&json, allocator); cxJsonFill(&json, text); CxJsonValue *obj; CxJsonStatus result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_NO_ERROR); CX_TEST_ASSERT(obj->allocator == allocator); // this recursively frees everything cxJsonValueFree(obj); cxJsonDestroy(&json); CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); } cx_testing_allocator_destroy(&talloc); } CX_TEST(test_json_allocator_parse_error) { CxTestingAllocator talloc; cx_testing_allocator_init(&talloc); CxAllocator *allocator = &talloc.base; cxstring text = cx_str( "{\n" "\t\"message\":\"success\"\n" // <-- missing comma "\t\"data\":[\"value1\",{\"x\":123, \"y\":523 }]\n" "}" ); CX_TEST_DO { CxJson json; cxJsonInit(&json, allocator); cxJsonFill(&json, text); CxJsonValue *obj = NULL; CxJsonStatus result = cxJsonNext(&json, &obj); CX_TEST_ASSERT(result == CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN); CX_TEST_ASSERT(obj != NULL && obj->type == CX_JSON_NOTHING); // clean-up any left-over memory cxJsonDestroy(&json); CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); } cx_testing_allocator_destroy(&talloc); } CX_TEST(test_json_create_value) { CxTestingAllocator talloc; cx_testing_allocator_init(&talloc); CxAllocator *allocator = &talloc.base; CX_TEST_DO { /* * This is the value we want to create in this test: * * { * "bool": false, * "int": 47, * "strings": [ "hello", "world" ], * "nested": { * "string": "test", * "floats": [ 3.1415, 47.11, 8.15 ], * "ints": [ 4, 8, 15, 16, 23, 42 ], * "literals": [ true, null, false ] * } * } */ // create the object CxJsonValue *obj = cxJsonCreateObj(allocator); CX_TEST_ASSERT(obj != NULL); CX_TEST_ASSERT(cxJsonIsObject(obj)); CX_TEST_ASSERT(obj->allocator == allocator); // add the members { cxJsonObjPutLiteral(obj, CX_STR("bool"), CX_JSON_FALSE); cxJsonObjPutInteger(obj, CX_STR("int"), 47); CxJsonValue *strings = cxJsonObjPutArr(obj, CX_STR("strings")); CX_TEST_ASSERT(strings != NULL); CX_TEST_ASSERT(cxJsonIsArray(strings)); const char* str[] = {"hello", "world"}; CX_TEST_ASSERT(0 == cxJsonArrAddStrings(strings, str, 2)); CxJsonValue *nested = cxJsonObjPutObj(obj, CX_STR("nested")); CX_TEST_ASSERT(nested != NULL); CX_TEST_ASSERT(cxJsonIsObject(nested)); cxJsonObjPutCxString(nested, CX_STR("string"), CX_STR("test")); cxJsonArrAddNumbers(cxJsonObjPutArr(nested, CX_STR("floats")), (double[]){3.1415, 47.11, 8.15}, 3); cxJsonArrAddIntegers(cxJsonObjPutArr(nested, CX_STR("ints")), (int64_t[]){4, 8, 15, 16, 23, 42}, 6); cxJsonArrAddLiterals(cxJsonObjPutArr(nested, CX_STR("literals")), (CxJsonLiteral[]){CX_JSON_TRUE, CX_JSON_NULL, CX_JSON_FALSE}, 3); } // verify the contents { CX_TEST_ASSERT(cxJsonIsFalse(cxJsonObjGet(obj, "bool"))); CX_TEST_ASSERT(47 == cxJsonAsInteger(cxJsonObjGet(obj, "int"))); CxJsonValue *strings = cxJsonObjGet(obj, "strings"); CX_TEST_ASSERT(cxJsonIsArray(strings)); CX_TEST_ASSERT(2 == cxJsonArrSize(strings)); CX_TEST_ASSERT(0 == cx_strcmp(CX_STR("hello"), cxJsonAsCxString(cxJsonArrGet(strings, 0)))); CX_TEST_ASSERT(0 == cx_strcmp(CX_STR("world"), cxJsonAsCxString(cxJsonArrGet(strings, 1)))); CxJsonValue *nested = cxJsonObjGet(obj, "nested"); CX_TEST_ASSERT(cxJsonIsObject(nested)); CX_TEST_ASSERT(0 == strcmp("test", cxJsonAsString(cxJsonObjGet(nested, "string")))); CxJsonValue *floats = cxJsonObjGet(nested, "floats"); CX_TEST_ASSERT(cxJsonIsArray(floats)); CX_TEST_ASSERT(3 == cxJsonArrSize(floats)); CX_TEST_ASSERT(3.1415 == cxJsonAsDouble(cxJsonArrGet(floats, 0))); CX_TEST_ASSERT(47.11 == cxJsonAsDouble(cxJsonArrGet(floats, 1))); CX_TEST_ASSERT(8.15 == cxJsonAsDouble(cxJsonArrGet(floats, 2))); CxJsonValue *ints = cxJsonObjGet(nested, "ints"); CX_TEST_ASSERT(cxJsonIsArray(ints)); CX_TEST_ASSERT(6 == cxJsonArrSize(ints)); CX_TEST_ASSERT(4 == cxJsonAsInteger(cxJsonArrGet(ints, 0))); CX_TEST_ASSERT(8 == cxJsonAsInteger(cxJsonArrGet(ints, 1))); CX_TEST_ASSERT(15 == cxJsonAsInteger(cxJsonArrGet(ints, 2))); CX_TEST_ASSERT(16 == cxJsonAsInteger(cxJsonArrGet(ints, 3))); CX_TEST_ASSERT(23 == cxJsonAsInteger(cxJsonArrGet(ints, 4))); CX_TEST_ASSERT(42 == cxJsonAsInteger(cxJsonArrGet(ints, 5))); CxJsonValue *literals = cxJsonObjGet(nested, "literals"); CX_TEST_ASSERT(cxJsonIsArray(literals)); CX_TEST_ASSERT(3 == cxJsonArrSize(literals)); CX_TEST_ASSERT(cxJsonIsTrue(cxJsonArrGet(literals, 0))); CX_TEST_ASSERT(cxJsonIsNull(cxJsonArrGet(literals, 1))); CX_TEST_ASSERT(cxJsonIsFalse(cxJsonArrGet(literals, 2))); } // destroy the value and verify the allocations cxJsonValueFree(obj); CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); } cx_testing_allocator_destroy(&talloc); } CX_TEST_SUBROUTINE(test_json_write_sub, const CxAllocator *allocator, cxstring expected, const CxJsonWriter *writer ) { // create the value CxJsonValue *obj = cxJsonCreateObj(allocator); cxJsonObjPutLiteral(obj, CX_STR("bool"), CX_JSON_FALSE); cxJsonObjPutNumber(obj, CX_STR("int"), 47); // purposely use PutNumber to put an int CxJsonValue *strings = cxJsonObjPutArr(obj, CX_STR("strings")); cxJsonArrAddCxStrings(strings, (cxstring[]) {CX_STR("hello"), CX_STR("world")}, 2); CxJsonValue *nested = cxJsonObjPutObj(obj, CX_STR("nested")); CxJsonValue *objects = cxJsonObjPutArr(nested, CX_STR("objects")); CxJsonValue *obj_in_arr[2] = {cxJsonCreateObj(allocator), cxJsonCreateObj(allocator)}; cxJsonObjPutInteger(obj_in_arr[0], CX_STR("name1"), 1); cxJsonObjPutInteger(obj_in_arr[0], CX_STR("name2"), 3); cxJsonObjPutInteger(obj_in_arr[1], CX_STR("name2"), 7); cxJsonObjPutInteger(obj_in_arr[1], CX_STR("name1"), 3); cxJsonArrAddValues(objects, obj_in_arr, 2); cxJsonArrAddNumbers(cxJsonObjPutArr(nested, CX_STR("floats")), (double[]){3.1415, 47.11, 8.15}, 3); cxJsonArrAddLiterals(cxJsonObjPutArr(nested, CX_STR("literals")), (CxJsonLiteral[]){CX_JSON_TRUE, CX_JSON_NULL, CX_JSON_FALSE}, 3); CxJsonValue *ints = cxJsonObjPutArr(nested, CX_STR("ints")); cxJsonArrAddIntegers(ints, (int64_t[]){4, 8, 15}, 3); CxJsonValue *nested_array = cxJsonCreateArr(allocator); cxJsonArrAddValues(ints, &nested_array, 1); cxJsonArrAddIntegers(nested_array, (int64_t[]){16, 23}, 2); cxJsonArrAddIntegers(ints, (int64_t[]){42}, 1); // write it to a buffer CxBuffer buf; cxBufferInit(&buf, NULL, 512, NULL, CX_BUFFER_DEFAULT); int result = cxJsonWrite(&buf, obj, cxBufferWriteFunc, writer); cxBufferTerminate(&buf); // makes debugging easier CX_TEST_ASSERT(result == 0); // compare the string CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), expected)); // destroy everything cxBufferDestroy(&buf); cxJsonValueFree(obj); } CX_TEST(test_json_write_default_format) { CxTestingAllocator talloc; cx_testing_allocator_init(&talloc); CxAllocator *allocator = &talloc.base; CX_TEST_DO { // expected value cxstring expected = CX_STR( "{\"bool\":false," "\"int\":47," "\"nested\":{" "\"floats\":[3.1415,47.11,8.15]," "\"ints\":[4,8,15,[16,23],42]," "\"literals\":[true,null,false]," "\"objects\":[{" "\"name1\":1," "\"name2\":3" "},{" "\"name1\":3," "\"name2\":7" "}]" "}," "\"strings\":[\"hello\",\"world\"]" "}" ); CxJsonWriter writer = cxJsonWriterCompact(); CX_TEST_CALL_SUBROUTINE(test_json_write_sub, allocator, expected, &writer); CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); } cx_testing_allocator_destroy(&talloc); } CX_TEST(test_json_write_pretty_default_spaces) { CxTestingAllocator talloc; cx_testing_allocator_init(&talloc); CxAllocator *allocator = &talloc.base; CX_TEST_DO { cxstring expected = CX_STR( "{\n" " \"bool\": false,\n" " \"int\": 47,\n" " \"nested\": {\n" " \"floats\": [3.1415, 47.11, 8.15],\n" " \"ints\": [4, 8, 15, [16, 23], 42],\n" " \"literals\": [true, null, false],\n" " \"objects\": [{\n" " \"name1\": 1,\n" " \"name2\": 3\n" " }, {\n" " \"name1\": 3,\n" " \"name2\": 7\n" " }]\n" " },\n" " \"strings\": [\"hello\", \"world\"]\n" "}" ); CxJsonWriter writer = cxJsonWriterPretty(true); CX_TEST_CALL_SUBROUTINE(test_json_write_sub, allocator, expected, &writer); CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); } cx_testing_allocator_destroy(&talloc); } CX_TEST(test_json_write_pretty_default_tabs) { CxTestingAllocator talloc; cx_testing_allocator_init(&talloc); CxAllocator *allocator = &talloc.base; CX_TEST_DO { cxstring expected = CX_STR( "{\n" "\t\"bool\": false,\n" "\t\"int\": 47,\n" "\t\"nested\": {\n" "\t\t\"floats\": [3.1415, 47.11, 8.15],\n" "\t\t\"ints\": [4, 8, 15, [16, 23], 42],\n" "\t\t\"literals\": [true, null, false],\n" "\t\t\"objects\": [{\n" "\t\t\t\"name1\": 1,\n" "\t\t\t\"name2\": 3\n" "\t\t}, {\n" "\t\t\t\"name1\": 3,\n" "\t\t\t\"name2\": 7\n" "\t\t}]\n" "\t},\n" "\t\"strings\": [\"hello\", \"world\"]\n" "}" ); CxJsonWriter writer = cxJsonWriterPretty(false); CX_TEST_CALL_SUBROUTINE(test_json_write_sub, allocator, expected, &writer); CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); } cx_testing_allocator_destroy(&talloc); } CX_TEST(test_json_write_pretty_preserve_order) { CxTestingAllocator talloc; cx_testing_allocator_init(&talloc); CxAllocator *allocator = &talloc.base; CX_TEST_DO { cxstring expected = CX_STR( "{\n" " \"bool\": false,\n" " \"int\": 47,\n" " \"strings\": [\"hello\", \"world\"],\n" " \"nested\": {\n" " \"objects\": [{\n" " \"name1\": 1,\n" " \"name2\": 3\n" " }, {\n" " \"name2\": 7,\n" " \"name1\": 3\n" " }],\n" " \"floats\": [3.1415, 47.11, 8.15],\n" " \"literals\": [true, null, false],\n" " \"ints\": [4, 8, 15, [16, 23], 42]\n" " }\n" "}" ); CxJsonWriter writer = cxJsonWriterPretty(true); writer.sort_members = false; CX_TEST_CALL_SUBROUTINE(test_json_write_sub, allocator, expected, &writer); CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); } cx_testing_allocator_destroy(&talloc); } CX_TEST(test_json_write_frac_max_digits) { CxJsonValue* num = cxJsonCreateNumber(NULL, 3.141592653589793); CxJsonWriter writer = cxJsonWriterCompact(); CxBuffer buf; cxBufferInit(&buf, NULL, 32, NULL, 0); CX_TEST_DO { // test default settings (6 digits) cxJsonWrite(&buf, num, cxBufferWriteFunc, &writer); CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("3.141592"))); // test too many digits cxBufferReset(&buf); writer.frac_max_digits = 200; cxJsonWrite(&buf, num, cxBufferWriteFunc, &writer); CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("3.141592653589793"))); // test 0 digits cxBufferReset(&buf); writer.frac_max_digits = 0; cxJsonWrite(&buf, num, cxBufferWriteFunc, &writer); CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("3"))); // test 2 digits cxBufferReset(&buf); writer.frac_max_digits = 2; cxJsonWrite(&buf, num, cxBufferWriteFunc, &writer); CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("3.14"))); // test 3 digits cxBufferReset(&buf); writer.frac_max_digits = 3; cxJsonWrite(&buf, num, cxBufferWriteFunc, &writer); CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("3.141"))); // test 6 digits, but two are left of the decimal point num->value.number = 47.110815; cxBufferReset(&buf); writer.frac_max_digits = 6; cxJsonWrite(&buf, num, cxBufferWriteFunc, &writer); CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("47.110815"))); // test 4 digits with exponent num->value.number = 5.11223344e23; cxBufferReset(&buf); writer.frac_max_digits = 4; cxJsonWrite(&buf, num, cxBufferWriteFunc, &writer); CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("5.1122e+23"))); } cxBufferDestroy(&buf); cxJsonValueFree(num); } CX_TEST(test_json_write_string_escape) { /** * According to RFC-8259 we have to test the following characters: * " quotation mark * \ reverse solidus * / solidus ---> we make this optional, see test_json_write_solidus * b backspace * f form feed * n line feed * r carriage return * t tab * And all other control characters must be encoded uXXXX - in our example the bell character. * Also, all unicode characters are encoded that way - in our example the 'ö'. */ CxJsonValue* str = cxJsonCreateString(NULL, "hello\twörld\r\nthis is\\a \"string\"\b in \a string\f"); CxJsonWriter writer = cxJsonWriterCompact(); CxBuffer buf; cxBufferInit(&buf, NULL, 128, NULL, 0); CX_TEST_DO { cxJsonWrite(&buf, str, cxBufferWriteFunc, &writer); CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("\"hello\\twörld\\r\\nthis is\\\\a \\\"string\\\"\\b in \\u0007 string\\f\""))); } cxBufferDestroy(&buf); cxJsonValueFree(str); } CX_TEST(test_json_write_name_escape) { CxJsonValue* obj = cxJsonCreateObj(NULL); cxJsonObjPutLiteral(obj, CX_STR("hello\twörld\r\nthis is\\a \"string\"\b in \a string\f"), CX_JSON_TRUE); CxJsonWriter writer = cxJsonWriterCompact(); CxBuffer buf; cxBufferInit(&buf, NULL, 128, NULL, 0); CX_TEST_DO { cxJsonWrite(&buf, obj, cxBufferWriteFunc, &writer); CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("{\"hello\\twörld\\r\\nthis is\\\\a \\\"string\\\"\\b in \\u0007 string\\f\":true}"))); } cxBufferDestroy(&buf); cxJsonValueFree(obj); } CX_TEST(test_json_write_solidus) { CxJsonValue* str = cxJsonCreateString(NULL,"test/solidus"); CxJsonWriter writer = cxJsonWriterCompact(); CxBuffer buf; cxBufferInit(&buf, NULL, 16, NULL, 0); CX_TEST_DO { // default: do not escape cxJsonWrite(&buf, str, cxBufferWriteFunc, &writer); CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("\"test/solidus\""))); // enable escaping writer.escape_slash = true; cxBufferReset(&buf); cxJsonWrite(&buf, str, cxBufferWriteFunc, &writer); CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("\"test\\/solidus\""))); } cxBufferDestroy(&buf); cxJsonValueFree(str); } CxTestSuite *cx_test_suite_json(void) { CxTestSuite *suite = cx_test_suite_new("json"); cx_test_register(suite, test_json_init_default); cx_test_register(suite, test_json_simple_object); cx_test_register(suite, test_json_escaped_strings); cx_test_register(suite, test_json_escaped_unicode_strings); cx_test_register(suite, test_json_escaped_unicode_malformed); cx_test_register(suite, test_json_escaped_end_of_string); cx_test_register(suite, test_json_object_incomplete_token); cx_test_register(suite, test_json_token_wrongly_completed); cx_test_register(suite, test_json_object_error); cx_test_register(suite, test_json_subsequent_fill); cx_test_register(suite, test_json_large_nesting_depth); cx_test_register(suite, test_json_number); cx_test_register(suite, test_json_number_format_errors); cx_test_register(suite, test_json_multiple_values); cx_test_register(suite, test_json_array_iterator); cx_test_register(suite, test_json_allocator); cx_test_register(suite, test_json_allocator_parse_error); cx_test_register(suite, test_json_create_value); cx_test_register(suite, test_json_write_default_format); cx_test_register(suite, test_json_write_pretty_default_spaces); cx_test_register(suite, test_json_write_pretty_default_tabs); cx_test_register(suite, test_json_write_pretty_preserve_order); cx_test_register(suite, test_json_write_frac_max_digits); cx_test_register(suite, test_json_write_string_escape); cx_test_register(suite, test_json_write_name_escape); cx_test_register(suite, test_json_write_solidus); return suite; }