From 374b4a7e7ca1b2315eb04a460e39f935281c3404 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 28 Aug 2026 21:05:51 +0200 Subject: [PATCH] test: add tests for player profile persistence (position, inventory, stats) --- tests/testPlayerData.cpp | 123 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/testPlayerData.cpp diff --git a/tests/testPlayerData.cpp b/tests/testPlayerData.cpp new file mode 100644 index 0000000..efc27c2 --- /dev/null +++ b/tests/testPlayerData.cpp @@ -0,0 +1,123 @@ +#include "testHarness.h" +#include "vf/PlayerData.h" + +#include +#include + +using namespace vf; + +namespace { + +bool slotsEqual(const ItemSlot& a, const ItemSlot& b) { + return a.item == b.item && a.count == b.count && a.durability == b.durability; +} + +PlayerData samplePlayer() { + PlayerData player; + player.position = {123.5f, 68.25f, -940.75f}; + player.yaw = 145.0f; + player.pitch = -22.5f; + player.survival.health = 14.5f; + player.survival.hunger = 7.0f; + player.survival.saturation = 2.5f; + player.survival.exhaustion = 3.2f; + player.survival.airRemaining = 9.0f; + player.survival.alive = true; + player.xpLevel = 27; + player.xpProgress = 0.6f; + player.selectedSlot = 3; + player.inventory = { + {ItemId::Stone, 64, 0}, + {ItemId::Planks, 32, 0}, + {ItemId::None, 0, 0}, + {ItemId::DiamondPickaxe, 1, 1200}, + {ItemId::CookedBeef, 12, 0}, + {ItemId::IronSword, 1, 47}, + {ItemId::RedstoneDust, 8, 0}, + }; + return player; +} + +} // namespace + +VF_TEST(playerDataRoundTripsAllFields) { + PlayerData original = samplePlayer(); + ByteBuffer bytes = savePlayer(original); + PlayerData restored; + CHECK(loadPlayer(bytes.data(), bytes.size(), restored)); + + CHECK(restored.position.x == original.position.x); + CHECK(restored.position.y == original.position.y); + CHECK(restored.position.z == original.position.z); + CHECK(restored.yaw == original.yaw); + CHECK(restored.pitch == original.pitch); + CHECK(restored.survival.health == original.survival.health); + CHECK(restored.survival.hunger == original.survival.hunger); + CHECK(restored.survival.saturation == original.survival.saturation); + CHECK(restored.survival.exhaustion == original.survival.exhaustion); + CHECK(restored.survival.airRemaining == original.survival.airRemaining); + CHECK(restored.survival.alive == original.survival.alive); + CHECK_EQ(restored.xpLevel, original.xpLevel); + CHECK(restored.xpProgress == original.xpProgress); + CHECK_EQ(restored.selectedSlot, original.selectedSlot); +} + +VF_TEST(playerDataInventoryRoundTrips) { + PlayerData original = samplePlayer(); + ByteBuffer bytes = savePlayer(original); + PlayerData restored; + CHECK(loadPlayer(bytes.data(), bytes.size(), restored)); + + CHECK_EQ(restored.inventory.size(), original.inventory.size()); + bool identical = true; + for (std::size_t i = 0; i < original.inventory.size(); ++i) + if (!slotsEqual(restored.inventory[i], original.inventory[i])) identical = false; + CHECK(identical); + + // Tool durability specifically must survive the trip. + CHECK(restored.inventory[3].item == ItemId::DiamondPickaxe); + CHECK_EQ(restored.inventory[3].durability, 1200); + CHECK(restored.inventory[5].item == ItemId::IronSword); + CHECK_EQ(restored.inventory[5].durability, 47); +} + +VF_TEST(playerDataEmptyInventory) { + PlayerData player; + player.position = {0.0f, 0.0f, 0.0f}; + ByteBuffer bytes = savePlayer(player); + PlayerData restored; + CHECK(loadPlayer(bytes.data(), bytes.size(), restored)); + CHECK(restored.inventory.empty()); + CHECK(restored.survival.alive == player.survival.alive); +} + +VF_TEST(playerDataNbtTreeIsInspectable) { + PlayerData player = samplePlayer(); + NbtTag root = playerToNbt(player); + const NbtTag* health = root.find("Health"); + CHECK(health != nullptr && health->kind() == NbtType::Float); + CHECK(health->asFloat() == player.survival.health); + const NbtTag* inventory = root.find("Inventory"); + CHECK(inventory != nullptr && inventory->kind() == NbtType::List); + CHECK_EQ(inventory->asList().size(), player.inventory.size()); +} + +VF_TEST(playerDataRejectsTruncatedBytes) { + PlayerData player = samplePlayer(); + ByteBuffer bytes = savePlayer(player); + PlayerData restored; + CHECK(!loadPlayer(bytes.data(), bytes.size() / 2, restored)); + CHECK(!loadPlayer(bytes.data(), 3, restored)); +} + +VF_TEST(playerDataRejectsMissingField) { + NbtTag root = playerToNbt(samplePlayer()); + NbtTag broken = NbtTag::makeCompound(); + for (const auto& entry : root.asCompound()) + if (entry.first != "Health") broken.put(entry.first, entry.second); + PlayerData out; + CHECK(!playerFromNbt(broken, out)); + // The same tree with Health present must still parse, proving the guard is real. + PlayerData ok; + CHECK(playerFromNbt(root, ok)); +}