test: add tests for NBT tag tree read and write
This commit is contained in:
1 parent
0bf884f15d
commit
31de25a33a
1 file changed
+260
@@ -0,0 +1,260 @@
|
||||
#include "testHarness.h"
|
||||
#include "vf/Nbt.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
using namespace vf;
|
||||
|
||||
namespace {
|
||||
|
||||
bool roundTrip(const std::string& name, const NbtTag& tag, std::string& outName, NbtTag& outTag) {
|
||||
std::vector<uint8_t> 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<int8_t>(-42));
|
||||
|
||||
CHECK(roundTrip("shortField", NbtTag::makeShort(-12345), name, tag));
|
||||
CHECK(tag.kind() == NbtType::Short);
|
||||
CHECK_EQ(tag.asShort(), static_cast<int16_t>(-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<int8_t> 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<int8_t>& 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<int32_t> 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<int32_t>& 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<std::size_t>(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<std::size_t>(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<std::size_t>(1));
|
||||
CHECK_EQ(outInventory->asList()[0].find("id")->asShort(), static_cast<int16_t>(9));
|
||||
CHECK_EQ(outInventory->asList()[0].find("count")->asByte(), static_cast<int8_t>(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<std::size_t>(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<uint8_t> 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<Block, chunkVolume> blocks;
|
||||
blocks.fill(Block::Stone);
|
||||
|
||||
NbtPaletteSection section = encodeSectionPalette(blocks);
|
||||
CHECK_EQ(section.palette.size(), static_cast<std::size_t>(1));
|
||||
CHECK_EQ(section.bitsPerIndex, 1);
|
||||
|
||||
std::array<Block, chunkVolume> decoded;
|
||||
decoded.fill(Block::Air);
|
||||
decodeSectionPalette(section, decoded);
|
||||
CHECK(decoded == blocks);
|
||||
}
|
||||
|
||||
VF_TEST(nbtPaletteRoundTripsMixedSection) {
|
||||
std::array<Block, chunkVolume> 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<std::size_t>(5));
|
||||
CHECK_EQ(section.bitsPerIndex, 3);
|
||||
|
||||
std::array<Block, chunkVolume> decoded;
|
||||
decodeSectionPalette(section, decoded);
|
||||
CHECK(decoded == blocks);
|
||||
}
|
||||
|
||||
VF_TEST(nbtPaletteBitsPerIndexScalesWithPaletteSize) {
|
||||
auto bitsForPaletteSize = [](int distinctBlocks) {
|
||||
std::array<Block, chunkVolume> blocks;
|
||||
for (std::size_t i = 0; i < blocks.size(); ++i)
|
||||
blocks[i] = static_cast<Block>(static_cast<int>(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<Block, chunkVolume> blocks;
|
||||
for (std::size_t i = 0; i < blocks.size(); ++i)
|
||||
blocks[i] = static_cast<Block>(static_cast<int>(i) % 9);
|
||||
|
||||
NbtPaletteSection section = encodeSectionPalette(blocks);
|
||||
std::size_t expectedBits = static_cast<std::size_t>(section.bitsPerIndex) * blocks.size();
|
||||
std::size_t expectedBytes = (expectedBits + 7) / 8;
|
||||
CHECK_EQ(section.packedIndices.size(), expectedBytes);
|
||||
}
|
||||
|
||||
VF_TEST(nbtPalettePreservesFirstSeenOrder) {
|
||||
std::array<Block, chunkVolume> 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<std::size_t>(3));
|
||||
CHECK(section.palette[0] == Block::Water);
|
||||
CHECK(section.palette[1] == Block::Grass);
|
||||
CHECK(section.palette[2] == Block::Stone);
|
||||
}
|
||||
Reference in new issue
Block a user