Files
tinygltf/tests/v3/tester_v3.cc

613 lines
18 KiB
C++

/*
* tester_v3.cc — Unit tests for tiny_gltf_v3.h (v3 API), including
* the tg3_validate() validation feature.
*/
#define TINYGLTF3_IMPLEMENTATION
#include "tiny_gltf_v3.h"
#define CATCH_CONFIG_MAIN
#include "../catch.hpp"
#include <cstring>
#include <cstdio>
/* ======================================================================
* Helpers
* ====================================================================== */
/* Build a minimal valid glTF 2.0 JSON string */
static const char *MINIMAL_GLTF_JSON =
"{\"asset\":{\"version\":\"2.0\"}}";
/* Parse a JSON string into a tg3_model, returns error code. */
static tg3_error_code parse_json(tg3_model *model, tg3_error_stack *errors,
const char *json) {
tg3_parse_options opts;
tg3_parse_options_init(&opts);
opts.required_sections = TG3_NO_REQUIRE;
return tg3_parse(model, errors,
(const uint8_t *)json, (uint64_t)strlen(json),
"", 0, &opts);
}
/* ======================================================================
* Tests: tg3_validate() — basic API behaviour
* ====================================================================== */
TEST_CASE("v3-validate-null-model", "[v3][validate]") {
tg3_error_stack errors;
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(NULL, &errors);
REQUIRE(rc == TG3_ERR_INVALID_VALUE);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-null-errors", "[v3][validate]") {
tg3_model model;
memset(&model, 0, sizeof(model));
model.default_scene = -1;
/* errors may be NULL — results are silently discarded */
tg3_error_code rc = tg3_validate(&model, NULL);
/* Empty model (no asset.version) should still return an error code */
REQUIRE(rc != TG3_OK); /* missing asset.version */
tg3_model_free(&model);
}
TEST_CASE("v3-validate-minimal-valid", "[v3][validate]") {
tg3_model model;
tg3_error_stack errors;
memset(&model, 0, sizeof(model));
tg3_error_stack_init(&errors);
tg3_error_code rc = parse_json(&model, &errors, MINIMAL_GLTF_JSON);
REQUIRE(rc == TG3_OK);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_OK);
REQUIRE(tg3_errors_has_error(&errors) == 0);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: asset.version validation
* ====================================================================== */
TEST_CASE("v3-validate-missing-asset-version", "[v3][validate][asset]") {
tg3_model model;
tg3_error_stack errors;
memset(&model, 0, sizeof(model));
tg3_error_stack_init(&errors);
/* asset.version.data == NULL means absent */
model.default_scene = -1;
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_MISSING_REQUIRED);
REQUIRE(tg3_errors_has_error(&errors) == 1);
bool found = false;
for (uint32_t i = 0; i < tg3_errors_count(&errors); i++) {
const tg3_error_entry *e = tg3_errors_get(&errors, i);
if (e->code == TG3_ERR_MISSING_REQUIRED) { found = true; break; }
}
REQUIRE(found);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-wrong-asset-version-warn", "[v3][validate][asset]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
/* Parse a model with version "1.0" */
const char *json = "{\"asset\":{\"version\":\"1.0\"}}";
tg3_error_code rc = parse_json(&model, &errors, json);
REQUIRE(rc == TG3_OK);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
rc = tg3_validate(&model, &errors);
/* Wrong version emits a WARNING, not an error */
REQUIRE(rc == TG3_OK);
REQUIRE(tg3_errors_has_error(&errors) == 0);
bool found_warn = false;
for (uint32_t i = 0; i < tg3_errors_count(&errors); i++) {
const tg3_error_entry *e = tg3_errors_get(&errors, i);
if (e->severity == TG3_SEVERITY_WARNING &&
e->code == TG3_ERR_INVALID_VALUE) {
found_warn = true; break;
}
}
REQUIRE(found_warn);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: default scene index
* ====================================================================== */
TEST_CASE("v3-validate-invalid-default-scene", "[v3][validate][scene]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
parse_json(&model, &errors, MINIMAL_GLTF_JSON);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
/* Point default_scene to a non-existent scene */
model.default_scene = 5;
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_INDEX);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: accessor validation
* ====================================================================== */
TEST_CASE("v3-validate-accessor-invalid-component-type", "[v3][validate][accessor]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"accessors\":[{"
" \"componentType\":9999,"
" \"count\":1,"
" \"type\":\"SCALAR\""
"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_ACCESSOR);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-accessor-zero-count", "[v3][validate][accessor]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
/* Build model directly */
parse_json(&model, &errors, MINIMAL_GLTF_JSON);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
/* Inject a fake accessor with count=0 */
tg3_accessor *acc = (tg3_accessor *)calloc(1, sizeof(tg3_accessor));
acc->buffer_view = -1;
acc->component_type = TG3_COMPONENT_TYPE_FLOAT;
acc->type = TG3_TYPE_SCALAR;
acc->count = 0; /* invalid */
model.accessors = acc;
model.accessors_count = 1;
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_ACCESSOR);
/* Restore so tg3_model_free doesn't double-free */
model.accessors = NULL;
model.accessors_count = 0;
free(acc);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-accessor-invalid-bufferView-index", "[v3][validate][accessor]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"accessors\":[{"
" \"bufferView\":99,"
" \"componentType\":5126,"
" \"count\":1,"
" \"type\":\"SCALAR\""
"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_INDEX);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: mesh validation
* ====================================================================== */
TEST_CASE("v3-validate-mesh-no-primitives", "[v3][validate][mesh]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"meshes\":[{\"primitives\":[]}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_MESH);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-mesh-indices-out-of-range", "[v3][validate][mesh]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"accessors\":["
" {\"componentType\":5126,\"count\":3,\"type\":\"VEC3\"}"
"],"
"\"meshes\":[{"
" \"primitives\":[{"
" \"attributes\":{\"POSITION\":0},"
" \"indices\":99"
" }]"
"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_INDEX);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: node validation
* ====================================================================== */
TEST_CASE("v3-validate-node-invalid-mesh-index", "[v3][validate][node]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"nodes\":[{\"mesh\":42}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_INDEX);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-node-self-reference", "[v3][validate][node]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"nodes\":[{\"children\":[0]}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_NODE);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: texture validation
* ====================================================================== */
TEST_CASE("v3-validate-texture-invalid-source", "[v3][validate][texture]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"textures\":[{\"source\":99}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_INDEX);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: material validation
* ====================================================================== */
TEST_CASE("v3-validate-material-invalid-alpha-mode", "[v3][validate][material]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"materials\":[{\"alphaMode\":\"INVALID\"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_MATERIAL);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: camera validation
* ====================================================================== */
TEST_CASE("v3-validate-camera-perspective-bad-yfov", "[v3][validate][camera]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"cameras\":[{"
" \"type\":\"perspective\","
" \"perspective\":{\"yfov\":0.0,\"znear\":0.01,\"zfar\":100.0,"
" \"aspectRatio\":1.0}"
"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_CAMERA);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-camera-invalid-type", "[v3][validate][camera]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"cameras\":[{\"type\":\"unknown\"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_CAMERA);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: scene validation
* ====================================================================== */
TEST_CASE("v3-validate-scene-invalid-node-index", "[v3][validate][scene]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"scenes\":[{\"nodes\":[99]}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_SCENE);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: animation validation
* ====================================================================== */
TEST_CASE("v3-validate-animation-invalid-sampler-ref", "[v3][validate][animation]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"accessors\":["
" {\"componentType\":5126,\"count\":2,\"type\":\"SCALAR\"},"
" {\"componentType\":5126,\"count\":2,\"type\":\"VEC3\"}"
"],"
"\"animations\":[{"
" \"samplers\":[{\"input\":0,\"output\":1,\"interpolation\":\"LINEAR\"}],"
" \"channels\":[{"
" \"sampler\":99,"
" \"target\":{\"path\":\"translation\"}"
" }]"
"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_INDEX);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
TEST_CASE("v3-validate-animation-invalid-target-path", "[v3][validate][animation]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"accessors\":["
" {\"componentType\":5126,\"count\":2,\"type\":\"SCALAR\"},"
" {\"componentType\":5126,\"count\":2,\"type\":\"VEC3\"}"
"],"
"\"animations\":[{"
" \"samplers\":[{\"input\":0,\"output\":1,\"interpolation\":\"LINEAR\"}],"
" \"channels\":[{"
" \"sampler\":0,"
" \"target\":{\"path\":\"bogus\"}"
" }]"
"}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_error_code rc = tg3_validate(&model, &errors);
REQUIRE(rc == TG3_ERR_INVALID_VALUE);
REQUIRE(tg3_errors_has_error(&errors) == 1);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: error stack msg_arena_ lifetime
* ====================================================================== */
TEST_CASE("v3-validate-error-messages-persist", "[v3][validate][errorstack]") {
tg3_model model;
tg3_error_stack errors;
tg3_error_stack_init(&errors);
/* Trigger multiple validation errors */
const char *json =
"{"
"\"asset\":{\"version\":\"2.0\"},"
"\"nodes\":[{\"mesh\":99,\"camera\":88}],"
"\"textures\":[{\"source\":77}]"
"}";
parse_json(&model, &errors, json);
tg3_error_stack_free(&errors);
tg3_error_stack_init(&errors);
tg3_validate(&model, &errors);
uint32_t count = tg3_errors_count(&errors);
REQUIRE(count >= 3);
/* All message and path pointers must be non-NULL and readable */
for (uint32_t i = 0; i < count; i++) {
const tg3_error_entry *e = tg3_errors_get(&errors, i);
REQUIRE(e != NULL);
REQUIRE(e->message != NULL);
/* Sanity: message should not be empty */
REQUIRE(strlen(e->message) > 0);
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
}
/* ======================================================================
* Tests: C++ wrapper
* ====================================================================== */
TEST_CASE("v3-validate-cpp-wrapper", "[v3][validate][cpp]") {
tinygltf3::Model model;
tinygltf3::ErrorStack errors;
const char *json = "{\"asset\":{\"version\":\"2.0\"}}";
tg3_parse_options opts;
tg3_parse_options_init(&opts);
opts.required_sections = TG3_NO_REQUIRE;
tg3_parse_auto(model.get(), errors.get(),
(const uint8_t *)json, strlen(json),
"", 0, &opts);
tg3_error_code rc = tinygltf3::validate(model, errors);
REQUIRE(rc == TG3_OK);
REQUIRE(errors.has_error() == false);
}