tests/test_json.c

Fri, 23 May 2025 12:44:24 +0200

author
Mike Becker <universe@uap-core.de>
date
Fri, 23 May 2025 12:44:24 +0200
changeset 1327
ed75dc1db503
parent 1247
e30d38e06559
permissions
-rw-r--r--

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;
}

mercurial