Files
weaselbot abeaae7ed7
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-arm64, ubuntu-latest-arm64, true) (pull_request) Successful in 51s
CI / pre-commit (pull_request) Successful in 53s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-arm64, ubuntu-latest-arm64, false) (pull_request) Successful in 51s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-amd64, ubuntu-latest-amd64, true) (pull_request) Successful in 1m34s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-amd64, ubuntu-latest-amd64, false) (pull_request) Successful in 1m27s
schemagen: handle WeaselJsonParser_create failure in RootBuilder
If WeaselJsonParser_create returns nullptr (e.g. negative stack size or allocation failure), set the existing error_ flag so that subsequent feed()/finish() calls return WeaselJson_REJECT instead of dereferencing the null parser_.

Also add a regression test in test_gen.cpp that constructs a RootBuilder with an invalid stack size and verifies it rejects without crashing.

Closes #36
2026-06-30 11:26:59 -04:00

209 lines
7.6 KiB
C++

// Test harness for the generated example.schema.json parser.
// Feeds input one byte at a time to exercise chunked string/number paths.
#include <cassert>
#include <cstdio>
#include <string>
#include "gen.h"
using namespace weasel_schema;
// Feed `in` one byte at a time. Returns final status.
static WeaselJsonStatus parseStrided(RootBuilder &b, std::string in) {
for (size_t i = 0; i < in.size(); ++i) {
char c = in[i];
WeaselJsonStatus s = b.feed(&c, 1);
if (s != WeaselJson_AGAIN)
return s;
}
return b.finish();
}
static int failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
++failures; \
} \
} while (0)
static void expectReject(const std::string &json, const char *what) {
RootBuilder b;
WeaselJsonStatus s = parseStrided(b, json);
if (s == WeaselJson_REJECT) {
printf("ok reject: %s\n", what);
} else {
printf("FAIL expected reject (%s) got status %d for: %s\n", what, s,
json.c_str());
++failures;
}
}
int main() {
// ---- happy path, full feature coverage, byte-strided ----
{
std::string json = R"({
"name": "Ada É",
"age": 36,
"height": 1.75,
"active": true,
"nickname": null,
"role": "admin",
"hobbies": ["math", "lace"],
"address": { "city": "London", "zip": 12345 },
"friends": [
{ "name": "Bob" },
{ "name": "Cay", "age": 40 }
],
"tree": {
"value": 1,
"children": [ { "value": 2 }, { "value": 3 } ],
"next": { "value": 99 }
},
"extra_unknown": { "ignored": [1, 2, {"x": "y"}] }
})";
// Root is strict (additionalProperties:false), so "extra_unknown" must
// make it reject. Verify that, then test a clean version.
expectReject(json, "unknown key in strict root");
}
// ---- invalid stack size is rejected without crashing ----
{
RootBuilder b(-1);
char buf[] = "null";
WeaselJsonStatus s = b.feed(buf, sizeof(buf) - 1);
CHECK(s == WeaselJson_REJECT);
printf("ok invalid stack size rejected, not crashed\n");
}
{
std::string json = R"({
"name": "Ada É",
"age": 36,
"height": 1.75,
"active": true,
"nickname": null,
"role": "admin",
"hobbies": ["math", "lace"],
"address": { "city": "London", "zip": 12345 },
"friends": [
{ "name": "Bob" },
{ "name": "Cay", "age": 40 }
],
"tree": {
"value": 1,
"children": [ { "value": 2 }, { "value": 3 } ],
"next": { "value": 99 }
},
"nullable_hobbies": [null, "math", null, "lace", null],
"nullable_scores": [null, 10, null, 20, null]
})";
RootBuilder b;
WeaselJsonStatus s = parseStrided(b, json);
CHECK(s == WeaselJson_OK);
if (s == WeaselJson_OK) {
Root r = b.take();
CHECK(r.name == "Ada \xC3\x89"); // É -> UTF-8
CHECK(r.age == 36);
CHECK(r.height > 1.74 && r.height < 1.76);
CHECK(r.active.has_value() && *r.active == true);
CHECK(!r.nickname.has_value()); // explicit null
CHECK(r.role.has_value() && *r.role == Role::admin);
CHECK(r.hobbies.has_value() && r.hobbies->size() == 2);
CHECK(r.hobbies && (*r.hobbies)[0] == "math" &&
(*r.hobbies)[1] == "lace");
CHECK(r.address.has_value() && r.address->city == "London");
CHECK(r.address && r.address->zip.has_value() &&
*r.address->zip == 12345);
CHECK(r.friends.has_value() && r.friends->size() == 2);
CHECK(r.friends && (*r.friends)[0].name == "Bob");
CHECK(r.friends && !(*r.friends)[0].age.has_value());
CHECK(r.friends && (*r.friends)[1].age.has_value() &&
*(*r.friends)[1].age == 40);
CHECK(r.tree.has_value() && r.tree->value == 1);
CHECK(r.tree && r.tree->children.has_value() &&
r.tree->children->size() == 2);
CHECK(r.tree && r.tree->children && (*r.tree->children)[0].value == 2);
CHECK(r.tree && r.tree->children && (*r.tree->children)[1].value == 3);
CHECK(r.tree && r.tree->next && r.tree->next->value == 99);
CHECK(r.nullable_hobbies.has_value() && r.nullable_hobbies->size() == 5);
CHECK(r.nullable_hobbies && !(*r.nullable_hobbies)[0] &&
(*r.nullable_hobbies)[1] && *(*r.nullable_hobbies)[1] == "math" &&
!(*r.nullable_hobbies)[2] && (*r.nullable_hobbies)[3] &&
*(*r.nullable_hobbies)[3] == "lace" && !(*r.nullable_hobbies)[4]);
CHECK(r.nullable_scores.has_value() && r.nullable_scores->size() == 5);
CHECK(r.nullable_scores && !(*r.nullable_scores)[0] &&
(*r.nullable_scores)[1] && *(*r.nullable_scores)[1] == 10 &&
!(*r.nullable_scores)[2] && (*r.nullable_scores)[3] &&
*(*r.nullable_scores)[3] == 20 && !(*r.nullable_scores)[4]);
printf("ok happy path (byte-strided)\n");
}
}
// ---- whole-buffer feed (not strided) ----
{
std::string json = R"({"name":"x","age":7})";
RootBuilder b;
WeaselJsonStatus s = b.feed(json.data(), (int)json.size());
if (s == WeaselJson_AGAIN)
s = b.finish();
CHECK(s == WeaselJson_OK);
Root r = b.take();
CHECK(r.name == "x" && r.age == 7);
CHECK(!r.role.has_value() && !r.hobbies.has_value());
printf("ok minimal object\n");
}
// ---- integer accepts any number with no fractional part (JSON Schema) ----
{
struct {
const char *json;
int64_t age;
} cases[] = {
{R"({"name":"x","age":4e1})", 40},
{R"({"name":"x","age":2.0})", 2},
{R"({"name":"x","age":-1.5e1})", -15},
{R"({"name":"x","age":0e0})", 0},
};
for (auto &c : cases) {
RootBuilder b;
WeaselJsonStatus s = parseStrided(b, c.json);
CHECK(s == WeaselJson_OK);
if (s == WeaselJson_OK) {
Root r = b.take();
CHECK(r.age == c.age);
}
}
printf("ok integer in exponent/decimal form\n");
}
// a fractional or out-of-range value is still rejected for an integer slot
expectReject(R"({"name":"x","age":2.5e0})", "non-integral exponent form");
expectReject(R"({"name":"x","age":1e30})", "integer out of int64 range");
// ---- schema-violation rejections ----
expectReject(R"({"name":"x"})", "missing required field 'age'");
expectReject(R"({"name":"x","age":"notnum"})", "wrong type (string for int)");
expectReject(R"({"name":"x","age":1,"role":"boss"})", "bad enum value");
expectReject(R"({"name":"x","age":1,"name":"y"})", "duplicate key");
expectReject(R"({"name":"x","age":1,"active":null})",
"null for non-nullable");
expectReject(R"({"name":"x","age":1,"age":2})", "duplicate required key");
expectReject(R"({"name":"x","age":1.5})", "fractional for integer");
expectReject(R"({"name":"x","age":1,"address":{"zip":5}})",
"missing required nested 'city'");
expectReject(R"([1,2,3])", "array where object expected (root)");
expectReject("null", "null where object expected (root)");
expectReject("true", "boolean where object expected (root)");
expectReject("123", "number where object expected (root)");
expectReject(R"("hi")", "string where object expected (root)");
expectReject(R"({"name":"x","age":1,)", "truncated / invalid json");
if (failures == 0) {
printf("\nALL TESTS PASSED\n");
return 0;
}
printf("\n%d FAILURE(S)\n", failures);
return 1;
}