| /* |
| * Intensive parser unit tests for tinygltf with the custom JSON backend |
| * (tinygltf_json.h). |
| * |
| * These tests exercise the custom JSON parser and glTF loader with a wide |
| * range of edge-case inputs: deeply nested structures, malformed JSON, |
| * truncated inputs, unicode escapes, number edge cases, and round-trip |
| * serialisation/deserialisation. |
| * |
| * Build: |
| * c++ -std=c++11 -DTINYGLTF_USE_CUSTOM_JSON -I.. -I. -g -O0 \ |
| * -o tester_intensive_customjson tester_intensive_customjson.cc |
| */ |
| |
| #define TINYGLTF_IMPLEMENTATION |
| #define STB_IMAGE_IMPLEMENTATION |
| #define STB_IMAGE_WRITE_IMPLEMENTATION |
| #ifndef TINYGLTF_USE_CUSTOM_JSON |
| #define TINYGLTF_USE_CUSTOM_JSON |
| #endif |
| #include "tiny_gltf.h" |
| |
| #define CATCH_CONFIG_MAIN |
| #include "catch.hpp" |
| |
| #include <cstdio> |
| #include <cstdlib> |
| #include <cassert> |
| #include <iostream> |
| #include <sstream> |
| #include <fstream> |
| #include <string> |
| #include <cstring> |
| #include <cmath> |
| #include <limits> |
| |
| /* ---------- helpers --------------------------------------------------------*/ |
| |
| /* Convenience: parse JSON string through the custom backend. */ |
| static tinygltf::detail::JsonDocument JsonConstruct(const char *str) { |
| tinygltf::detail::JsonDocument doc; |
| tinygltf::detail::JsonParse(doc, str, strlen(str)); |
| return doc; |
| } |
| |
| /* Helper: access the i-th element of an array-type json node. */ |
| static tinygltf::detail::json &JsonArrayAt(tinygltf::detail::json &arr, |
| size_t idx) { |
| auto it = arr.begin(); |
| for (size_t i = 0; i < idx; ++i) ++it; |
| return *it; |
| } |
| |
| /* Load a glTF JSON string through TinyGLTF and return success. */ |
| static bool LoadGltfFromString(const char *json, |
| tinygltf::Model *model = nullptr, |
| std::string *err = nullptr, |
| std::string *warn = nullptr) { |
| tinygltf::Model tmp_model; |
| std::string tmp_err, tmp_warn; |
| if (!model) model = &tmp_model; |
| if (!err) err = &tmp_err; |
| if (!warn) warn = &tmp_warn; |
| tinygltf::TinyGLTF ctx; |
| return ctx.LoadASCIIFromString(model, err, warn, json, |
| static_cast<unsigned int>(strlen(json)), |
| /* base_dir */ ""); |
| } |
| |
| /* ===== Section 1: Custom JSON parser edge-cases =========================== */ |
| |
| TEST_CASE("cj-parse-empty-input", "[customjson][parse]") { |
| auto doc = JsonConstruct(""); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-parse-whitespace-only", "[customjson][parse]") { |
| auto doc = JsonConstruct(" \t\n\r "); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-parse-null", "[customjson][parse]") { |
| auto doc = JsonConstruct("null"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-parse-true", "[customjson][parse]") { |
| auto doc = JsonConstruct("true"); |
| REQUIRE(doc.is_boolean()); |
| REQUIRE(doc.get<bool>() == true); |
| } |
| |
| TEST_CASE("cj-parse-false", "[customjson][parse]") { |
| auto doc = JsonConstruct("false"); |
| REQUIRE(doc.is_boolean()); |
| REQUIRE(doc.get<bool>() == false); |
| } |
| |
| TEST_CASE("cj-parse-integer-zero", "[customjson][parse]") { |
| auto doc = JsonConstruct("0"); |
| REQUIRE(doc.is_number()); |
| REQUIRE(doc.get<int>() == 0); |
| } |
| |
| TEST_CASE("cj-parse-negative-zero", "[customjson][parse]") { |
| auto doc = JsonConstruct("-0"); |
| REQUIRE(doc.is_number()); |
| // -0 should still compare equal to 0 |
| REQUIRE(doc.get<double>() == 0.0); |
| } |
| |
| TEST_CASE("cj-parse-positive-integer", "[customjson][parse]") { |
| auto doc = JsonConstruct("42"); |
| REQUIRE(doc.is_number()); |
| REQUIRE(doc.get<int>() == 42); |
| } |
| |
| TEST_CASE("cj-parse-negative-integer", "[customjson][parse]") { |
| auto doc = JsonConstruct("-999"); |
| REQUIRE(doc.is_number()); |
| REQUIRE(doc.get<int>() == -999); |
| } |
| |
| TEST_CASE("cj-parse-large-integer", "[customjson][parse]") { |
| auto doc = JsonConstruct("2147483647"); |
| REQUIRE(doc.is_number()); |
| REQUIRE(doc.get<int>() == 2147483647); |
| } |
| |
| TEST_CASE("cj-parse-double", "[customjson][parse]") { |
| auto doc = JsonConstruct("3.14159"); |
| REQUIRE(doc.is_number()); |
| REQUIRE(doc.get<double>() == Approx(3.14159)); |
| } |
| |
| TEST_CASE("cj-parse-double-exponent", "[customjson][parse]") { |
| auto doc = JsonConstruct("1.5e10"); |
| REQUIRE(doc.is_number()); |
| REQUIRE(doc.get<double>() == Approx(1.5e10)); |
| } |
| |
| TEST_CASE("cj-parse-double-negative-exponent", "[customjson][parse]") { |
| auto doc = JsonConstruct("2.5e-3"); |
| REQUIRE(doc.is_number()); |
| REQUIRE(doc.get<double>() == Approx(0.0025)); |
| } |
| |
| TEST_CASE("cj-parse-double-positive-exponent", "[customjson][parse]") { |
| auto doc = JsonConstruct("1E+2"); |
| REQUIRE(doc.is_number()); |
| REQUIRE(doc.get<double>() == Approx(100.0)); |
| } |
| |
| TEST_CASE("cj-parse-simple-string", "[customjson][parse]") { |
| auto doc = JsonConstruct("\"hello world\""); |
| REQUIRE(doc.is_string()); |
| REQUIRE(doc.get<std::string>() == "hello world"); |
| } |
| |
| TEST_CASE("cj-parse-empty-string", "[customjson][parse]") { |
| auto doc = JsonConstruct("\"\""); |
| REQUIRE(doc.is_string()); |
| REQUIRE(doc.get<std::string>().empty()); |
| } |
| |
| TEST_CASE("cj-parse-string-escapes", "[customjson][parse]") { |
| // Test all standard JSON escape sequences |
| auto doc = JsonConstruct("\"a\\nb\\tc\\rd\\\\e\\\"/f\""); |
| REQUIRE(doc.is_string()); |
| std::string expected = "a\nb\tc\rd\\e\"/f"; |
| REQUIRE(doc.get<std::string>() == expected); |
| } |
| |
| TEST_CASE("cj-parse-string-unicode-escape", "[customjson][parse]") { |
| // \u0041 == 'A' |
| auto doc = JsonConstruct("\"\\u0041\""); |
| REQUIRE(doc.is_string()); |
| REQUIRE(doc.get<std::string>() == "A"); |
| } |
| |
| TEST_CASE("cj-parse-string-unicode-non-ascii", "[customjson][parse]") { |
| // \u00E9 == é (UTF-8: 0xC3 0xA9) |
| auto doc = JsonConstruct("\"caf\\u00E9\""); |
| REQUIRE(doc.is_string()); |
| REQUIRE(doc.get<std::string>() == "caf\xC3\xA9"); |
| } |
| |
| TEST_CASE("cj-parse-string-unicode-cjk", "[customjson][parse]") { |
| // \u4E16 == 世 (UTF-8: 0xE4 0xB8 0x96) |
| auto doc = JsonConstruct("\"\\u4E16\""); |
| REQUIRE(doc.is_string()); |
| std::string expected; |
| expected += '\xE4'; |
| expected += '\xB8'; |
| expected += '\x96'; |
| REQUIRE(doc.get<std::string>() == expected); |
| } |
| |
| TEST_CASE("cj-parse-empty-array", "[customjson][parse]") { |
| auto doc = JsonConstruct("[]"); |
| REQUIRE(doc.is_array()); |
| REQUIRE(doc.size() == 0); |
| } |
| |
| TEST_CASE("cj-parse-array-of-ints", "[customjson][parse]") { |
| auto doc = JsonConstruct("[1, 2, 3, 4, 5]"); |
| REQUIRE(doc.is_array()); |
| REQUIRE(doc.size() == 5); |
| for (size_t i = 0; i < 5; i++) { |
| REQUIRE(JsonArrayAt(doc, i).get<int>() == static_cast<int>(i + 1)); |
| } |
| } |
| |
| TEST_CASE("cj-parse-nested-arrays", "[customjson][parse]") { |
| auto doc = JsonConstruct("[[1, 2], [3, [4, 5]]]"); |
| REQUIRE(doc.is_array()); |
| REQUIRE(doc.size() == 2); |
| REQUIRE(JsonArrayAt(doc, 0).is_array()); |
| REQUIRE(JsonArrayAt(doc, 0).size() == 2); |
| REQUIRE(JsonArrayAt(doc, 1).is_array()); |
| REQUIRE(JsonArrayAt(doc, 1).size() == 2); |
| REQUIRE(JsonArrayAt(JsonArrayAt(doc, 1), 1).is_array()); |
| REQUIRE(JsonArrayAt(JsonArrayAt(doc, 1), 1).size() == 2); |
| } |
| |
| TEST_CASE("cj-parse-empty-object", "[customjson][parse]") { |
| auto doc = JsonConstruct("{}"); |
| REQUIRE(doc.is_object()); |
| REQUIRE(doc.size() == 0); |
| } |
| |
| TEST_CASE("cj-parse-simple-object", "[customjson][parse]") { |
| auto doc = JsonConstruct("{\"a\": 1, \"b\": \"hello\", \"c\": true}"); |
| REQUIRE(doc.is_object()); |
| REQUIRE(doc.size() == 3); |
| |
| int val = 0; |
| REQUIRE(tinygltf::detail::GetInt(doc["a"], val)); |
| REQUIRE(val == 1); |
| |
| std::string s; |
| REQUIRE(tinygltf::detail::GetString(doc["b"], s)); |
| REQUIRE(s == "hello"); |
| |
| REQUIRE(doc["c"].get<bool>() == true); |
| } |
| |
| TEST_CASE("cj-parse-nested-object", "[customjson][parse]") { |
| auto doc = JsonConstruct("{\"outer\": {\"inner\": {\"value\": 42}}}"); |
| REQUIRE(doc.is_object()); |
| REQUIRE(doc["outer"].is_object()); |
| REQUIRE(doc["outer"]["inner"].is_object()); |
| int v = 0; |
| REQUIRE(tinygltf::detail::GetInt(doc["outer"]["inner"]["value"], v)); |
| REQUIRE(v == 42); |
| } |
| |
| TEST_CASE("cj-parse-mixed-types-array", "[customjson][parse]") { |
| auto doc = |
| JsonConstruct("[null, true, false, 42, 3.14, \"text\", [], {}]"); |
| REQUIRE(doc.is_array()); |
| REQUIRE(doc.size() == 8); |
| REQUIRE(JsonArrayAt(doc, 0).is_null()); |
| REQUIRE(JsonArrayAt(doc, 1).is_boolean()); |
| REQUIRE(JsonArrayAt(doc, 2).is_boolean()); |
| REQUIRE(JsonArrayAt(doc, 3).is_number()); |
| REQUIRE(JsonArrayAt(doc, 4).is_number()); |
| REQUIRE(JsonArrayAt(doc, 5).is_string()); |
| REQUIRE(JsonArrayAt(doc, 6).is_array()); |
| REQUIRE(JsonArrayAt(doc, 7).is_object()); |
| } |
| |
| /* ===== Section 2: Deeply nested structures ================================ */ |
| |
| TEST_CASE("cj-deep-nested-arrays", "[customjson][depth]") { |
| // Build a deeply nested array: [[[[...]]]] |
| const int depth = 200; |
| std::string json; |
| for (int i = 0; i < depth; i++) json += "["; |
| json += "1"; |
| for (int i = 0; i < depth; i++) json += "]"; |
| |
| auto doc = JsonConstruct(json.c_str()); |
| REQUIRE(doc.is_array()); |
| |
| // Walk down |
| tinygltf::detail::json *cur = &doc; |
| for (int i = 0; i < depth - 1; i++) { |
| REQUIRE(cur->is_array()); |
| REQUIRE(cur->size() == 1); |
| cur = &JsonArrayAt(*cur, 0); |
| } |
| REQUIRE(cur->is_array()); |
| REQUIRE(cur->size() == 1); |
| REQUIRE(JsonArrayAt(*cur, 0).get<int>() == 1); |
| } |
| |
| TEST_CASE("cj-deep-nested-objects", "[customjson][depth]") { |
| const int depth = 100; |
| std::string json; |
| for (int i = 0; i < depth; i++) { |
| json += "{\"k\":"; |
| } |
| json += "42"; |
| for (int i = 0; i < depth; i++) { |
| json += "}"; |
| } |
| |
| auto doc = JsonConstruct(json.c_str()); |
| REQUIRE(doc.is_object()); |
| |
| tinygltf::detail::json *cur = &doc; |
| for (int i = 0; i < depth - 1; i++) { |
| REQUIRE(cur->is_object()); |
| cur = &((*cur)["k"]); |
| } |
| REQUIRE(cur->is_object()); |
| int v = 0; |
| REQUIRE(tinygltf::detail::GetInt((*cur)["k"], v)); |
| REQUIRE(v == 42); |
| } |
| |
| /* ===== Section 3: Malformed input handling ================================ */ |
| |
| TEST_CASE("cj-malformed-truncated-object", "[customjson][malformed]") { |
| auto doc = JsonConstruct("{\"key\":"); |
| // Should return null on parse error |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-truncated-array", "[customjson][malformed]") { |
| auto doc = JsonConstruct("[1, 2,"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-trailing-comma-array", "[customjson][malformed]") { |
| auto doc = JsonConstruct("[1, 2, 3,]"); |
| // Trailing comma is not valid JSON |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-trailing-comma-object", "[customjson][malformed]") { |
| auto doc = JsonConstruct("{\"a\": 1,}"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-missing-colon", "[customjson][malformed]") { |
| auto doc = JsonConstruct("{\"key\" \"value\"}"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-missing-comma", "[customjson][malformed]") { |
| auto doc = JsonConstruct("[1 2 3]"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-double-comma", "[customjson][malformed]") { |
| auto doc = JsonConstruct("[1,,2]"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-unquoted-key", "[customjson][malformed]") { |
| auto doc = JsonConstruct("{key: 1}"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-single-quoted-string", "[customjson][malformed]") { |
| auto doc = JsonConstruct("{'key': 1}"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-unterminated-string", "[customjson][malformed]") { |
| auto doc = JsonConstruct("\"hello"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-bad-escape", "[customjson][malformed]") { |
| auto doc = JsonConstruct("\"bad\\xescape\""); |
| // \\x is not a valid JSON escape |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-truncated-unicode-escape", "[customjson][malformed]") { |
| auto doc = JsonConstruct("\"trunc\\u00\""); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-incomplete-true", "[customjson][malformed]") { |
| auto doc = JsonConstruct("tru"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-incomplete-false", "[customjson][malformed]") { |
| auto doc = JsonConstruct("fals"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-incomplete-null", "[customjson][malformed]") { |
| auto doc = JsonConstruct("nul"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-extra-data", "[customjson][malformed]") { |
| // Valid JSON followed by extra non-whitespace data |
| auto doc = JsonConstruct("42 extra"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-trailing-whitespace-ok", "[customjson][parse]") { |
| // Valid JSON followed by whitespace only should be accepted |
| auto doc = JsonConstruct("42 \t\n "); |
| REQUIRE(doc.is_number()); |
| REQUIRE(doc.get<int>() == 42); |
| } |
| |
| TEST_CASE("cj-malformed-just-brace", "[customjson][malformed]") { |
| auto doc = JsonConstruct("{"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-just-bracket", "[customjson][malformed]") { |
| auto doc = JsonConstruct("["); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-mismatched-brackets", "[customjson][malformed]") { |
| auto doc = JsonConstruct("[}"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| TEST_CASE("cj-malformed-mismatched-braces", "[customjson][malformed]") { |
| auto doc = JsonConstruct("{]"); |
| REQUIRE(doc.is_null()); |
| } |
| |
| /* ===== Section 4: glTF-level loading edge-cases =========================== */ |
| |
| TEST_CASE("cj-gltf-empty-string", "[customjson][gltf]") { |
| REQUIRE(false == LoadGltfFromString("")); |
| } |
| |
| TEST_CASE("cj-gltf-garbage-input", "[customjson][gltf]") { |
| REQUIRE(false == LoadGltfFromString("not json at all")); |
| } |
| |
| TEST_CASE("cj-gltf-valid-null", "[customjson][gltf]") { |
| REQUIRE(false == LoadGltfFromString("null")); |
| } |
| |
| TEST_CASE("cj-gltf-valid-array-root", "[customjson][gltf]") { |
| // glTF expects root to be an object, not an array |
| REQUIRE(false == LoadGltfFromString("[]")); |
| } |
| |
| TEST_CASE("cj-gltf-minimal-valid", "[customjson][gltf]") { |
| // Minimal valid glTF 2.0 |
| const char *json = R"({"asset":{"version":"2.0"}})"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| REQUIRE(model.asset.version == "2.0"); |
| } |
| |
| TEST_CASE("cj-gltf-empty-meshes", "[customjson][gltf]") { |
| const char *json = R"({ |
| "asset": {"version": "2.0"}, |
| "meshes": [] |
| })"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| REQUIRE(model.meshes.empty()); |
| } |
| |
| TEST_CASE("cj-gltf-scene-with-nodes", "[customjson][gltf]") { |
| const char *json = R"({ |
| "asset": {"version": "2.0"}, |
| "scenes": [{"nodes": [0]}], |
| "nodes": [{"name": "TestNode"}] |
| })"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| REQUIRE(model.scenes.size() == 1); |
| REQUIRE(model.nodes.size() == 1); |
| REQUIRE(model.nodes[0].name == "TestNode"); |
| } |
| |
| TEST_CASE("cj-gltf-node-with-transform", "[customjson][gltf]") { |
| const char *json = R"({ |
| "asset": {"version": "2.0"}, |
| "nodes": [{ |
| "translation": [1.0, 2.0, 3.0], |
| "rotation": [0.0, 0.0, 0.0, 1.0], |
| "scale": [1.0, 1.0, 1.0] |
| }] |
| })"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| REQUIRE(model.nodes.size() == 1); |
| REQUIRE(model.nodes[0].translation.size() == 3); |
| REQUIRE(model.nodes[0].translation[0] == Approx(1.0)); |
| REQUIRE(model.nodes[0].translation[1] == Approx(2.0)); |
| REQUIRE(model.nodes[0].translation[2] == Approx(3.0)); |
| REQUIRE(model.nodes[0].rotation.size() == 4); |
| REQUIRE(model.nodes[0].scale.size() == 3); |
| } |
| |
| TEST_CASE("cj-gltf-material-pbr", "[customjson][gltf]") { |
| const char *json = R"({ |
| "asset": {"version": "2.0"}, |
| "materials": [{ |
| "pbrMetallicRoughness": { |
| "baseColorFactor": [1.0, 0.5, 0.25, 1.0], |
| "metallicFactor": 0.8, |
| "roughnessFactor": 0.2 |
| } |
| }] |
| })"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| REQUIRE(model.materials.size() == 1); |
| auto &pbr = model.materials[0].pbrMetallicRoughness; |
| REQUIRE(pbr.baseColorFactor.size() == 4); |
| REQUIRE(pbr.baseColorFactor[0] == Approx(1.0)); |
| REQUIRE(pbr.baseColorFactor[1] == Approx(0.5)); |
| REQUIRE(pbr.baseColorFactor[2] == Approx(0.25)); |
| REQUIRE(pbr.metallicFactor == Approx(0.8)); |
| REQUIRE(pbr.roughnessFactor == Approx(0.2)); |
| } |
| |
| TEST_CASE("cj-gltf-accessors-and-bufferviews", "[customjson][gltf]") { |
| const char *json = R"({ |
| "asset": {"version": "2.0"}, |
| "buffers": [{"uri": "data:application/octet-stream;base64,AAAAAAAAAAA=", "byteLength": 8}], |
| "bufferViews": [{"buffer": 0, "byteOffset": 0, "byteLength": 8}], |
| "accessors": [{ |
| "bufferView": 0, |
| "componentType": 5126, |
| "count": 2, |
| "type": "SCALAR", |
| "max": [1.0], |
| "min": [0.0] |
| }] |
| })"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| REQUIRE(model.buffers.size() == 1); |
| REQUIRE(model.bufferViews.size() == 1); |
| REQUIRE(model.accessors.size() == 1); |
| REQUIRE(model.accessors[0].componentType == TINYGLTF_COMPONENT_TYPE_FLOAT); |
| REQUIRE(model.accessors[0].count == 2); |
| REQUIRE(model.accessors[0].type == TINYGLTF_TYPE_SCALAR); |
| } |
| |
| TEST_CASE("cj-gltf-extensions", "[customjson][gltf]") { |
| const char *json = R"({ |
| "asset": {"version": "2.0"}, |
| "extensionsUsed": ["KHR_lights_punctual"], |
| "extensions": { |
| "KHR_lights_punctual": { |
| "lights": [{ |
| "color": [1.0, 1.0, 1.0], |
| "intensity": 5.0, |
| "type": "point" |
| }] |
| } |
| } |
| })"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| REQUIRE(model.extensionsUsed.size() == 1); |
| REQUIRE(model.extensionsUsed[0] == "KHR_lights_punctual"); |
| REQUIRE(model.extensions.count("KHR_lights_punctual") == 1); |
| } |
| |
| TEST_CASE("cj-gltf-extras", "[customjson][gltf]") { |
| const char *json = R"({ |
| "asset": {"version": "2.0"}, |
| "nodes": [{ |
| "name": "Test", |
| "extras": {"custom_field": 123, "nested": {"a": [1,2,3]}} |
| }] |
| })"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| REQUIRE(model.nodes.size() == 1); |
| // extras should be preserved as a Value |
| REQUIRE(model.nodes[0].extras.IsObject()); |
| } |
| |
| /* ===== Section 5: Round-trip serialisation ================================ */ |
| |
| TEST_CASE("cj-roundtrip-minimal", "[customjson][roundtrip]") { |
| const char *json = R"({"asset":{"version":"2.0"}})"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| |
| // Serialise to string |
| std::stringstream os; |
| tinygltf::TinyGLTF ctx; |
| REQUIRE(true == ctx.WriteGltfSceneToStream(&model, os, false, false)); |
| |
| // Re-parse and verify |
| tinygltf::Model model2; |
| std::string json2 = os.str(); |
| REQUIRE(true == |
| ctx.LoadASCIIFromString(&model2, &err, &warn, json2.c_str(), |
| static_cast<unsigned int>(json2.size()), "")); |
| REQUIRE(model2.asset.version == "2.0"); |
| } |
| |
| TEST_CASE("cj-roundtrip-with-nodes", "[customjson][roundtrip]") { |
| const char *json = R"({ |
| "asset": {"version": "2.0"}, |
| "scenes": [{"nodes": [0]}], |
| "nodes": [ |
| {"name": "Root", "children": [1]}, |
| {"name": "Child", "translation": [1.0, 2.0, 3.0]} |
| ] |
| })"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| |
| std::stringstream os; |
| tinygltf::TinyGLTF ctx; |
| REQUIRE(true == ctx.WriteGltfSceneToStream(&model, os, false, false)); |
| |
| tinygltf::Model model2; |
| std::string json2 = os.str(); |
| REQUIRE(true == |
| ctx.LoadASCIIFromString(&model2, &err, &warn, json2.c_str(), |
| static_cast<unsigned int>(json2.size()), "")); |
| REQUIRE(model2.nodes.size() == 2); |
| REQUIRE(model2.nodes[0].name == "Root"); |
| REQUIRE(model2.nodes[1].name == "Child"); |
| REQUIRE(model2.nodes[1].translation[0] == Approx(1.0)); |
| } |
| |
| TEST_CASE("cj-roundtrip-material", "[customjson][roundtrip]") { |
| const char *json = R"({ |
| "asset": {"version": "2.0"}, |
| "materials": [{ |
| "name": "TestMat", |
| "pbrMetallicRoughness": { |
| "baseColorFactor": [0.8, 0.2, 0.1, 1.0], |
| "metallicFactor": 0.5, |
| "roughnessFactor": 0.7 |
| }, |
| "doubleSided": true |
| }] |
| })"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| |
| std::stringstream os; |
| tinygltf::TinyGLTF ctx; |
| REQUIRE(true == ctx.WriteGltfSceneToStream(&model, os, false, false)); |
| |
| tinygltf::Model model2; |
| std::string json2 = os.str(); |
| REQUIRE(true == |
| ctx.LoadASCIIFromString(&model2, &err, &warn, json2.c_str(), |
| static_cast<unsigned int>(json2.size()), "")); |
| REQUIRE(model2.materials.size() == 1); |
| REQUIRE(model2.materials[0].name == "TestMat"); |
| REQUIRE(model2.materials[0].doubleSided == true); |
| REQUIRE(model2.materials[0].pbrMetallicRoughness.metallicFactor == |
| Approx(0.5)); |
| } |
| |
| /* ===== Section 6: Binary (GLB) loading ==================================== */ |
| |
| TEST_CASE("cj-glb-invalid-magic", "[customjson][glb]") { |
| // GLB file header fields: magic (4), version (4), length (4) = 12 bytes. |
| // TinyGLTF::LoadBinaryFromMemory, however, requires at least 20 bytes |
| // (12-byte file header + 8-byte first chunk header) before checking magic/version. |
| // This buffer is only 12 bytes long (and uses a wrong magic), so the loader |
| // will reject it as an undersized/invalid GLB binary. |
| unsigned char data[12] = {0x00, 0x00, 0x00, 0x00, 0x02, 0x00, |
| 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00}; |
| tinygltf::Model model; |
| tinygltf::TinyGLTF ctx; |
| std::string err, warn; |
| bool ret = ctx.LoadBinaryFromMemory(&model, &err, &warn, data, sizeof(data)); |
| REQUIRE(false == ret); |
| } |
| |
| TEST_CASE("cj-glb-truncated-header", "[customjson][glb]") { |
| // Less than 12 bytes |
| unsigned char data[8] = {0x67, 0x6C, 0x54, 0x46, 0x02, 0x00, 0x00, 0x00}; |
| tinygltf::Model model; |
| tinygltf::TinyGLTF ctx; |
| std::string err, warn; |
| bool ret = ctx.LoadBinaryFromMemory(&model, &err, &warn, data, sizeof(data)); |
| REQUIRE(false == ret); |
| } |
| |
| TEST_CASE("cj-glb-zero-length", "[customjson][glb]") { |
| tinygltf::Model model; |
| tinygltf::TinyGLTF ctx; |
| std::string err, warn; |
| bool ret = ctx.LoadBinaryFromMemory(&model, &err, &warn, nullptr, 0); |
| REQUIRE(false == ret); |
| } |
| |
| /* ===== Section 7: JSON detail helpers ===================================== */ |
| |
| TEST_CASE("cj-detail-GetInt", "[customjson][detail]") { |
| auto doc = JsonConstruct("{\"val\": 42}"); |
| int v = 0; |
| REQUIRE(tinygltf::detail::GetInt(doc["val"], v)); |
| REQUIRE(v == 42); |
| } |
| |
| TEST_CASE("cj-detail-GetDouble", "[customjson][detail]") { |
| auto doc = JsonConstruct("{\"val\": 3.14}"); |
| double v = 0; |
| REQUIRE(tinygltf::detail::GetDouble(doc["val"], v)); |
| REQUIRE(v == Approx(3.14)); |
| } |
| |
| TEST_CASE("cj-detail-GetNumber", "[customjson][detail]") { |
| auto doc = JsonConstruct("{\"val\": 99}"); |
| double v = 0; |
| REQUIRE(tinygltf::detail::GetNumber(doc["val"], v)); |
| REQUIRE(v == Approx(99.0)); |
| } |
| |
| TEST_CASE("cj-detail-GetString", "[customjson][detail]") { |
| auto doc = JsonConstruct("{\"val\": \"hello\"}"); |
| std::string v; |
| REQUIRE(tinygltf::detail::GetString(doc["val"], v)); |
| REQUIRE(v == "hello"); |
| } |
| |
| TEST_CASE("cj-detail-IsArray", "[customjson][detail]") { |
| auto doc = JsonConstruct("[1, 2]"); |
| REQUIRE(tinygltf::detail::IsArray(doc)); |
| auto doc2 = JsonConstruct("42"); |
| REQUIRE_FALSE(tinygltf::detail::IsArray(doc2)); |
| } |
| |
| TEST_CASE("cj-detail-IsObject", "[customjson][detail]") { |
| auto doc = JsonConstruct("{\"a\": 1}"); |
| REQUIRE(tinygltf::detail::IsObject(doc)); |
| auto doc2 = JsonConstruct("[]"); |
| REQUIRE_FALSE(tinygltf::detail::IsObject(doc2)); |
| } |
| |
| TEST_CASE("cj-detail-IsEmpty", "[customjson][detail]") { |
| auto doc = JsonConstruct("{}"); |
| REQUIRE(tinygltf::detail::IsEmpty(doc)); |
| auto doc2 = JsonConstruct("{\"a\": 1}"); |
| REQUIRE_FALSE(tinygltf::detail::IsEmpty(doc2)); |
| } |
| |
| TEST_CASE("cj-detail-JsonIsNull", "[customjson][detail]") { |
| auto doc = JsonConstruct("null"); |
| REQUIRE(tinygltf::detail::JsonIsNull(doc)); |
| auto doc2 = JsonConstruct("42"); |
| REQUIRE_FALSE(tinygltf::detail::JsonIsNull(doc2)); |
| } |
| |
| /* ===== Section 8: Serialisation helpers =================================== */ |
| |
| TEST_CASE("cj-detail-JsonFromString", "[customjson][detail]") { |
| auto j = tinygltf::detail::JsonFromString("test"); |
| REQUIRE(j.is_string()); |
| REQUIRE(j.get<std::string>() == "test"); |
| } |
| |
| TEST_CASE("cj-detail-JsonSetObject", "[customjson][detail]") { |
| tinygltf::detail::json j; |
| tinygltf::detail::JsonSetObject(j); |
| REQUIRE(j.is_object()); |
| REQUIRE(j.size() == 0); |
| } |
| |
| TEST_CASE("cj-detail-JsonAddMember", "[customjson][detail]") { |
| tinygltf::detail::json j; |
| tinygltf::detail::JsonSetObject(j); |
| auto val = tinygltf::detail::JsonFromString("bar"); |
| tinygltf::detail::JsonAddMember(j, "foo", std::move(val)); |
| REQUIRE(j.size() == 1); |
| std::string s; |
| REQUIRE(tinygltf::detail::GetString(j["foo"], s)); |
| REQUIRE(s == "bar"); |
| } |
| |
| TEST_CASE("cj-detail-JsonPushBack", "[customjson][detail]") { |
| auto j = JsonConstruct("[]"); |
| tinygltf::detail::json val; |
| val = JsonConstruct("42"); |
| tinygltf::detail::JsonPushBack(j, std::move(val)); |
| REQUIRE(j.size() == 1); |
| REQUIRE(JsonArrayAt(j, 0).get<int>() == 42); |
| } |
| |
| /* ===== Section 9: Stress / large input ==================================== */ |
| |
| TEST_CASE("cj-large-array", "[customjson][stress]") { |
| // Array with 1000 integers |
| std::string json = "["; |
| for (int i = 0; i < 1000; i++) { |
| if (i > 0) json += ","; |
| json += std::to_string(i); |
| } |
| json += "]"; |
| auto doc = JsonConstruct(json.c_str()); |
| REQUIRE(doc.is_array()); |
| REQUIRE(doc.size() == 1000); |
| REQUIRE(JsonArrayAt(doc, 0).get<int>() == 0); |
| REQUIRE(JsonArrayAt(doc, 999).get<int>() == 999); |
| } |
| |
| TEST_CASE("cj-large-object", "[customjson][stress]") { |
| // Object with 500 keys |
| std::string json = "{"; |
| for (int i = 0; i < 500; i++) { |
| if (i > 0) json += ","; |
| json += "\"key" + std::to_string(i) + "\":" + std::to_string(i); |
| } |
| json += "}"; |
| auto doc = JsonConstruct(json.c_str()); |
| REQUIRE(doc.is_object()); |
| REQUIRE(doc.size() == 500); |
| int v = -1; |
| REQUIRE(tinygltf::detail::GetInt(doc["key0"], v)); |
| REQUIRE(v == 0); |
| REQUIRE(tinygltf::detail::GetInt(doc["key499"], v)); |
| REQUIRE(v == 499); |
| } |
| |
| TEST_CASE("cj-long-string", "[customjson][stress]") { |
| // String with 10000 characters |
| std::string content(10000, 'x'); |
| std::string json = "\"" + content + "\""; |
| auto doc = JsonConstruct(json.c_str()); |
| REQUIRE(doc.is_string()); |
| REQUIRE(doc.get<std::string>().size() == 10000); |
| } |
| |
| TEST_CASE("cj-many-escapes", "[customjson][stress]") { |
| // String with many escape sequences |
| std::string json = "\""; |
| for (int i = 0; i < 500; i++) { |
| json += "\\n\\t"; |
| } |
| json += "\""; |
| auto doc = JsonConstruct(json.c_str()); |
| REQUIRE(doc.is_string()); |
| REQUIRE(doc.get<std::string>().size() == 1000); |
| } |
| |
| /* ===== Section 10: glTF complex models ==================================== */ |
| |
| TEST_CASE("cj-gltf-multiple-meshes-primitives", "[customjson][gltf]") { |
| const char *json = R"({ |
| "asset": {"version": "2.0"}, |
| "meshes": [ |
| {"primitives": [{"attributes": {"POSITION": 0}, "mode": 4}]}, |
| {"primitives": [ |
| {"attributes": {"POSITION": 0}, "mode": 4}, |
| {"attributes": {"POSITION": 0, "NORMAL": 1}, "mode": 4} |
| ]} |
| ], |
| "accessors": [ |
| {"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3", |
| "max": [1,1,1], "min": [0,0,0]}, |
| {"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3", |
| "max": [1,1,1], "min": [0,0,0]} |
| ], |
| "bufferViews": [{"buffer": 0, "byteLength": 36}], |
| "buffers": [{"uri": "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "byteLength": 36}] |
| })"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| REQUIRE(model.meshes.size() == 2); |
| REQUIRE(model.meshes[0].primitives.size() == 1); |
| REQUIRE(model.meshes[1].primitives.size() == 2); |
| } |
| |
| TEST_CASE("cj-gltf-animations", "[customjson][gltf]") { |
| const char *json = R"({ |
| "asset": {"version": "2.0"}, |
| "animations": [{ |
| "channels": [{"sampler": 0, "target": {"node": 0, "path": "translation"}}], |
| "samplers": [{"input": 0, "output": 1, "interpolation": "LINEAR"}] |
| }], |
| "nodes": [{"name": "AnimNode"}], |
| "accessors": [ |
| {"bufferView": 0, "componentType": 5126, "count": 2, "type": "SCALAR", |
| "max": [1.0], "min": [0.0]}, |
| {"bufferView": 0, "componentType": 5126, "count": 2, "type": "VEC3", |
| "max": [1,1,1], "min": [0,0,0]} |
| ], |
| "bufferViews": [{"buffer": 0, "byteLength": 24}], |
| "buffers": [{"uri": "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "byteLength": 24}] |
| })"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| REQUIRE(model.animations.size() == 1); |
| REQUIRE(model.animations[0].channels.size() == 1); |
| REQUIRE(model.animations[0].samplers.size() == 1); |
| REQUIRE(model.animations[0].channels[0].target_path == "translation"); |
| } |
| |
| TEST_CASE("cj-gltf-cameras", "[customjson][gltf]") { |
| const char *json = R"({ |
| "asset": {"version": "2.0"}, |
| "cameras": [ |
| { |
| "type": "perspective", |
| "perspective": { |
| "aspectRatio": 1.5, |
| "yfov": 0.66, |
| "zfar": 100.0, |
| "znear": 0.01 |
| } |
| }, |
| { |
| "type": "orthographic", |
| "orthographic": { |
| "xmag": 10.0, |
| "ymag": 10.0, |
| "zfar": 100.0, |
| "znear": 0.01 |
| } |
| } |
| ] |
| })"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| REQUIRE(model.cameras.size() == 2); |
| REQUIRE(model.cameras[0].type == "perspective"); |
| REQUIRE(model.cameras[1].type == "orthographic"); |
| } |
| |
| TEST_CASE("cj-gltf-skins", "[customjson][gltf]") { |
| const char *json = R"({ |
| "asset": {"version": "2.0"}, |
| "nodes": [ |
| {"name": "Armature"}, |
| {"name": "Bone1"}, |
| {"name": "Bone2"} |
| ], |
| "skins": [{ |
| "joints": [1, 2], |
| "skeleton": 0 |
| }] |
| })"; |
| tinygltf::Model model; |
| std::string err, warn; |
| REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn)); |
| REQUIRE(model.skins.size() == 1); |
| REQUIRE(model.skins[0].joints.size() == 2); |
| REQUIRE(model.skins[0].skeleton == 0); |
| } |
| |
| /* ===== Section 11: Truncated glTF JSON at various positions =============== */ |
| |
| TEST_CASE("cj-gltf-truncated-at-key", "[customjson][truncated]") { |
| REQUIRE(false == LoadGltfFromString("{\"asset\"")); |
| } |
| |
| TEST_CASE("cj-gltf-truncated-at-colon", "[customjson][truncated]") { |
| REQUIRE(false == LoadGltfFromString("{\"asset\":")); |
| } |
| |
| TEST_CASE("cj-gltf-truncated-mid-value", "[customjson][truncated]") { |
| REQUIRE(false == LoadGltfFromString("{\"asset\":{\"ver")); |
| } |
| |
| TEST_CASE("cj-gltf-truncated-after-comma", "[customjson][truncated]") { |
| REQUIRE(false == |
| LoadGltfFromString("{\"asset\":{\"version\":\"2.0\"},")); |
| } |
| |
| /* ===== Section 12: JSON dump / serialisation ============================== */ |
| |
| TEST_CASE("cj-dump-null", "[customjson][dump]") { |
| auto doc = JsonConstruct("null"); |
| std::string s = doc.dump(-1); |
| REQUIRE(s == "null"); |
| } |
| |
| TEST_CASE("cj-dump-bool", "[customjson][dump]") { |
| auto doc = JsonConstruct("true"); |
| REQUIRE(doc.dump(-1) == "true"); |
| auto doc2 = JsonConstruct("false"); |
| REQUIRE(doc2.dump(-1) == "false"); |
| } |
| |
| TEST_CASE("cj-dump-int", "[customjson][dump]") { |
| auto doc = JsonConstruct("42"); |
| REQUIRE(doc.dump(-1) == "42"); |
| } |
| |
| TEST_CASE("cj-dump-string", "[customjson][dump]") { |
| auto doc = JsonConstruct("\"hello\""); |
| std::string s = doc.dump(-1); |
| REQUIRE(s == "\"hello\""); |
| } |
| |
| TEST_CASE("cj-dump-array", "[customjson][dump]") { |
| auto doc = JsonConstruct("[1,2,3]"); |
| std::string s = doc.dump(-1); |
| // Should contain all three values |
| REQUIRE(s.find("1") != std::string::npos); |
| REQUIRE(s.find("2") != std::string::npos); |
| REQUIRE(s.find("3") != std::string::npos); |
| } |
| |
| TEST_CASE("cj-dump-object", "[customjson][dump]") { |
| auto doc = JsonConstruct("{\"key\":\"val\"}"); |
| std::string s = doc.dump(-1); |
| REQUIRE(s.find("\"key\"") != std::string::npos); |
| REQUIRE(s.find("\"val\"") != std::string::npos); |
| } |
| |
| TEST_CASE("cj-dump-roundtrip", "[customjson][dump]") { |
| const char *input = "{\"a\":[1,2,3],\"b\":{\"c\":true}}"; |
| auto doc = JsonConstruct(input); |
| std::string s = doc.dump(-1); |
| // Re-parse the dump output |
| auto doc2 = JsonConstruct(s.c_str()); |
| REQUIRE(doc2.is_object()); |
| REQUIRE(doc2["a"].is_array()); |
| REQUIRE(doc2["a"].size() == 3); |
| REQUIRE(doc2["b"]["c"].get<bool>() == true); |
| } |
| |
| /* ===== Section 13: Fuzz-like systematic truncation ======================== */ |
| |
| TEST_CASE("cj-systematic-truncation", "[customjson][fuzzlike]") { |
| // Take a valid JSON string and verify the parser doesn't crash |
| // at every truncation point |
| const char *valid_json = |
| R"({"asset":{"version":"2.0"},"nodes":[{"name":"test","translation":[1.0,2.0,3.0]}]})"; |
| size_t len = strlen(valid_json); |
| |
| for (size_t i = 0; i < len; i++) { |
| std::string truncated(valid_json, i); |
| tinygltf::detail::JsonDocument doc; |
| // Should not crash; may fail gracefully |
| tinygltf::detail::JsonParse(doc, truncated.c_str(), truncated.size()); |
| // No assertion on result - just checking it doesn't crash/hang |
| } |
| } |
| |
| TEST_CASE("cj-systematic-byte-flip", "[customjson][fuzzlike]") { |
| // Take a valid JSON string and flip each byte, verify no crash |
| const char *valid_json = R"({"asset":{"version":"2.0"}})"; |
| size_t len = strlen(valid_json); |
| |
| for (size_t i = 0; i < len; i++) { |
| std::string mutated(valid_json, len); |
| mutated[i] = static_cast<char>(mutated[i] ^ 0xFF); |
| tinygltf::detail::JsonDocument doc; |
| tinygltf::detail::JsonParse(doc, mutated.c_str(), mutated.size()); |
| // No assertion - just checking it doesn't crash |
| } |
| } |
| |
| TEST_CASE("cj-systematic-null-insertion", "[customjson][fuzzlike]") { |
| // Insert null bytes at various positions |
| const char *valid_json = R"({"a":"b","c":1})"; |
| size_t len = strlen(valid_json); |
| |
| for (size_t i = 0; i <= len; i++) { |
| std::string mutated(valid_json, len); |
| mutated.insert(i, 1, '\0'); |
| tinygltf::detail::JsonDocument doc; |
| tinygltf::detail::JsonParse(doc, mutated.c_str(), mutated.size()); |
| // No assertion - just checking it doesn't crash |
| } |
| } |