From c3d658be6392f30d3f01e8e31203b09ff0180cac Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 16:34:09 +0200 Subject: [PATCH] test: extend coverage of testDataPack --- tests/testDataPack.cpp | 158 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/tests/testDataPack.cpp b/tests/testDataPack.cpp index a99c873..1839381 100644 --- a/tests/testDataPack.cpp +++ b/tests/testDataPack.cpp @@ -318,3 +318,161 @@ VF_TEST(aTagMarkedReplaceWipesWhatCameBeforeIt) { CHECK_EQ(logs.size(), static_cast(1)); if (!logs.empty()) CHECK(logs[0] == Block::Cactus); + const BlockTag* tag = loader.blockTags().find(id("voxelforge:logs")); + CHECK(tag != nullptr); + CHECK(tag->replace); + CHECK(tag->contains(id("voxelforge:cactus"))); + CHECK(!tag->contains(id("voxelforge:wood"))); +} + +VF_TEST(tagReferencesFlattenIntoTheirMembersWithoutDuplicates) { + DataPackLoader loader; + loader.addPack(basePack()); + CHECK(loader.load()); + + std::vector members; + CHECK(loader.blockTags().resolve(id("voxelforge:mineable"), members)); + CHECK_EQ(members.size(), static_cast(3)); + if (members.size() == 3u) { + CHECK(members[0] == id("voxelforge:wood")); + CHECK(members[1] == id("voxelforge:stone")); + CHECK(members[2] == id("voxelforge:sand")); + } + CHECK(loader.blockTags().contains(id("voxelforge:mineable"), id("voxelforge:wood"))); + CHECK(!loader.blockTags().contains(id("voxelforge:mineable"), id("voxelforge:cactus"))); + + std::vector blocks; + CHECK(loader.blocksInTag(id("voxelforge:mineable"), blocks)); + CHECK_EQ(blocks.size(), static_cast(3)); + if (!blocks.empty()) CHECK(blocks[0] == Block::Wood); + + std::vector fuel; + CHECK(loader.itemsInTag(id("voxelforge:fuel"), fuel)); + CHECK_EQ(fuel.size(), static_cast(2)); + if (fuel.size() == 2u) { + CHECK(fuel[0] == ItemId::CoalLump); + CHECK(fuel[1] == ItemId::Planks); + } + + // A tag that names a tag nobody defined resolves to nothing, loudly. + std::vector unresolved; + std::string problem; + TagRegistry registry; + BlockTag dangling; + dangling.id = id("voxelforge:dangling"); + dangling.entries.push_back({id("voxelforge:absent"), true}); + registry.define(dangling); + CHECK(!registry.resolve(dangling.id, unresolved, problem)); + CHECK(problem.find("unknown tag") != std::string::npos); + CHECK(!registry.hasCycle(dangling.id)); +} + +VF_TEST(aTagThatReferencesItselfIsReportedAsACycleInsteadOfLoopingForever) { + DataPackMeta meta; + meta.name = "looping"; + DataPack pack(meta); + pack.addFile("data/voxelforge/tags/blocks/first.json", + R"json({"values": ["voxelforge:stone", "#voxelforge:second"]})json"); + pack.addFile("data/voxelforge/tags/blocks/second.json", + R"json({"values": ["voxelforge:sand", "#voxelforge:third"]})json"); + pack.addFile("data/voxelforge/tags/blocks/third.json", + R"json({"values": ["#voxelforge:first"]})json"); + pack.addFile("data/voxelforge/tags/blocks/selfish.json", + R"json({"values": ["#voxelforge:selfish"]})json"); + + DataPackLoader loader; + loader.addPack(pack); + CHECK(loader.load()); + + const TagRegistry& tags = loader.blockTags(); + std::vector members; + std::string problem; + CHECK(!tags.resolve(id("voxelforge:first"), members, problem)); + CHECK(problem.find("tag cycle") != std::string::npos); + CHECK(members.empty()); + CHECK(tags.hasCycle(id("voxelforge:first"))); + CHECK(tags.hasCycle(id("voxelforge:second"))); + CHECK(tags.hasCycle(id("voxelforge:selfish"))); + + std::vector blocks; + CHECK(!loader.blocksInTag(id("voxelforge:first"), blocks)); + CHECK(blocks.empty()); + + // A diamond is not a cycle: two paths onto the same tag still resolve. + DataPackMeta diamondMeta; + diamondMeta.name = "diamond"; + DataPack diamond(diamondMeta); + diamond.addFile("data/voxelforge/tags/blocks/leaf.json", + R"json({"values": ["voxelforge:stone"]})json"); + diamond.addFile("data/voxelforge/tags/blocks/left.json", + R"json({"values": ["#voxelforge:leaf"]})json"); + diamond.addFile("data/voxelforge/tags/blocks/right.json", + R"json({"values": ["#voxelforge:leaf"]})json"); + diamond.addFile("data/voxelforge/tags/blocks/top.json", + R"json({"values": ["#voxelforge:left", "#voxelforge:right"]})json"); + DataPackLoader diamondLoader; + diamondLoader.addPack(diamond); + CHECK(diamondLoader.load()); + CHECK(diamondLoader.blockTags().resolve(id("voxelforge:top"), members)); + CHECK_EQ(members.size(), static_cast(1)); + CHECK(!diamondLoader.blockTags().hasCycle(id("voxelforge:top"))); +} + +VF_TEST(oneRottenFileCostsItselfAndNothingElseInThePack) { + DataPackMeta meta; + meta.name = "mixed"; + DataPack pack(meta); + pack.addFile("data/voxelforge/recipes/good.json", appleBread); + pack.addFile("data/voxelforge/recipes/torn.json", "{\"type\": \"craftingShapeless\","); + pack.addFile("data/voxelforge/recipes/ghost_item.json", + R"json({"type": "craftingShapeless", "ingredients": ["voxelforge:cheese"], + "result": {"item": "voxelforge:bread"}})json"); + pack.addFile("data/voxelforge/recipes/nonsense.json", + R"json({"type": "alchemy", "result": {"item": "voxelforge:bread"}})json"); + pack.addFile("data/voxelforge/nowhere/lost.json", R"json({"values": []})json"); + pack.addFile("data/voxelforge/lootTables/empty.json", R"json({"pools": []})json"); + pack.addFile("data/voxelforge/tags/blocks/bad_entry.json", + R"json({"values": ["Not An Id"]})json"); + pack.addFile("data/voxelforge/tags/blocks/fine.json", + R"json({"values": ["voxelforge:stone"]})json"); + pack.addFile("data/voxelforge/recipes/ShoutyName.json", appleBread); + + DataPackLoader loader; + loader.addPack(pack); + CHECK(!loader.load()); + CHECK_EQ(loader.errors().size(), static_cast(7)); + + for (const DataPackError& failure : loader.errors()) { + CHECK(!failure.file.empty()); + CHECK(!failure.reason.empty()); + CHECK_EQ(failure.pack, std::string("mixed")); + CHECK(failure.text().find("mixed/data/") != std::string::npos); + } + + // The healthy files in the very same pack still landed. + CHECK_EQ(loader.recipes().size(), static_cast(1)); + CHECK(loader.recipe(id("voxelforge:good")) != nullptr); + CHECK_EQ(loader.blockTags().size(), static_cast(1)); + CHECK(loader.blockTags().has(id("voxelforge:fine"))); + + bool sawParseError = false; + bool sawUnknownItem = false; + bool sawUnknownFolder = false; + bool sawUppercaseId = false; + for (const DataPackError& failure : loader.errors()) { + if (failure.file == "data/voxelforge/recipes/torn.json") { + sawParseError = failure.line > 0; + } + if (failure.file == "data/voxelforge/recipes/ghost_item.json") + sawUnknownItem = failure.reason.find("no such item") != std::string::npos; + if (failure.file == "data/voxelforge/nowhere/lost.json") + sawUnknownFolder = failure.reason.find("known folder") != std::string::npos; + if (failure.file == "data/voxelforge/recipes/ShoutyName.json") + sawUppercaseId = failure.reason.find("valid resource id") != std::string::npos; + } + CHECK(sawParseError); + CHECK(sawUnknownItem); + CHECK(sawUnknownFolder); + CHECK(sawUppercaseId); +} +