From 31de25a33a31c8fa78d8d0f8ac02a941481c4a97 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 28 Aug 2026 15:25:53 +0200 Subject: [PATCH] test: add tests for NBT tag tree read and write --- tests/testNbt.cpp | 260 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 tests/testNbt.cpp diff --git a/tests/testNbt.cpp b/tests/testNbt.cpp new file mode 100644 index 0000000..4a2bbe9 --- /dev/null +++ b/tests/testNbt.cpp @@ -0,0 +1,260 @@ +#include "testHarness.h" +#include "vf/Nbt.h" + +#include + +using namespace vf; + +namespace { + +bool roundTrip(const std::string& name, const NbtTag& tag, std::string& outName, NbtTag& outTag) { + std::vector bytes = serializeNbt(name, tag); + return deserializeNbt(bytes.data(), bytes.size(), outName, outTag); +} + +} // namespace + +VF_TEST(nbtScalarRoundTripPreservesValueAndType) { + std::string name; + NbtTag tag; + + CHECK(roundTrip("byteField", NbtTag::makeByte(-42), name, tag)); + CHECK_EQ(name, std::string("byteField")); + CHECK(tag.kind() == NbtType::Byte); + CHECK_EQ(tag.asByte(), static_cast(-42)); + + CHECK(roundTrip("shortField", NbtTag::makeShort(-12345), name, tag)); + CHECK(tag.kind() == NbtType::Short); + CHECK_EQ(tag.asShort(), static_cast(-12345)); + + CHECK(roundTrip("intField", NbtTag::makeInt(-2000000000), name, tag)); + CHECK(tag.kind() == NbtType::Int); + CHECK_EQ(tag.asInt(), -2000000000); + + CHECK(roundTrip("longField", NbtTag::makeLong(-9000000000000000000LL), name, tag)); + CHECK(tag.kind() == NbtType::Long); + CHECK_EQ(tag.asLong(), -9000000000000000000LL); + + CHECK(roundTrip("floatField", NbtTag::makeFloat(3.14159f), name, tag)); + CHECK(tag.kind() == NbtType::Float); + CHECK_NEAR(tag.asFloat(), 3.14159f, 1e-6f); + + CHECK(roundTrip("doubleField", NbtTag::makeDouble(-2.718281828), name, tag)); + CHECK(tag.kind() == NbtType::Double); + CHECK_NEAR(tag.asDouble(), -2.718281828, 1e-12); + + CHECK(roundTrip("stringField", NbtTag::makeString("voxelforge"), name, tag)); + CHECK(tag.kind() == NbtType::String); + CHECK_EQ(tag.asString(), std::string("voxelforge")); +} + +VF_TEST(nbtEmptyStringRoundTrips) { + std::string name; + NbtTag tag; + CHECK(roundTrip("empty", NbtTag::makeString(""), name, tag)); + CHECK_EQ(tag.asString(), std::string("")); +} + +VF_TEST(nbtByteArrayRoundTripsExactBytes) { + std::vector values = {1, -1, 0, 127, -128, 64, -64}; + std::string name; + NbtTag tag; + CHECK(roundTrip("bytes", NbtTag::makeByteArray(values), name, tag)); + CHECK(tag.kind() == NbtType::ByteArray); + const std::vector& out = tag.asByteArray(); + CHECK_EQ(out.size(), values.size()); + for (std::size_t i = 0; i < values.size(); ++i) CHECK_EQ(out[i], values[i]); +} + +VF_TEST(nbtIntArrayRoundTripsExactValues) { + std::vector values = {0, 1, -1, 2147483647, -2147483647 - 1, 12345}; + std::string name; + NbtTag tag; + CHECK(roundTrip("ints", NbtTag::makeIntArray(values), name, tag)); + CHECK(tag.kind() == NbtType::IntArray); + const std::vector& out = tag.asIntArray(); + CHECK_EQ(out.size(), values.size()); + for (std::size_t i = 0; i < values.size(); ++i) CHECK_EQ(out[i], values[i]); +} + +VF_TEST(nbtListOfIntRoundTrips) { + NbtTag list = NbtTag::makeList(NbtType::Int); + for (int32_t v : {10, -20, 30, -40, 50}) list.listAppend(NbtTag::makeInt(v)); + + std::string name; + NbtTag tag; + CHECK(roundTrip("intList", list, name, tag)); + CHECK(tag.kind() == NbtType::List); + CHECK(tag.listElementType() == NbtType::Int); + CHECK_EQ(tag.asList().size(), static_cast(5)); + CHECK_EQ(tag.asList()[0].asInt(), 10); + CHECK_EQ(tag.asList()[3].asInt(), -40); +} + +VF_TEST(nbtListOfStringRoundTrips) { + NbtTag list = NbtTag::makeList(NbtType::String); + list.listAppend(NbtTag::makeString("oak")); + list.listAppend(NbtTag::makeString("spruce")); + list.listAppend(NbtTag::makeString("jungle")); + + std::string name; + NbtTag tag; + CHECK(roundTrip("trees", list, name, tag)); + CHECK_EQ(tag.asList().size(), static_cast(3)); + CHECK_EQ(tag.asList()[1].asString(), std::string("spruce")); +} + +VF_TEST(nbtEmptyListRoundTrips) { + NbtTag list = NbtTag::makeList(NbtType::Compound); + std::string name; + NbtTag tag; + CHECK(roundTrip("nothing", list, name, tag)); + CHECK(tag.kind() == NbtType::List); + CHECK(tag.asList().empty()); +} + +VF_TEST(nbtNestedCompoundRoundTrips) { + NbtTag position = NbtTag::makeCompound(); + position.put("x", NbtTag::makeInt(120)); + position.put("y", NbtTag::makeInt(64)); + position.put("z", NbtTag::makeInt(-90)); + + NbtTag inventory = NbtTag::makeList(NbtType::Compound); + NbtTag item = NbtTag::makeCompound(); + item.put("id", NbtTag::makeShort(9)); + item.put("count", NbtTag::makeByte(32)); + inventory.listAppend(item); + + NbtTag root = NbtTag::makeCompound(); + root.put("position", position); + root.put("inventory", inventory); + root.put("health", NbtTag::makeFloat(18.5f)); + + std::string name; + NbtTag out; + CHECK(roundTrip("player", root, name, out)); + CHECK_EQ(name, std::string("player")); + CHECK(out.kind() == NbtType::Compound); + + const NbtTag* outPosition = out.find("position"); + CHECK(outPosition != nullptr); + CHECK_EQ(outPosition->find("x")->asInt(), 120); + CHECK_EQ(outPosition->find("y")->asInt(), 64); + CHECK_EQ(outPosition->find("z")->asInt(), -90); + + const NbtTag* outInventory = out.find("inventory"); + CHECK(outInventory != nullptr); + CHECK_EQ(outInventory->asList().size(), static_cast(1)); + CHECK_EQ(outInventory->asList()[0].find("id")->asShort(), static_cast(9)); + CHECK_EQ(outInventory->asList()[0].find("count")->asByte(), static_cast(32)); + + CHECK_NEAR(out.find("health")->asFloat(), 18.5f, 1e-6f); + CHECK(out.find("missing") == nullptr); +} + +VF_TEST(nbtPutOverwritesExistingKeyInsteadOfAppending) { + NbtTag compound = NbtTag::makeCompound(); + compound.put("score", NbtTag::makeInt(10)); + compound.put("score", NbtTag::makeInt(20)); + CHECK_EQ(compound.asCompound().size(), static_cast(1)); + CHECK_EQ(compound.find("score")->asInt(), 20); +} + +VF_TEST(nbtFindReturnsNullptrForMissingKey) { + NbtTag compound = NbtTag::makeCompound(); + compound.put("present", NbtTag::makeByte(1)); + CHECK(compound.find("present") != nullptr); + CHECK(compound.find("absent") == nullptr); +} + +VF_TEST(nbtDeserializeRejectsTruncatedData) { + NbtTag tag = NbtTag::makeInt(999); + std::vector bytes = serializeNbt("value", tag); + CHECK(bytes.size() > 2); + + std::string name; + NbtTag out; + for (std::size_t cut = 0; cut < bytes.size(); ++cut) { + bool ok = deserializeNbt(bytes.data(), cut, name, out); + CHECK(!ok); + } + CHECK(deserializeNbt(bytes.data(), bytes.size(), name, out)); +} + +VF_TEST(nbtPaletteRoundTripsUniformSection) { + std::array blocks; + blocks.fill(Block::Stone); + + NbtPaletteSection section = encodeSectionPalette(blocks); + CHECK_EQ(section.palette.size(), static_cast(1)); + CHECK_EQ(section.bitsPerIndex, 1); + + std::array decoded; + decoded.fill(Block::Air); + decodeSectionPalette(section, decoded); + CHECK(decoded == blocks); +} + +VF_TEST(nbtPaletteRoundTripsMixedSection) { + std::array blocks; + for (std::size_t i = 0; i < blocks.size(); ++i) { + switch (i % 5) { + case 0: blocks[i] = Block::Air; break; + case 1: blocks[i] = Block::Stone; break; + case 2: blocks[i] = Block::Dirt; break; + case 3: blocks[i] = Block::Grass; break; + default: blocks[i] = Block::Water; break; + } + } + + NbtPaletteSection section = encodeSectionPalette(blocks); + CHECK_EQ(section.palette.size(), static_cast(5)); + CHECK_EQ(section.bitsPerIndex, 3); + + std::array decoded; + decodeSectionPalette(section, decoded); + CHECK(decoded == blocks); +} + +VF_TEST(nbtPaletteBitsPerIndexScalesWithPaletteSize) { + auto bitsForPaletteSize = [](int distinctBlocks) { + std::array blocks; + for (std::size_t i = 0; i < blocks.size(); ++i) + blocks[i] = static_cast(static_cast(i) % distinctBlocks); + NbtPaletteSection section = encodeSectionPalette(blocks); + return section.bitsPerIndex; + }; + + CHECK_EQ(bitsForPaletteSize(1), 1); + CHECK_EQ(bitsForPaletteSize(2), 1); + CHECK_EQ(bitsForPaletteSize(3), 2); + CHECK_EQ(bitsForPaletteSize(4), 2); + CHECK_EQ(bitsForPaletteSize(5), 3); + CHECK_EQ(bitsForPaletteSize(9), 4); + CHECK_EQ(bitsForPaletteSize(20), 5); +} + +VF_TEST(nbtPalettePackedIndicesSizeMatchesExpectedByteCount) { + std::array blocks; + for (std::size_t i = 0; i < blocks.size(); ++i) + blocks[i] = static_cast(static_cast(i) % 9); + + NbtPaletteSection section = encodeSectionPalette(blocks); + std::size_t expectedBits = static_cast(section.bitsPerIndex) * blocks.size(); + std::size_t expectedBytes = (expectedBits + 7) / 8; + CHECK_EQ(section.packedIndices.size(), expectedBytes); +} + +VF_TEST(nbtPalettePreservesFirstSeenOrder) { + std::array blocks; + blocks.fill(Block::Stone); + blocks[0] = Block::Water; + blocks[1] = Block::Grass; + blocks[2] = Block::Stone; + + NbtPaletteSection section = encodeSectionPalette(blocks); + CHECK_EQ(section.palette.size(), static_cast(3)); + CHECK(section.palette[0] == Block::Water); + CHECK(section.palette[1] == Block::Grass); + CHECK(section.palette[2] == Block::Stone); +}