From c05e36b178450f0ecae9c53aa76e031cf3322d5c Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 20:20:13 +0200 Subject: [PATCH] test: cover mustParse and BadDocument --- tests/testJson.cpp | 145 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 tests/testJson.cpp diff --git a/tests/testJson.cpp b/tests/testJson.cpp new file mode 100644 index 0000000..f61f9fe --- /dev/null +++ b/tests/testJson.cpp @@ -0,0 +1,145 @@ +#include "testHarness.h" +#include "vf/Json.h" + +#include +#include +#include + +using namespace vf; + +namespace { + +const std::vector& uglyDocuments() { + static const std::vector documents = { + "{}", + "[]", + "null", + "true", + "false", + "0", + "-0", + "-17", + "3.5", + "1e-3", + "-2.5E+4", + "1e308", + "0.1", + "9223372036854775807", + "-9223372036854775808", + "\"\"", + "\"tab\\tnewline\\nquote\\\"backslash\\\\slash\\/bell\\bform\\fcarriage\\r\"", + "\"\\u0000 embedded nul, \\u001f control, \\u007f delete\"", + "\"\\u00e9\\u0141\\u4e2d\\ud83e\\udd8a\"", + "[[[[[[[[[[1]]]]]]]]]]", + "[1,2,3,[4,[5,[6]]],{\"deep\":{\"deeper\":{\"deepest\":[]}}}]", + "{\"\":\"empty key\"}", + "{\"a/b\":1,\"c~d\":2,\"e f\":3}", + "{\"duplicate\":1,\"duplicate\":2}", + " \n\t {\n \"spread\" :\t[ 1 ,\n 2 ]\n} \n", + "{\"mixed\":[null,true,false,0,-0.0,\"\",{},[]]}", + "[{\"item\":\"voxelforge:iron_ingot\",\"count\":3,\"chance\":0.25}]", + "{\"nested\":{\"list\":[{\"a\":[{\"b\":[[[1]]]}]}]}}", + "{\"unicodeKey\\u00e9\":\"value\\u00e9\"}", + }; + return documents; +} + +JsonValue mustParse(const std::string& text) { + JsonParseResult parsed = parseJson(text); + CHECK(parsed.ok); + if (!parsed.ok) std::printf(" parse failed: %s\n", parsed.error.text().c_str()); + return parsed.root; +} + +std::string repeated(const std::string& unit, int times) { + std::string out; + out.reserve(unit.size() * static_cast(times)); + for (int i = 0; i < times; ++i) out += unit; + return out; +} + +struct BadDocument { + const char* text; + int line; + int column; + const char* fragment; +}; + +} // namespace + +VF_TEST(everyUglyDocumentSurvivesParseWriteParseUnchanged) { + for (const std::string& text : uglyDocuments()) { + JsonParseResult first = parseJson(text); + CHECK(first.ok); + if (!first.ok) { + std::printf(" rejected: %s -> %s\n", text.c_str(), first.error.text().c_str()); + continue; + } + std::string compact = JsonWriter::compact(first.root); + JsonParseResult second = parseJson(compact); + CHECK(second.ok); + CHECK(second.root == first.root); + + std::string indented = JsonWriter::indented(first.root, 2); + JsonParseResult third = parseJson(indented); + CHECK(third.ok); + CHECK(third.root == first.root); + + // The compact form is a fixed point: writing it again changes nothing. + CHECK_EQ(JsonWriter::compact(second.root), compact); + CHECK_EQ(JsonWriter::compact(third.root), compact); + } +} + +VF_TEST(writtenDocumentsStayPureAsciiWhateverWentIn) { + JsonValue root = mustParse("{\"key\":\"\\u00e9\\u4e2d\\ud83e\\udd8a\"}"); + std::string compact = JsonWriter::compact(root); + for (char c : compact) CHECK(static_cast(c) < 0x80u); + CHECK_EQ(compact, std::string("{\"key\":\"\\u00e9\\u4e2d\\ud83e\\udd8a\"}")); + + JsonWriter raw; + raw.escapeNonAscii = false; + std::string utf8 = raw.write(root); + CHECK(utf8.size() < compact.size()); + bool carriesHighBytes = false; + for (char c : utf8) + if (static_cast(c) >= 0x80u) carriesHighBytes = true; + CHECK(carriesHighBytes); + CHECK(parseJson(utf8).ok); + CHECK(parseJson(utf8).root == root); +} + +VF_TEST(surrogatePairsBecomeOneCodePointAndComeBackOutAsAPair) { + JsonValue root = mustParse("[\"\\ud83e\\udd8a\"]"); + const JsonValue* text = root.at(0u); + CHECK(text != nullptr); + CHECK_EQ(text->stringValue().size(), static_cast(4)); + CHECK_EQ(static_cast(text->stringValue()[0]), 0xF0u); + CHECK_EQ(static_cast(text->stringValue()[1]), 0x9Fu); + CHECK_EQ(static_cast(text->stringValue()[2]), 0xA6u); + CHECK_EQ(static_cast(text->stringValue()[3]), 0x8Au); + CHECK_EQ(JsonWriter::compact(root), std::string("[\"\\ud83e\\udd8a\"]")); + + JsonValue latin = mustParse("\"\\u00e9\""); + CHECK_EQ(latin.stringValue().size(), static_cast(2)); + CHECK_EQ(JsonWriter::compact(latin), std::string("\"\\u00e9\"")); +} + +VF_TEST(brokenSurrogatesAndUnknownEscapesAreRefused) { + const char* broken[] = { + "\"\\ud83e\"", // high surrogate with nothing after it + "\"\\ud83eX\"", // high surrogate followed by a plain character + "\"\\ud83e\\u0041\"", // high surrogate followed by a non low surrogate + "\"\\udd8a\"", // low surrogate on its own + "\"\\q\"", // unknown escape + "\"\\u00g1\"", // not a hexadecimal digit + "\"\\u00\"", // truncated escape + }; + for (const char* text : broken) { + JsonParseResult parsed = parseJson(text); + CHECK(!parsed.ok); + CHECK(parsed.error.present()); + CHECK(!parsed.error.message.empty()); + } +} +