emberemu/tests/Protogen.cpp

1677 lines
49 KiB
C++

/*
* Copyright (c) 2026 Ember
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <protogen/Validator.h>
#include <protogen/Generator.h>
#include <jsoncons/json.hpp>
#include <gtest/gtest.h>
#include <array>
#include <filesystem>
#include <string>
using namespace ember;
static const std::filesystem::path templates_dir { "test_data/templates/" };
constexpr auto types_json = R"({
"types": {
"Result": {
"kind": "enum",
"underlying": "uint8",
"values": { "ok": 0, "queued": 1, "failed": 2 }
},
"MovementFlags": {
"kind": "flags",
"underlying": "uint32",
"values": { "swimming": 1, "jumping": 2, "flying": 4 }
},
"Vector3": {
"kind": "struct",
"fields": [
{ "name": "x", "type": "float" },
{ "name": "y", "type": "float" },
{ "name": "z", "type": "float" }
]
},
"Movement": {
"kind": "struct",
"fields": [
{ "name": "flags", "type": "MovementFlags" },
{ "name": "pos", "type": "Vector3" },
{
"type": "group",
"when": { "op": "has_flag", "field": "flags", "value": "swimming" },
"fields": [ { "name": "pitch", "type": "float" } ]
}
]
},
"CString": {
"kind": "string",
"encoding": "null_terminated"
}
}
})";
jsoncons::json parse_fields(std::string_view body) {
const auto doc = std::string(R"({"fields":)") + std::string(body) + "}";
return jsoncons::json::parse(doc);
}
class Protogen : public ::testing::Test {
public:
void SetUp() override {
auto types_doc = jsoncons::json::parse(types_json);
reg = protogen::build_registry(types_doc);
}
protogen::TypeRegistry reg;
};
TEST_F(Protogen, RegistryInternalsValidate) {
ASSERT_NO_THROW(validate_registry_internals(reg));
}
TEST_F(Protogen, PrimitivesOnly) {
const auto msg = parse_fields(R"([
{ "name": "a", "type": "uint8" },
{ "name": "b", "type": "uint32" },
{ "name": "c", "type": "double" }
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, CustomTypeReference) {
const auto msg = parse_fields(R"([
{ "name": "result", "type": "Result" },
{ "name": "flags", "type": "MovementFlags" },
{ "name": "pos", "type": "Vector3" }
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, GroupWithEqCondition) {
const auto msg = parse_fields(R"([
{ "name": "result", "type": "Result" },
{
"type": "group",
"when": { "op": "eq", "field": "result", "value": "ok" },
"fields": [ { "name": "payload", "type": "uint32" } ]
}
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, GroupWithInCondition) {
const auto msg = parse_fields(R"([
{ "name": "result", "type": "Result" },
{
"type": "group",
"when": { "op": "in", "field": "result", "value": ["ok", "queued"] },
"fields": [ { "name": "payload", "type": "uint32" } ]
}
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, GroupWithHasFlagCondition) {
const auto msg = parse_fields(R"([
{ "name": "flags", "type": "MovementFlags" },
{
"type": "group",
"when": { "op": "has_flag", "field": "flags", "value": "jumping" },
"fields": [ { "name": "z_speed", "type": "float" } ]
}
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, EqConditionOnPrimitiveWithIntegerValue) {
const auto msg = parse_fields(R"([
{ "name": "version", "type": "uint8" },
{
"type": "group",
"when": { "op": "eq", "field": "version", "value": 3 },
"fields": [ { "name": "payload", "type": "uint32" } ]
}
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, NestedGroups) {
const auto msg = parse_fields(R"([
{ "name": "result", "type": "Result" },
{ "name": "flags", "type": "MovementFlags" },
{
"type": "group",
"when": { "op": "eq", "field": "result", "value": "ok" },
"fields": [
{
"type": "group",
"when": { "op": "has_flag", "field": "flags", "value": "flying" },
"fields": [ { "name": "altitude", "type": "float" } ]
}
]
}
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, StructWithInternalConditionInRegistry) {
// 'Movement' in the fixture carries its own has_flag group referencing flags
// validate_registry_internals is what sanity-checks that internal condition
ASSERT_NO_THROW(validate_registry_internals(reg));
}
TEST_F(Protogen, RejectsUnknownType) {
auto msg = parse_fields(R"([
{ "name": "x", "type": "Nonexistent" }
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RejectsConditionOnUnknownField) {
auto msg = parse_fields(R"([
{ "name": "a", "type": "uint8" },
{
"type": "group",
"when": { "op": "eq", "field": "does_not_exist", "value": 1 },
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RejectsHasFlagOnEnum) {
auto msg = parse_fields(R"([
{ "name": "result", "type": "Result" },
{
"type": "group",
"when": { "op": "has_flag", "field": "result", "value": "ok" },
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RejectsHasFlagOnPrimitive) {
auto msg = parse_fields(R"([
{ "name": "raw_flags", "type": "uint32" },
{
"type": "group",
"when": { "op": "has_flag", "field": "raw_flags", "value": 1 },
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RejectsUnknownEnumeratorInEq) {
auto msg = parse_fields(R"([
{ "name": "result", "type": "Result" },
{
"type": "group",
"when": { "op": "eq", "field": "result", "value": "bogus_name" },
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RejectsUnknownEnumeratorInInList) {
auto msg = parse_fields(R"([
{ "name": "result", "type": "Result" },
{
"type": "group",
"when": { "op": "in", "field": "result", "value": ["ok", "not_real"] },
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RejectsConditionOnStructField) {
auto msg = parse_fields(R"([
{ "name": "pos", "type": "Vector3" },
{
"type": "group",
"when": { "op": "eq", "field": "pos", "value": 0 },
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RejectsPostGroupReferenceToGroupLocalField) {
// `pitch_flags` is declared inside a conditional group, so it's only on the
// wire when `flags` has `swimming`. A later group's `when` mustn't be able
// to reference it — validation should reject the whole message.
auto msg = parse_fields(R"([
{ "name": "flags", "type": "MovementFlags" },
{
"type": "group",
"when": { "op": "has_flag", "field": "flags", "value": "swimming" },
"fields": [ { "name": "pitch_flags", "type": "MovementFlags" } ]
},
{
"type": "group",
"when": { "op": "has_flag", "field": "pitch_flags", "value": "jumping" },
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RejectsNamedValueAgainstPrimitive) {
auto msg = parse_fields(R"([
{ "name": "a", "type": "uint8" },
{
"type": "group",
"when": { "op": "eq", "field": "a", "value": "some_name" },
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, GeneratesStructAndUsingAlias) {
auto msg = jsoncons::json::parse(R"({
"name": "Ping",
"opcode": "cmsg_ping",
"direction": "client",
"fields": [
{ "name": "ping", "type": "uint32" },
{ "name": "latency","type": "uint32" }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_EQ(out.relative_path, "client/Ping.h");
EXPECT_NE(out.content.find("struct Ping final"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("std::uint32_t ping;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> ping;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream << latency;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find(
"using cmsg_ping = ClientPacket<ClientOpcode::cmsg_ping, client::Ping>;"
), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesConditionalGroupAsIfBlock) {
auto msg = jsoncons::json::parse(R"({
"name": "AuthResult",
"opcode": "smsg_auth_result",
"direction": "server",
"fields": [
{ "name": "result", "type": "Result" },
{
"type": "group",
"when": { "op": "eq", "field": "result", "value": "ok" },
"fields": [ { "name": "extra", "type": "uint32" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_EQ(out.relative_path, "server/AuthResult.h");
EXPECT_NE(out.content.find("if(result == Result::ok) {"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> extra;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream << extra;"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesHasFlagPredicate) {
auto msg = jsoncons::json::parse(R"({
"name": "FlagGated",
"opcode": "smsg_flag_gated",
"direction": "server",
"fields": [
{ "name": "flags", "type": "MovementFlags" },
{
"type": "group",
"when": { "op": "has_flag", "field": "flags", "value": "jumping" },
"fields": [ { "name": "z_speed", "type": "float" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("if(flags & MovementFlags::jumping) {"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesInPredicateAsOrChain) {
auto msg = jsoncons::json::parse(R"({
"name": "InGated",
"opcode": "smsg_in_gated",
"direction": "server",
"fields": [
{ "name": "result", "type": "Result" },
{
"type": "group",
"when": { "op": "in", "field": "result", "value": ["ok", "queued"] },
"fields": [ { "name": "extra", "type": "uint32" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("result == Result::ok || result == Result::queued"), std::string::npos) << out.content;
}
TEST_F(Protogen, InlinesStructExpansionAtStreamSite) {
// registry fixture's 'Movement' struct contains flags + pos + conditional pitch
// A field of type 'Movement' should inline-expand into 'info.flags', 'info.pos.x',
// 'info.pos.y', 'info.pos.z' and the swimming conditional block
auto msg = jsoncons::json::parse(R"({
"name": "MovementPacket",
"opcode": "smsg_movement_packet",
"direction": "server",
"fields": [
{ "name": "info", "type": "Movement" }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("Movement info;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> info.flags;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> info.pos.x;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("if(info.flags & MovementFlags::swimming) {"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> info.pitch;"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesOneHeaderPerEnumFlagsAndStruct) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"Colour": {
"kind": "enum",
"underlying": "uint8",
"cpp_namespace": "graphics",
"values": { "red": 0, "green": 1 }
},
"Perms": {
"kind": "flags",
"underlying": "uint32",
"cpp_namespace": "sys",
"values": { "read": 1, "write": 2 }
},
"Point": {
"kind": "struct",
"cpp_namespace": "geom",
"fields": [
{ "name": "x", "type": "int32" },
{ "name": "y", "type": "int32" }
]
},
"CString": {
"kind": "string",
"encoding": "null_terminated"
}
}
})");
auto reg = protogen::build_registry(types_doc);
const auto headers = protogen::generate_type_headers(reg, templates_dir);
// one header for each type - string types produce none
ASSERT_EQ(headers.size(), 3u);
auto find_header = [&](std::string_view suffix) -> const protogen::GeneratedFile* {
for(const auto& h : headers) {
if(h.relative_path == suffix) {
return &h;
}
}
return nullptr;
};
const auto* colour = find_header("types/Colour.h");
ASSERT_NE(colour, nullptr);
EXPECT_NE(colour->content.find("namespace graphics"), std::string::npos) << colour->content;
EXPECT_NE(colour->content.find("enum Colour : std::uint8_t"), std::string::npos) << colour->content;
EXPECT_NE(colour->content.find("red = 0"), std::string::npos) << colour->content;
EXPECT_NE(colour->content.find("to_string(Colour value)"), std::string::npos) << colour->content;
EXPECT_NE(colour->content.find("Colour_to_string(Colour value)"), std::string::npos) << colour->content;
const auto* perms = find_header("types/Perms.h");
ASSERT_NE(perms, nullptr);
EXPECT_NE(perms->content.find("enum Perms : std::uint32_t"), std::string::npos) << perms->content;
const auto* point = find_header("types/Point.h");
ASSERT_NE(point, nullptr);
EXPECT_NE(point->content.find("namespace geom"), std::string::npos) << point->content;
EXPECT_NE(point->content.find("struct Point"), std::string::npos) << point->content;
EXPECT_NE(point->content.find("std::int32_t x"), std::string::npos) << point->content;
}
TEST_F(Protogen, ValidatesStreamNotEmptyGroup) {
auto msg = parse_fields(R"([
{ "name": "a", "type": "uint32" },
{
"type": "group",
"when": { "op": "stream_not_empty" },
"fields": [ { "name": "trailing", "type": "uint32" } ]
}
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, GeneratesStreamNotEmptyAsymmetrically) {
auto msg = jsoncons::json::parse(R"({
"name": "TrailingPacket",
"opcode": "smsg_trailing",
"direction": "server",
"fields": [
{ "name": "a", "type": "uint32" },
{
"type": "group",
"when": { "op": "stream_not_empty" },
"fields": [ { "name": "trailing", "type": "uint32" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
// read test
EXPECT_NE(out.content.find("if(!stream.empty()) {"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> trailing;"), std::string::npos) << out.content;
// write test, should be no stream.empty() condition
const auto write_start = out.content.find("write_to_stream");
ASSERT_NE(write_start, std::string::npos) << out.content;
const auto write_body = out.content.substr(write_start);
EXPECT_EQ(write_body.find("if(!stream.empty())"), std::string::npos) << write_body;
EXPECT_NE(write_body.find("stream << trailing;"), std::string::npos) << write_body;
// indentiation test, since it's asymmetrical
EXPECT_NE(write_body.find("\t\tstream << trailing;"), std::string::npos) << write_body;
}
TEST_F(Protogen, StreamNotEmptyGroupStillPopsScope) {
// trailing group's fields must not leak into outer scope
// (same rule as the other group ops)
auto msg = parse_fields(R"([
{ "name": "a", "type": "uint32" },
{
"type": "group",
"when": { "op": "stream_not_empty" },
"fields": [ { "name": "trailing_flags", "type": "MovementFlags" } ]
},
{
"type": "group",
"when": { "op": "has_flag", "field": "trailing_flags", "value": "jumping" },
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, ReferencesExternalType) {
// stand-in for the actual character struct
auto types_doc = jsoncons::json::parse(R"({
"types": {
"Guid": {
"kind": "external",
"include": "shared/Guid.h",
"cpp_namespace": "ember"
}
}
})");
auto reg = protogen::build_registry(types_doc);
auto msg = jsoncons::json::parse(R"({
"name": "CharacterEnumTest",
"opcode": "smsg_character_enum_test",
"direction": "server",
"fields": [
{ "name": "id", "type": "Guid" }
]
})");
const auto out = protogen::generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("#include <shared/Guid.h>"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("ember::Guid id;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> id;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream << id;"), std::string::npos) << out.content;
}
TEST_F(Protogen, NoHeaderGeneratedForExternalType) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"Guid": {
"kind": "external",
"include": "shared/Guid.h",
"cpp_namespace": "ember"
},
"Colour": {
"kind": "enum",
"underlying": "uint8",
"cpp_namespace": "graphics",
"values": { "red": 0 }
}
}
})");
auto reg = protogen::build_registry(types_doc);
const auto headers = protogen::generate_type_headers(reg, templates_dir);
ASSERT_EQ(headers.size(), 1u);
EXPECT_EQ(headers.front().relative_path, "types/Colour.h");
}
TEST_F(Protogen, RejectsExternalInCondition) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"Guid": {
"kind": "external",
"include": "shared/Guid.h",
"cpp_namespace": "ember"
}
}
})");
auto reg = protogen::build_registry(types_doc);
auto fields = jsoncons::json::parse(R"([
{ "name": "id", "type": "Guid" },
{
"type": "group",
"when": { "op": "eq", "field": "id", "value": 0 },
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(fields, reg, "test"));
}
TEST_F(Protogen, AcceptsFixedSizeArrayOfExternal) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"Guid": {
"kind": "external",
"include": "shared/Guid.h",
"cpp_namespace": "ember"
}
}
})");
auto reg = protogen::build_registry(types_doc);
auto fields = jsoncons::json::parse(R"([
{ "name": "ids", "type": "Guid", "array": { "size": 4 } }
])");
EXPECT_NO_THROW(validate_message_fields(fields, reg, "test"));
}
TEST_F(Protogen, StructMemberOfExternalPullsItsInclude) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"Guid": {
"kind": "external",
"include": "shared/Guid.h",
"cpp_namespace": "ember"
},
"Pair": {
"kind": "struct",
"cpp_namespace": "ns",
"fields": [
{ "name": "left", "type": "Guid" },
{ "name": "right", "type": "Guid" }
]
}
}
})");
auto reg = protogen::build_registry(types_doc);
const auto headers = protogen::generate_type_headers(reg, templates_dir);
const protogen::GeneratedFile* pair = nullptr;
for(const auto& h : headers) {
if(h.relative_path == "types/Pair.h") { pair = &h; }
}
ASSERT_NE(pair, nullptr);
EXPECT_NE(pair->content.find("#include <shared/Guid.h>"), std::string::npos) << pair->content;
EXPECT_NE(pair->content.find("ember::Guid left"), std::string::npos) << pair->content;
}
TEST_F(Protogen, StructPullsInGeneratedHeadersForCustomMembers) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"Mode": {
"kind": "flags",
"underlying": "uint8",
"cpp_namespace": "ns",
"values": { "on": 1 }
},
"Inner": {
"kind": "struct",
"cpp_namespace": "ns",
"fields": [ { "name": "value", "type": "uint32" } ]
},
"Outer": {
"kind": "struct",
"cpp_namespace": "ns",
"fields": [
{ "name": "m", "type": "Mode" },
{ "name": "i", "type": "Inner" }
]
}
}
})");
auto reg = protogen::build_registry(types_doc);
const auto headers = protogen::generate_type_headers(reg, templates_dir);
const protogen::GeneratedFile* outer = nullptr;
for(const auto& h : headers) {
if(h.relative_path == "types/Outer.h") { outer = &h; }
}
ASSERT_NE(outer, nullptr);
EXPECT_NE(outer->content.find("#include <protocol/types/Mode.h>"), std::string::npos) << outer->content;
EXPECT_NE(outer->content.find("#include <protocol/types/Inner.h>"), std::string::npos) << outer->content;
}
TEST_F(Protogen, QualifiesEnumAndMemberWithCppNamespace) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"Colour": {
"kind": "enum",
"underlying": "uint8",
"cpp_namespace": "graphics",
"values": { "red": 0, "green": 1 }
}
}
})");
auto reg = protogen::build_registry(types_doc);
auto msg = jsoncons::json::parse(R"({
"name": "ColourPacket",
"opcode": "smsg_colour",
"direction": "server",
"fields": [
{ "name": "c", "type": "Colour" },
{
"type": "group",
"when": { "op": "eq", "field": "c", "value": "red" },
"fields": [ { "name": "extra", "type": "uint32" } ]
}
]
})");
const auto out = protogen::generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("graphics::Colour c;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("c == graphics::Colour::red"), std::string::npos) << out.content;
}
TEST_F(Protogen, AggregatorIncludesEachHeader) {
std::array<std::string, 3> headers {
"server/AuthResponse.h",
"server/Pong.h",
"client/Ping.h",
};
const auto out = protogen::generate_aggregator(headers, templates_dir);
for(const auto& h : headers) {
EXPECT_NE(out.find(std::string("#include <protocol/") + h + ">"), std::string::npos) << out;
}
}
TEST_F(Protogen, ValidatesFixedSizeArray) {
auto msg = parse_fields(R"([
{ "name": "data", "type": "uint32", "array": { "size": 32 } }
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, ValidatesDynamicArrayWithCountField) {
auto msg = parse_fields(R"([
{ "name": "count", "type": "uint8" },
{ "name": "items", "type": "uint32", "array": { "count_field": "count" } }
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RejectsArrayWithUnknownCountField) {
auto msg = parse_fields(R"([
{ "name": "items", "type": "uint32", "array": { "count_field": "missing" } }
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, AcceptsFixedSizeArrayOfStruct) {
auto msg = parse_fields(R"([
{ "name": "positions", "type": "Vector3", "array": { "size": 4 } }
])");
EXPECT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, AcceptsFixedSizeArrayOfFlags) {
auto msg = parse_fields(R"([
{ "name": "masks", "type": "MovementFlags", "array": { "size": 2 } }
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RejectsArrayWithNonIntegerCountField) {
auto msg = parse_fields(R"([
{ "name": "result", "type": "Result" },
{ "name": "items", "type": "uint32", "array": { "count_field": "result" } }
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, GeneratesFixedSizeArray) {
auto msg = jsoncons::json::parse(R"({
"name": "AccountData",
"opcode": "smsg_account_data",
"direction": "server",
"fields": [
{ "name": "data", "type": "uint32", "array": { "size": 32 } }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("std::array<std::uint32_t, 32> data{};"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> data;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream << data;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("#include <array>"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesDynamicVectorWithCountLoop) {
auto msg = jsoncons::json::parse(R"({
"name": "ItemList",
"opcode": "smsg_item_list",
"direction": "server",
"fields": [
{ "name": "count", "type": "uint8" },
{ "name": "items", "type": "uint32", "array": { "count_field": "count" } }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("std::vector<std::uint32_t> items;"), std::string::npos) << out.content;
// the count field is implicit: no struct member, no `stream >> count`,
// no `stream << count` — it lives only as a local on the read side.
EXPECT_EQ(out.content.find("std::uint8_t count;"), std::string::npos) << out.content;
// read: count goes into a local, vector resizes against it, then iterates
EXPECT_NE(out.content.find("std::uint8_t count_"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("items.resize(count_"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("for(auto& e : items) {"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> e;"), std::string::npos) << out.content;
// write: vec.size() cast to count's wire type, then iterate
EXPECT_NE(out.content.find("stream << static_cast<std::uint8_t>(items.size());"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("for(const auto& e : items) {"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream << e;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("#include <vector>"), std::string::npos) << out.content;
}
TEST_F(Protogen, ValidatesNeqCondition) {
const auto msg = parse_fields(R"([
{ "name": "result", "type": "Result" },
{
"type": "group",
"when": { "op": "neq", "field": "result", "value": "ok" },
"fields": [ { "name": "err", "type": "uint32" } ]
}
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, GeneratesNeqPredicate) {
auto msg = jsoncons::json::parse(R"({
"name": "NeqGated",
"opcode": "smsg_neq_gated",
"direction": "server",
"fields": [
{ "name": "result", "type": "Result" },
{
"type": "group",
"when": { "op": "neq", "field": "result", "value": "ok" },
"fields": [ { "name": "err", "type": "uint32" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("if(result != Result::ok) {"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesAndCompoundCondition) {
auto msg = jsoncons::json::parse(R"({
"name": "AndGated",
"opcode": "smsg_and_gated",
"direction": "server",
"fields": [
{ "name": "result", "type": "Result" },
{ "name": "flags", "type": "MovementFlags" },
{
"type": "group",
"when": {
"op": "and",
"conditions": [
{ "op": "eq", "field": "result", "value": "ok" },
{ "op": "has_flag", "field": "flags", "value": "jumping" }
]
},
"fields": [ { "name": "extra", "type": "uint32" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("(result == Result::ok) && (flags & MovementFlags::jumping)"),
std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesOrCompoundCondition) {
auto msg = jsoncons::json::parse(R"({
"name": "OrGated",
"opcode": "smsg_or_gated",
"direction": "server",
"fields": [
{ "name": "a", "type": "uint8" },
{ "name": "b", "type": "uint8" },
{
"type": "group",
"when": {
"op": "or",
"conditions": [
{ "op": "eq", "field": "a", "value": 1 },
{ "op": "eq", "field": "b", "value": 2 }
]
},
"fields": [ { "name": "payload", "type": "uint32" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("(a == 1) || (b == 2)"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesNotCondition) {
auto msg = jsoncons::json::parse(R"({
"name": "NotGated",
"opcode": "smsg_not_gated",
"direction": "server",
"fields": [
{ "name": "flags", "type": "MovementFlags" },
{
"type": "group",
"when": {
"op": "not",
"condition": { "op": "has_flag", "field": "flags", "value": "jumping" }
},
"fields": [ { "name": "grounded", "type": "uint8" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("if(!(flags & MovementFlags::jumping)) {"),
std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesFieldToFieldEq) {
auto msg = jsoncons::json::parse(R"({
"name": "FieldEq",
"opcode": "smsg_field_eq",
"direction": "server",
"fields": [
{ "name": "foo", "type": "uint32" },
{ "name": "bar", "type": "uint32" },
{
"type": "group",
"when": { "op": "eq", "field": "foo", "value": { "field": "bar" } },
"fields": [ { "name": "payload", "type": "uint32" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("if(foo == bar) {"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesFieldToFieldNeq) {
auto msg = jsoncons::json::parse(R"({
"name": "FieldNeq",
"opcode": "smsg_field_neq",
"direction": "server",
"fields": [
{ "name": "foo", "type": "uint32" },
{ "name": "bar", "type": "uint32" },
{
"type": "group",
"when": { "op": "neq", "field": "foo", "value": { "field": "bar" } },
"fields": [ { "name": "payload", "type": "uint32" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("if(foo != bar) {"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesNegatedFieldReference) {
auto msg = jsoncons::json::parse(R"({
"name": "FieldNegate",
"opcode": "smsg_field_negate",
"direction": "server",
"fields": [
{ "name": "foo", "type": "uint32" },
{ "name": "bar", "type": "uint32" },
{
"type": "group",
"when": { "op": "eq", "field": "foo", "value": { "field": "bar", "negate": true } },
"fields": [ { "name": "payload", "type": "uint32" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("if(foo == !bar) {"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesZeroTestViaConstant) {
auto msg = jsoncons::json::parse(R"({
"name": "ZeroTest",
"opcode": "smsg_zero_test",
"direction": "server",
"fields": [
{ "name": "count", "type": "uint32" },
{
"type": "group",
"when": { "op": "neq", "field": "count", "value": 0 },
"fields": [ { "name": "payload", "type": "uint32" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("if(count != 0) {"), std::string::npos) << out.content;
}
TEST_F(Protogen, ValidatesEmptyOnStringField) {
const auto msg = parse_fields(R"([
{ "name": "name", "type": "CString" },
{
"type": "group",
"when": { "op": "not", "condition": { "op": "empty", "field": "name" } },
"fields": [ { "name": "flag", "type": "uint8" } ]
}
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, GeneratesEmptyStringTest) {
auto msg = jsoncons::json::parse(R"({
"name": "EmptyTest",
"opcode": "smsg_empty_test",
"direction": "server",
"fields": [
{ "name": "name", "type": "CString" },
{
"type": "group",
"when": { "op": "not", "condition": { "op": "empty", "field": "name" } },
"fields": [ { "name": "flag", "type": "uint8" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("if(!(name.empty())) {"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesSiblingConditionalGroups) {
auto msg = jsoncons::json::parse(R"({
"name": "Sibling",
"opcode": "smsg_sibling",
"direction": "server",
"fields": [
{ "name": "foo", "type": "uint32" },
{ "name": "bar", "type": "uint32" },
{
"type": "group",
"when": { "op": "neq", "field": "foo", "value": 0 },
"fields": [ { "name": "foo_extra", "type": "uint32" } ]
},
{
"type": "group",
"when": { "op": "neq", "field": "bar", "value": 0 },
"fields": [ { "name": "bar_extra", "type": "uint32" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("if(foo != 0) {"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("if(bar != 0) {"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> foo_extra;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> bar_extra;"), std::string::npos) << out.content;
}
TEST_F(Protogen, RejectsEmptyOnNonStringField) {
auto msg = parse_fields(R"([
{ "name": "count", "type": "uint32" },
{
"type": "group",
"when": { "op": "empty", "field": "count" },
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RejectsFieldRefValueToUnknownField) {
auto msg = parse_fields(R"([
{ "name": "foo", "type": "uint32" },
{
"type": "group",
"when": { "op": "eq", "field": "foo", "value": { "field": "missing" } },
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RejectsFieldRefValueToStructField) {
auto msg = parse_fields(R"([
{ "name": "pos", "type": "Vector3" },
{ "name": "foo", "type": "uint32" },
{
"type": "group",
"when": { "op": "eq", "field": "foo", "value": { "field": "pos" } },
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RejectsNestedStreamNotEmptyInCompound) {
auto msg = parse_fields(R"([
{ "name": "foo", "type": "uint32" },
{
"type": "group",
"when": {
"op": "and",
"conditions": [
{ "op": "eq", "field": "foo", "value": 1 },
{ "op": "stream_not_empty" }
]
},
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, RecursesValidationIntoCompoundConditions) {
// unknown enum value buried inside an AND should still be caught
auto msg = parse_fields(R"([
{ "name": "result", "type": "Result" },
{ "name": "flags", "type": "MovementFlags" },
{
"type": "group",
"when": {
"op": "and",
"conditions": [
{ "op": "has_flag", "field": "flags", "value": "jumping" },
{ "op": "eq", "field": "result", "value": "bogus_name" }
]
},
"fields": [ { "name": "x", "type": "uint8" } ]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, ValidatesIfChain) {
auto msg = parse_fields(R"([
{ "name": "result", "type": "Result" },
{
"type": "if_chain",
"branches": [
{
"when": { "op": "eq", "field": "result", "value": "ok" },
"fields": [ { "name": "code", "type": "uint32" } ]
},
{
"when": { "op": "eq", "field": "result", "value": "queued" },
"fields": [ { "name": "pos", "type": "uint32" } ]
}
],
"else": [ { "name": "reason", "type": "uint32" } ]
}
])");
ASSERT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, IfChainRejectsUnknownBranchValue) {
auto msg = parse_fields(R"([
{ "name": "result", "type": "Result" },
{
"type": "if_chain",
"branches": [
{
"when": { "op": "eq", "field": "result", "value": "bogus" },
"fields": [ { "name": "code", "type": "uint32" } ]
}
]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, IfChainScopeIsBranchLocal) {
// A field declared inside one branch must not be visible to a later
// branch or to the `else` body.
auto msg = parse_fields(R"([
{ "name": "result", "type": "Result" },
{
"type": "if_chain",
"branches": [
{
"when": { "op": "eq", "field": "result", "value": "ok" },
"fields": [ { "name": "local_field", "type": "uint8" } ]
}
],
"else": [
{
"type": "group",
"when": { "op": "eq", "field": "local_field", "value": 1 },
"fields": [ { "name": "x", "type": "uint8" } ]
}
]
}
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, GeneratesIfChainAsElseCascade) {
auto msg = jsoncons::json::parse(R"({
"name": "Resp",
"opcode": "smsg_resp",
"direction": "server",
"fields": [
{ "name": "result", "type": "Result" },
{
"type": "if_chain",
"branches": [
{
"when": { "op": "eq", "field": "result", "value": "ok" },
"fields": [ { "name": "code", "type": "uint32" } ]
},
{
"when": { "op": "eq", "field": "result", "value": "queued" },
"fields": [ { "name": "pos", "type": "uint32" } ]
}
],
"else": [ { "name": "reason", "type": "uint32" } ]
}
]
})");
const auto out = generate_message(msg, reg, templates_dir);
// exactly one if, one else-if, one else, not three standalone ifs
EXPECT_NE(out.content.find("if(result == Result::ok) {"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("else if(result == Result::queued) {"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("else {"), std::string::npos) << out.content;
}
TEST_F(Protogen, AsCastMatchingUnderlyingValidates) {
auto msg = parse_fields(R"([
{ "name": "kind", "type": "uint8", "as": "Result" }
])");
EXPECT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, AsCastAcceptsSizeMismatch) {
auto msg = parse_fields(R"([
{ "name": "kind", "type": "uint32", "as": "Result" }
])");
EXPECT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, AsCastScopesFieldAsSemanticType) {
auto msg = parse_fields(R"([
{ "name": "kind", "type": "uint8", "as": "Result" },
{
"type": "group",
"when": { "op": "eq", "field": "kind", "value": "ok" },
"fields": [ { "name": "x", "type": "uint32" } ]
}
])");
EXPECT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, GeneratesAsCastWithRawTempAndStaticCast) {
auto msg = jsoncons::json::parse(R"({
"name": "Resp",
"opcode": "smsg_resp",
"direction": "server",
"fields": [
{ "name": "kind", "type": "uint8", "as": "Result" }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("Result kind;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("std::uint8_t raw_"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("kind = static_cast<Result>("), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream << static_cast<std::uint8_t>(kind);"), std::string::npos) << out.content;
}
TEST_F(Protogen, ValidatesUntilEndArray) {
auto msg = parse_fields(R"([
{ "name": "tail", "type": "uint32", "array": { "until_end": true } }
])");
EXPECT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, GeneratesUntilEndLoopWithPerIterationCheck) {
auto msg = jsoncons::json::parse(R"({
"name": "Bulk",
"opcode": "smsg_bulk",
"direction": "server",
"fields": [
{ "name": "tail", "type": "uint32", "array": { "until_end": true } }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("std::vector<std::uint32_t> tail;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("while(!stream.empty()) {"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("tail.emplace_back();"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> tail.back();"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("if(!stream) return StreamResult::failed;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("for(const auto& e : tail) {"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream << e;"), std::string::npos) << out.content;
}
TEST_F(Protogen, UntilEndRejectsAlongsideSize) {
auto msg = parse_fields(R"([
{ "name": "tail", "type": "uint32", "array": { "size": 3, "until_end": true } }
])");
EXPECT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
}
TEST_F(Protogen, GeneratesFixedSizeStructArrayAsLoop) {
auto msg = jsoncons::json::parse(R"({
"name": "Points",
"opcode": "smsg_points",
"direction": "server",
"fields": [
{ "name": "pos", "type": "Vector3", "array": { "size": 3 } }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("std::array<Vector3, 3> pos{};"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("for(auto& e : pos) {"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> e.x;"), std::string::npos) << out.content;
}
TEST_F(Protogen, ExternalWithCppTypeNameEmitsQualifiedName) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"MyFlag": {
"kind": "external",
"include": "foo/bar.h",
"cpp_namespace": "foo::container",
"cpp_type_name": "Inner"
}
}
})");
auto reg = protogen::build_registry(types_doc);
auto msg = jsoncons::json::parse(R"({
"name": "P",
"opcode": "smsg_p",
"direction": "server",
"fields": [ { "name": "f", "type": "MyFlag" } ]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("foo::container::Inner f;"), std::string::npos) << out.content;
}
TEST_F(Protogen, ExternalWithUnderlyingAcceptsAsCast) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"DbcEnum": {
"kind": "external",
"include": "dbcreader/MemoryDefs.h",
"cpp_namespace": "ember::dbc::Owner",
"cpp_type_name": "Kind",
"underlying": "int32"
}
}
})");
auto reg = protogen::build_registry(types_doc);
auto fields = jsoncons::json::parse(R"([
{ "name": "kind", "type": "uint8", "as": "DbcEnum" }
])");
EXPECT_NO_THROW(validate_message_fields(fields, reg, "test"));
}
TEST_F(Protogen, ExternalWithUnderlyingUsedAsTypeIsAnAlias) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"Map": {
"kind": "external",
"include": "dbcreader/MemoryDefs.h",
"cpp_namespace": "ember::dbc",
"underlying": "uint32"
}
}
})");
auto reg = protogen::build_registry(types_doc);
auto msg = jsoncons::json::parse(R"({
"name": "Joined",
"opcode": "smsg_joined",
"direction": "server",
"fields": [ { "name": "map", "type": "Map" } ]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("std::uint32_t map;"), std::string::npos) << out.content;
EXPECT_EQ(out.content.find("ember::dbc::Map"), std::string::npos) << out.content;
EXPECT_EQ(out.content.find("#include <dbcreader/MemoryDefs.h>"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream >> map;"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("stream << map;"), std::string::npos) << out.content;
}
TEST_F(Protogen, AliasExternalUsableInCondition) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"Map": {
"kind": "external",
"include": "dbcreader/MemoryDefs.h",
"cpp_namespace": "ember::dbc",
"underlying": "uint32"
}
}
})");
auto reg = protogen::build_registry(types_doc);
auto msg = jsoncons::json::parse(R"({
"name": "Q",
"opcode": "smsg_q",
"direction": "server",
"fields": [
{ "name": "m", "type": "Map" },
{
"type": "group",
"when": { "op": "neq", "field": "m", "value": 0 },
"fields": [ { "name": "x", "type": "uint32" } ]
}
]
})");
EXPECT_NO_THROW(validate_message_fields(msg["fields"], reg, "test"));
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("if(m != 0)"), std::string::npos) << out.content;
}
TEST_F(Protogen, EnumClassEmitsScopedEnum) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"Colour": {
"kind": "enum_class",
"underlying": "uint8",
"values": { "red": 0, "green": 1 }
}
}
})");
auto reg = protogen::build_registry(types_doc);
const auto headers = protogen::generate_type_headers(reg, templates_dir);
const protogen::GeneratedFile* h = nullptr;
for(const auto& f : headers) {
if(f.relative_path == "types/Colour.h") { h = &f; }
}
ASSERT_NE(h, nullptr);
EXPECT_NE(h->content.find("enum class Colour : std::uint8_t"), std::string::npos) << h->content;
}
TEST_F(Protogen, EnumStaysUnscoped) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"Style": {
"kind": "enum",
"underlying": "uint8",
"values": { "plain": 0, "bold": 1 }
}
}
})");
auto reg = protogen::build_registry(types_doc);
const auto headers = protogen::generate_type_headers(reg, templates_dir);
const protogen::GeneratedFile* h = nullptr;
for(const auto& f : headers) {
if(f.relative_path == "types/Style.h") { h = &f; }
}
ASSERT_NE(h, nullptr);
EXPECT_NE(h->content.find("enum Style : std::uint8_t"), std::string::npos) << h->content;
EXPECT_EQ(h->content.find("enum class"), std::string::npos) << h->content;
}
TEST_F(Protogen, GeneratesFixedSizeStringArrayAsLoop) {
auto msg = jsoncons::json::parse(R"({
"name": "Names",
"opcode": "smsg_names",
"direction": "server",
"fields": [
{ "name": "n", "type": "CString", "array": { "size": 4 } }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("std::array<std::string, 4> n{};"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("for(auto& e : n) {"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("spark::io::null_terminated(e)"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesDefaultForIntegerField) {
auto msg = jsoncons::json::parse(R"({
"name": "Defaulted",
"opcode": "smsg_defaulted",
"direction": "server",
"fields": [
{ "name": "chat_name_attr", "type": "uint64", "default": 0 },
{ "name": "channel_id", "type": "int32", "default": -1 }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("std::uint64_t chat_name_attr{0};"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("std::int32_t channel_id{-1};"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesDefaultForBoolField) {
auto msg = jsoncons::json::parse(R"({
"name": "Toggle",
"opcode": "smsg_toggle",
"direction": "server",
"fields": [
{ "name": "active", "type": "bool", "default": true }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("bool active{true};"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesDefaultForFloatAndDouble) {
auto msg = jsoncons::json::parse(R"({
"name": "Coords",
"opcode": "smsg_coords",
"direction": "server",
"fields": [
{ "name": "f", "type": "float", "default": 1.5 },
{ "name": "d", "type": "double", "default": 2.5 }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("float f{static_cast<float>(1.5)};"), std::string::npos) << out.content;
EXPECT_NE(out.content.find("double d{2.5};"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesDefaultForEnumField) {
auto msg = jsoncons::json::parse(R"({
"name": "Resp",
"opcode": "smsg_resp",
"direction": "server",
"fields": [
{ "name": "result", "type": "Result", "default": "ok" }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("Result result{Result::ok};"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesDefaultForFlagsField) {
auto msg = jsoncons::json::parse(R"({
"name": "Move",
"opcode": "smsg_move",
"direction": "server",
"fields": [
{ "name": "flags", "type": "MovementFlags", "default": "swimming" }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("MovementFlags flags{MovementFlags::swimming};"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesDefaultForStringField) {
auto msg = jsoncons::json::parse(R"({
"name": "Greet",
"opcode": "smsg_greet",
"direction": "server",
"fields": [
{ "name": "name", "type": "CString", "default": "hello" }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("std::string name{\"hello\"};"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesDefaultForAsCastField) {
auto msg = jsoncons::json::parse(R"({
"name": "Resp",
"opcode": "smsg_resp",
"direction": "server",
"fields": [
{ "name": "kind", "type": "uint8", "as": "Result", "default": "ok" }
]
})");
const auto out = generate_message(msg, reg, templates_dir);
EXPECT_NE(out.content.find("Result kind{Result::ok};"), std::string::npos) << out.content;
}
TEST_F(Protogen, GeneratesDefaultInsideStructHeader) {
auto types_doc = jsoncons::json::parse(R"({
"types": {
"Counter": {
"kind": "struct",
"cpp_namespace": "ns",
"fields": [
{ "name": "value", "type": "uint32", "default": 7 }
]
}
}
})");
auto reg = protogen::build_registry(types_doc);
const auto headers = protogen::generate_type_headers(reg, templates_dir);
const protogen::GeneratedFile* counter = nullptr;
for(const auto& h : headers) {
if(h.relative_path == "types/Counter.h") { counter = &h; }
}
ASSERT_NE(counter, nullptr);
EXPECT_NE(counter->content.find("std::uint32_t value{7};"), std::string::npos) << counter->content;
}
TEST_F(Protogen, RejectsDefaultOnArrayField) {
auto msg = parse_fields(R"([
{ "name": "data", "type": "uint32", "array": { "size": 4 }, "default": 0 }
])");
ASSERT_ANY_THROW(validate_message_fields(msg["fields"], reg, "test"));
}