mirror of
				https://github.com/DarkflameUniverse/DarkflameServer.git
				synced 2025-10-31 20:52:01 +00:00 
			
		
		
		
	Make LXFML class for robust and add more tests to edge cases and malformed data
This commit is contained in:
		| @@ -32,10 +32,18 @@ namespace { | ||||
|  | ||||
| Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition) { | ||||
| 	Result toReturn; | ||||
| 	 | ||||
| 	// Handle empty or invalid input | ||||
| 	if (data.empty()) { | ||||
| 		return toReturn; | ||||
| 	} | ||||
| 	 | ||||
| 	// Ensure null-terminated string for tinyxml2::Parse | ||||
| 	std::string nullTerminatedData(data); | ||||
| 	 | ||||
| 	tinyxml2::XMLDocument doc; | ||||
| 	const auto err = doc.Parse(data.data()); | ||||
| 	const auto err = doc.Parse(nullTerminatedData.c_str()); | ||||
| 	if (err != tinyxml2::XML_SUCCESS) { | ||||
| 		LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data()); | ||||
| 		return toReturn; | ||||
| 	} | ||||
|  | ||||
| @@ -44,7 +52,6 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin | ||||
|  | ||||
| 	auto lxfml = reader["LXFML"]; | ||||
| 	if (!lxfml) { | ||||
| 		LOG("Failed to find LXFML element."); | ||||
| 		return toReturn; | ||||
| 	} | ||||
|  | ||||
| @@ -73,16 +80,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin | ||||
| 		// Calculate the lowest and highest points on the entire model | ||||
| 		for (const auto& transformation : transformations | std::views::values) { | ||||
| 			auto split = GeneralUtils::SplitString(transformation, ','); | ||||
| 			if (split.size() < 12) { | ||||
| 				LOG("Not enough in the split?"); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			auto x = GeneralUtils::TryParse<float>(split[9]).value(); | ||||
| 			auto y = GeneralUtils::TryParse<float>(split[10]).value(); | ||||
| 			auto z = GeneralUtils::TryParse<float>(split[11]).value(); | ||||
| 			if (x < lowest.x) lowest.x = x; | ||||
| 			if (y < lowest.y) lowest.y = y; | ||||
| 		if (split.size() < 12) continue; | ||||
| 	 | ||||
| 		auto xOpt = GeneralUtils::TryParse<float>(split[9]); | ||||
| 		auto yOpt = GeneralUtils::TryParse<float>(split[10]); | ||||
| 		auto zOpt = GeneralUtils::TryParse<float>(split[11]); | ||||
| 			 | ||||
| 		if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) continue; | ||||
| 		 | ||||
| 		auto x = xOpt.value(); | ||||
| 		auto y = yOpt.value(); | ||||
| 		auto z = zOpt.value(); | ||||
| 		if (x < lowest.x) lowest.x = x; | ||||
| 		if (y < lowest.y) lowest.y = y; | ||||
| 			if (z < lowest.z) lowest.z = z; | ||||
|  | ||||
| 			if (highest.x < x) highest.x = x; | ||||
| @@ -111,13 +121,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin | ||||
| 	for (auto& transformation : transformations | std::views::values) { | ||||
| 		auto split = GeneralUtils::SplitString(transformation, ','); | ||||
| 		if (split.size() < 12) { | ||||
| 			LOG("Not enough in the split?"); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		auto x = GeneralUtils::TryParse<float>(split[9]).value() - newRootPos.x + curPosition.x; | ||||
| 		auto y = GeneralUtils::TryParse<float>(split[10]).value() - newRootPos.y + curPosition.y; | ||||
| 		auto z = GeneralUtils::TryParse<float>(split[11]).value() - newRootPos.z + curPosition.z; | ||||
| 		auto xOpt = GeneralUtils::TryParse<float>(split[9]); | ||||
| 		auto yOpt = GeneralUtils::TryParse<float>(split[10]); | ||||
| 		auto zOpt = GeneralUtils::TryParse<float>(split[11]); | ||||
| 		 | ||||
| 		if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) { | ||||
| 			continue; | ||||
| 		}		 | ||||
| 		auto x = xOpt.value() - newRootPos.x + curPosition.x; | ||||
| 		auto y = yOpt.value() - newRootPos.y + curPosition.y; | ||||
| 		auto z = zOpt.value() - newRootPos.z + curPosition.z; | ||||
| 		std::stringstream stream; | ||||
| 		for (int i = 0; i < 9; i++) { | ||||
| 			stream << split[i]; | ||||
| @@ -181,16 +197,29 @@ static tinyxml2::XMLElement* CloneElementDeep(const tinyxml2::XMLElement* src, t | ||||
|  | ||||
| std::vector<Lxfml::Result> Lxfml::Split(const std::string_view data, const NiPoint3& curPosition) { | ||||
| 	std::vector<Result> results; | ||||
| 	 | ||||
| 	// Handle empty or invalid input | ||||
| 	if (data.empty()) { | ||||
| 		return results; | ||||
| 	} | ||||
| 	 | ||||
| 	// Prevent processing extremely large inputs that could cause hangs | ||||
| 	if (data.size() > 10000000) { // 10MB limit | ||||
| 		return results; | ||||
| 	} | ||||
| 	 | ||||
| 	// Ensure null-terminated string for tinyxml2::Parse | ||||
| 	// string_view::data() may not be null-terminated, causing undefined behavior | ||||
| 	std::string nullTerminatedData(data); | ||||
| 	 | ||||
| 	tinyxml2::XMLDocument doc; | ||||
| 	const auto err = doc.Parse(data.data()); | ||||
| 	const auto err = doc.Parse(nullTerminatedData.c_str()); | ||||
| 	if (err != tinyxml2::XML_SUCCESS) { | ||||
| 		LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data()); | ||||
| 		return results; | ||||
| 	} | ||||
|  | ||||
| 	auto* lxfml = doc.FirstChildElement("LXFML"); | ||||
| 	if (!lxfml) { | ||||
| 		LOG("Failed to find LXFML element."); | ||||
| 		return results; | ||||
| 	} | ||||
|  | ||||
| @@ -284,7 +313,13 @@ std::vector<Lxfml::Result> Lxfml::Split(const std::string_view data, const NiPoi | ||||
| 		tinyxml2::XMLPrinter printer; | ||||
| 		outDoc.Print(&printer); | ||||
| 		// Normalize position and compute center using existing helper | ||||
| 		auto normalized = NormalizePosition(printer.CStr(), curPosition); | ||||
| 		std::string xmlString = printer.CStr(); | ||||
| 		if (xmlString.size() > 5000000) { // 5MB limit for normalization | ||||
| 			Result emptyResult; | ||||
| 			emptyResult.lxfml = xmlString; | ||||
| 			return emptyResult; | ||||
| 		} | ||||
| 		auto normalized = NormalizePosition(xmlString, curPosition); | ||||
| 		return normalized; | ||||
| 	}; | ||||
|  | ||||
| @@ -324,8 +359,11 @@ std::vector<Lxfml::Result> Lxfml::Split(const std::string_view data, const NiPoi | ||||
| 		// Iteratively include any RigidSystems that reference any boneRefsIncluded | ||||
| 		bool changed = true; | ||||
| 		std::vector<tinyxml2::XMLElement*> rigidSystemsToInclude; | ||||
| 		while (changed) { | ||||
| 		int maxIterations = 1000; // Safety limit to prevent infinite loops | ||||
| 		int iteration = 0; | ||||
| 		while (changed && iteration < maxIterations) { | ||||
| 			changed = false; | ||||
| 			iteration++; | ||||
| 			for (auto* rs : rigidSystems) { | ||||
| 				if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue; | ||||
| 				// parse boneRefs of this rigid system (from its <Rigid> children) | ||||
| @@ -357,7 +395,12 @@ std::vector<Lxfml::Result> Lxfml::Split(const std::string_view data, const NiPoi | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		 | ||||
| 		if (iteration >= maxIterations) { | ||||
| 			// Iteration limit reached, stop processing to prevent infinite loops | ||||
| 			// The file is likely malformed, so just skip further processing | ||||
| 			return results; | ||||
| 		}		 | ||||
| 		// include bricks from bricksIncluded into used set | ||||
| 		for (const auto& b : bricksIncluded) usedBrickRefs.insert(b); | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ set(DCOMMONTEST_SOURCES | ||||
| 	"TestLUString.cpp" | ||||
| 	"TestLUWString.cpp" | ||||
| 	"dCommonDependencies.cpp" | ||||
| 	"LxfmlSplitTests.cpp" | ||||
| 	"LxfmlTests.cpp" | ||||
| ) | ||||
|  | ||||
| add_subdirectory(dEnumsTests) | ||||
|   | ||||
| @@ -1,148 +0,0 @@ | ||||
| #include "gtest/gtest.h" | ||||
|  | ||||
| #include "Lxfml.h" | ||||
| #include "TinyXmlUtils.h" | ||||
|  | ||||
| #include <fstream> | ||||
| #include <sstream> | ||||
| #include <unordered_set> | ||||
| #include <filesystem> | ||||
|  | ||||
| using namespace TinyXmlUtils; | ||||
|  | ||||
| static std::string ReadFile(const std::string& path) { | ||||
|     std::ifstream in(path, std::ios::in | std::ios::binary); | ||||
|     std::ostringstream ss; | ||||
|     ss << in.rdbuf(); | ||||
|     return ss.str(); | ||||
| } | ||||
|  | ||||
| TEST(LxfmlSplitTests, SplitUsesAllBricksAndNoDuplicates) { | ||||
|     // Read the sample test.lxfml included in tests. Resolve path relative to this source file. | ||||
|     std::filesystem::path srcDir = std::filesystem::path(__FILE__).parent_path(); | ||||
|     std::filesystem::path filePath = srcDir / "test.lxfml"; | ||||
| 	std::ifstream in(filePath, std::ios::in | std::ios::binary); | ||||
|     std::ostringstream ss; | ||||
|     ss << in.rdbuf(); | ||||
|     std::string data = ss.str(); | ||||
|     ASSERT_FALSE(data.empty()) << "Failed to read " << filePath.string(); | ||||
|      | ||||
|  | ||||
|     auto results = Lxfml::Split(data); | ||||
|     ASSERT_GT(results.size(), 0); | ||||
|  | ||||
|     // Write split outputs to disk for manual inspection | ||||
|     std::filesystem::path outDir = srcDir / "lxfml_splits"; | ||||
|     std::error_code ec; | ||||
|     std::filesystem::create_directories(outDir, ec); | ||||
|     for (size_t i = 0; i < results.size(); ++i) { | ||||
|         auto outPath = outDir / ("split_" + std::to_string(i) + ".lxfml"); | ||||
|         std::ofstream ofs(outPath, std::ios::out | std::ios::binary); | ||||
|         ASSERT_TRUE(ofs) << "Failed to open output file: " << outPath.string(); | ||||
|         ofs << results[i].lxfml; | ||||
|         ofs.close(); | ||||
|     } | ||||
|  | ||||
|     // parse original to count bricks | ||||
|     tinyxml2::XMLDocument doc; | ||||
|     ASSERT_EQ(doc.Parse(data.c_str()), tinyxml2::XML_SUCCESS); | ||||
|     DocumentReader reader(doc); | ||||
|     auto lxfml = reader["LXFML"]; | ||||
|     ASSERT_TRUE(lxfml); | ||||
|  | ||||
|     // Collect original RigidSystems and Groups (serialize each element string) | ||||
|     auto serializeElement = [](tinyxml2::XMLElement* elem) { | ||||
|         tinyxml2::XMLPrinter p; | ||||
|         elem->Accept(&p); | ||||
|         return std::string(p.CStr()); | ||||
|     }; | ||||
|  | ||||
|     std::unordered_set<std::string> originalRigidSet; | ||||
|     if (auto* rsParent = doc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) { | ||||
|         for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) { | ||||
|             originalRigidSet.insert(serializeElement(rs)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     std::unordered_set<std::string> originalGroupSet; | ||||
|     if (auto* gsParent = doc.FirstChildElement("LXFML")->FirstChildElement("GroupSystems")) { | ||||
|         for (auto* gs = gsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) { | ||||
|             for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) { | ||||
|                 // collect this group and nested groups | ||||
|                 std::function<void(tinyxml2::XMLElement*)> collectGroups = [&](tinyxml2::XMLElement* grp) { | ||||
|                     originalGroupSet.insert(serializeElement(grp)); | ||||
|                     for (auto* child = grp->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectGroups(child); | ||||
|                 }; | ||||
|                 collectGroups(g); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     std::unordered_set<std::string> originalBricks; | ||||
|     for (const auto& brick : lxfml["Bricks"]) { | ||||
|         const auto* ref = brick.Attribute("refID"); | ||||
|         if (ref) originalBricks.insert(ref); | ||||
|     } | ||||
|     ASSERT_GT(originalBricks.size(), 0); | ||||
|  | ||||
|     // Collect bricks across all results and ensure no duplicates and all used | ||||
|     std::unordered_set<std::string> usedBricks; | ||||
|     // Track used rigid systems and groups (serialized strings) | ||||
|     std::unordered_set<std::string> usedRigidSet; | ||||
|     std::unordered_set<std::string> usedGroupSet; | ||||
|     for (const auto& res : results) { | ||||
|         tinyxml2::XMLDocument outDoc; | ||||
|         ASSERT_EQ(outDoc.Parse(res.lxfml.c_str()), tinyxml2::XML_SUCCESS); | ||||
|         DocumentReader outReader(outDoc); | ||||
|         auto outLxfml = outReader["LXFML"]; | ||||
|         ASSERT_TRUE(outLxfml); | ||||
|         // collect rigid systems in this output | ||||
|         if (auto* rsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) { | ||||
|             for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) { | ||||
|                 auto s = serializeElement(rs); | ||||
|                 // no duplicate allowed across outputs | ||||
|                 ASSERT_EQ(usedRigidSet.find(s), usedRigidSet.end()) << "Duplicate RigidSystem across splits"; | ||||
|                 usedRigidSet.insert(s); | ||||
|             } | ||||
|         } | ||||
|         // collect groups in this output | ||||
|         if (auto* gsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("GroupSystems")) { | ||||
|             for (auto* gs = gsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) { | ||||
|                 for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) { | ||||
|                     std::function<void(tinyxml2::XMLElement*)> collectGroupsOut = [&](tinyxml2::XMLElement* grp) { | ||||
|                         auto s = serializeElement(grp); | ||||
|                         ASSERT_EQ(usedGroupSet.find(s), usedGroupSet.end()) << "Duplicate Group across splits"; | ||||
|                         usedGroupSet.insert(s); | ||||
|                         for (auto* child = grp->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectGroupsOut(child); | ||||
|                     }; | ||||
|                     collectGroupsOut(g); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         for (const auto& brick : outLxfml["Bricks"]) { | ||||
|             const auto* ref = brick.Attribute("refID"); | ||||
|             if (ref) { | ||||
|                 // no duplicate allowed | ||||
|                 ASSERT_EQ(usedBricks.find(ref), usedBricks.end()) << "Duplicate brick ref across splits: " << ref; | ||||
|                 usedBricks.insert(ref); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Every original brick must be used in one of the outputs | ||||
|     for (const auto& bref : originalBricks) { | ||||
|         ASSERT_NE(usedBricks.find(bref), usedBricks.end()) << "Brick not used in splits: " << bref; | ||||
|     } | ||||
|  | ||||
|     // And usedBricks should not contain anything outside original | ||||
|     for (const auto& ub : usedBricks) { | ||||
|         ASSERT_NE(originalBricks.find(ub), originalBricks.end()) << "Split produced unknown brick: " << ub; | ||||
|     } | ||||
|  | ||||
|     // Ensure all original rigid systems and groups were used exactly once | ||||
|     ASSERT_EQ(originalRigidSet.size(), usedRigidSet.size()) << "RigidSystem count mismatch"; | ||||
|     for (const auto& s : originalRigidSet) ASSERT_NE(usedRigidSet.find(s), usedRigidSet.end()) << "RigidSystem missing in splits"; | ||||
|  | ||||
|     ASSERT_EQ(originalGroupSet.size(), usedGroupSet.size()) << "Group count mismatch"; | ||||
|     for (const auto& s : originalGroupSet) ASSERT_NE(usedGroupSet.find(s), usedGroupSet.end()) << "Group missing in splits"; | ||||
| } | ||||
							
								
								
									
										298
									
								
								tests/dCommonTests/LxfmlTests.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										298
									
								
								tests/dCommonTests/LxfmlTests.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,298 @@ | ||||
| #include "gtest/gtest.h" | ||||
|  | ||||
| #include "Lxfml.h" | ||||
| #include "TinyXmlUtils.h" | ||||
| #include "dCommonDependencies.h" | ||||
|  | ||||
| #include <fstream> | ||||
| #include <sstream> | ||||
| #include <unordered_set> | ||||
| #include <filesystem> | ||||
|  | ||||
| using namespace TinyXmlUtils; | ||||
|  | ||||
| static std::string ReadFile(const std::string& path) { | ||||
|     std::ifstream in(path, std::ios::in | std::ios::binary); | ||||
|     std::ostringstream ss; | ||||
|     ss << in.rdbuf(); | ||||
|     return ss.str(); | ||||
| } | ||||
|  | ||||
| TEST(LxfmlTests, SplitUsesAllBricksAndNoDuplicates) { | ||||
|     // Read the sample test.lxfml included in tests. Resolve path relative to this source file. | ||||
|     std::filesystem::path srcDir = std::filesystem::path(__FILE__).parent_path(); | ||||
|     std::filesystem::path filePath = srcDir / "LxfmlTestFiles" / "test.lxfml"; | ||||
| 	std::ifstream in(filePath, std::ios::in | std::ios::binary); | ||||
|     std::ostringstream ss; | ||||
|     ss << in.rdbuf(); | ||||
|     std::string data = ss.str(); | ||||
|     ASSERT_FALSE(data.empty()) << "Failed to read " << filePath.string(); | ||||
|      | ||||
|  | ||||
|     auto results = Lxfml::Split(data); | ||||
|     ASSERT_GT(results.size(), 0); | ||||
|  | ||||
|     // Write split outputs to disk for manual inspection | ||||
|     std::filesystem::path outDir = srcDir / "LxfmlTestFiles" / "lxfml_splits"; | ||||
|     std::error_code ec; | ||||
|     std::filesystem::create_directories(outDir, ec); | ||||
|     for (size_t i = 0; i < results.size(); ++i) { | ||||
|         auto outPath = outDir / ("split_" + std::to_string(i) + ".lxfml"); | ||||
|         std::ofstream ofs(outPath, std::ios::out | std::ios::binary); | ||||
|         ASSERT_TRUE(ofs) << "Failed to open output file: " << outPath.string(); | ||||
|         ofs << results[i].lxfml; | ||||
|         ofs.close(); | ||||
|     } | ||||
|  | ||||
|     // parse original to count bricks | ||||
|     tinyxml2::XMLDocument doc; | ||||
|     ASSERT_EQ(doc.Parse(data.c_str()), tinyxml2::XML_SUCCESS); | ||||
|     DocumentReader reader(doc); | ||||
|     auto lxfml = reader["LXFML"]; | ||||
|     ASSERT_TRUE(lxfml); | ||||
|  | ||||
|     // Collect original RigidSystems and Groups (serialize each element string) | ||||
|     auto serializeElement = [](tinyxml2::XMLElement* elem) { | ||||
|         tinyxml2::XMLPrinter p; | ||||
|         elem->Accept(&p); | ||||
|         return std::string(p.CStr()); | ||||
|     }; | ||||
|  | ||||
|     std::unordered_set<std::string> originalRigidSet; | ||||
|     if (auto* rsParent = doc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) { | ||||
|         for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) { | ||||
|             originalRigidSet.insert(serializeElement(rs)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     std::unordered_set<std::string> originalGroupSet; | ||||
|     if (auto* gsParent = doc.FirstChildElement("LXFML")->FirstChildElement("GroupSystems")) { | ||||
|         for (auto* gs = gsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) { | ||||
|             for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) { | ||||
|                 // collect this group and nested groups | ||||
|                 std::function<void(tinyxml2::XMLElement*)> collectGroups = [&](tinyxml2::XMLElement* grp) { | ||||
|                     originalGroupSet.insert(serializeElement(grp)); | ||||
|                     for (auto* child = grp->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectGroups(child); | ||||
|                 }; | ||||
|                 collectGroups(g); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     std::unordered_set<std::string> originalBricks; | ||||
|     for (const auto& brick : lxfml["Bricks"]) { | ||||
|         const auto* ref = brick.Attribute("refID"); | ||||
|         if (ref) originalBricks.insert(ref); | ||||
|     } | ||||
|     ASSERT_GT(originalBricks.size(), 0); | ||||
|  | ||||
|     // Collect bricks across all results and ensure no duplicates and all used | ||||
|     std::unordered_set<std::string> usedBricks; | ||||
|     // Track used rigid systems and groups (serialized strings) | ||||
|     std::unordered_set<std::string> usedRigidSet; | ||||
|     std::unordered_set<std::string> usedGroupSet; | ||||
|     for (const auto& res : results) { | ||||
|         tinyxml2::XMLDocument outDoc; | ||||
|         ASSERT_EQ(outDoc.Parse(res.lxfml.c_str()), tinyxml2::XML_SUCCESS); | ||||
|         DocumentReader outReader(outDoc); | ||||
|         auto outLxfml = outReader["LXFML"]; | ||||
|         ASSERT_TRUE(outLxfml); | ||||
|         // collect rigid systems in this output | ||||
|         if (auto* rsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) { | ||||
|             for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) { | ||||
|                 auto s = serializeElement(rs); | ||||
|                 // no duplicate allowed across outputs | ||||
|                 ASSERT_EQ(usedRigidSet.find(s), usedRigidSet.end()) << "Duplicate RigidSystem across splits"; | ||||
|                 usedRigidSet.insert(s); | ||||
|             } | ||||
|         } | ||||
|         // collect groups in this output | ||||
|         if (auto* gsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("GroupSystems")) { | ||||
|             for (auto* gs = gsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) { | ||||
|                 for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) { | ||||
|                     std::function<void(tinyxml2::XMLElement*)> collectGroupsOut = [&](tinyxml2::XMLElement* grp) { | ||||
|                         auto s = serializeElement(grp); | ||||
|                         ASSERT_EQ(usedGroupSet.find(s), usedGroupSet.end()) << "Duplicate Group across splits"; | ||||
|                         usedGroupSet.insert(s); | ||||
|                         for (auto* child = grp->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectGroupsOut(child); | ||||
|                     }; | ||||
|                     collectGroupsOut(g); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         for (const auto& brick : outLxfml["Bricks"]) { | ||||
|             const auto* ref = brick.Attribute("refID"); | ||||
|             if (ref) { | ||||
|                 // no duplicate allowed | ||||
|                 ASSERT_EQ(usedBricks.find(ref), usedBricks.end()) << "Duplicate brick ref across splits: " << ref; | ||||
|                 usedBricks.insert(ref); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Every original brick must be used in one of the outputs | ||||
|     for (const auto& bref : originalBricks) { | ||||
|         ASSERT_NE(usedBricks.find(bref), usedBricks.end()) << "Brick not used in splits: " << bref; | ||||
|     } | ||||
|  | ||||
|     // And usedBricks should not contain anything outside original | ||||
|     for (const auto& ub : usedBricks) { | ||||
|         ASSERT_NE(originalBricks.find(ub), originalBricks.end()) << "Split produced unknown brick: " << ub; | ||||
|     } | ||||
|  | ||||
|     // Ensure all original rigid systems and groups were used exactly once | ||||
|     ASSERT_EQ(originalRigidSet.size(), usedRigidSet.size()) << "RigidSystem count mismatch"; | ||||
|     for (const auto& s : originalRigidSet) ASSERT_NE(usedRigidSet.find(s), usedRigidSet.end()) << "RigidSystem missing in splits"; | ||||
|  | ||||
|     ASSERT_EQ(originalGroupSet.size(), usedGroupSet.size()) << "Group count mismatch"; | ||||
|     for (const auto& s : originalGroupSet) ASSERT_NE(usedGroupSet.find(s), usedGroupSet.end()) << "Group missing in splits"; | ||||
| } | ||||
|  | ||||
| // Tests for invalid input handling - now working with the improved Split function | ||||
|  | ||||
| TEST(LxfmlTests, InvalidLxfmlHandling) { | ||||
|     // Test LXFML with invalid transformation matrices | ||||
|     std::string invalidTransformData = R"(<?xml version="1.0" encoding="UTF-8" standalone="no" ?> | ||||
| <LXFML versionMajor="5" versionMinor="0"> | ||||
|     <Meta> | ||||
|         <Application name="LEGO Universe" versionMajor="0" versionMinor="0"/> | ||||
|         <Brand name="LEGOUniverse"/> | ||||
|         <BrickSet version="457"/> | ||||
|     </Meta> | ||||
|     <Bricks> | ||||
|         <Brick refID="0" designID="74340"> | ||||
|             <Part refID="0" designID="3679" materials="23"> | ||||
|                 <Bone refID="0" transformation="invalid,matrix,with,text,values,here,not,numbers,at,all,fails,parse"/> | ||||
|             </Part> | ||||
|         </Brick> | ||||
|         <Brick refID="1" designID="41533"> | ||||
|             <Part refID="1" designID="41533" materials="23"> | ||||
|                 <Bone refID="1" transformation="1,2,3"/> | ||||
|             </Part> | ||||
|         </Brick> | ||||
|     </Bricks> | ||||
| </LXFML>)"; | ||||
|      | ||||
|     // The Split function should handle invalid transformation matrices gracefully | ||||
|     std::vector<Lxfml::Result> results; | ||||
|     EXPECT_NO_FATAL_FAILURE({ | ||||
|         results = Lxfml::Split(invalidTransformData); | ||||
|     }) << "Split should not crash on invalid transformation matrices"; | ||||
|      | ||||
|     // Function should handle invalid transforms gracefully, possibly returning empty or partial results | ||||
|     // The exact behavior depends on how the function handles invalid numeric parsing | ||||
| } | ||||
|  | ||||
| TEST(LxfmlTests, EmptyLxfmlHandling) { | ||||
|     // Test with completely empty input | ||||
|     std::string emptyData = ""; | ||||
|     std::vector<Lxfml::Result> results; | ||||
|      | ||||
|     EXPECT_NO_FATAL_FAILURE({ | ||||
|         results = Lxfml::Split(emptyData); | ||||
|     }) << "Split should not crash on empty input"; | ||||
|      | ||||
|     EXPECT_EQ(results.size(), 0) << "Empty input should return empty results"; | ||||
| } | ||||
|  | ||||
| TEST(LxfmlTests, InvalidTransformHandling) { | ||||
|     // Test with various types of invalid transformation matrices | ||||
|     std::vector<std::string> invalidTransformTests = { | ||||
|         // LXFML with empty transformation | ||||
|         R"(<?xml version="1.0"?><LXFML versionMajor="5" versionMinor="0"><Meta></Meta><Bricks><Brick refID="0" designID="74340"><Part refID="0" designID="3679"><Bone refID="0" transformation=""/></Part></Brick></Bricks></LXFML>)", | ||||
|          | ||||
|         // LXFML with too few transformation values (needs 12, has 6) | ||||
|         R"(<?xml version="1.0"?><LXFML versionMajor="5" versionMinor="0"><Meta></Meta><Bricks><Brick refID="0" designID="74340"><Part refID="0" designID="3679"><Bone refID="0" transformation="1,0,0,0,1,0"/></Part></Brick></Bricks></LXFML>)", | ||||
|          | ||||
|         // LXFML with non-numeric transformation values | ||||
|         R"(<?xml version="1.0"?><LXFML versionMajor="5" versionMinor="0"><Meta></Meta><Bricks><Brick refID="0" designID="74340"><Part refID="0" designID="3679"><Bone refID="0" transformation="a,b,c,d,e,f,g,h,i,j,k,l"/></Part></Brick></Bricks></LXFML>)", | ||||
|          | ||||
|         // LXFML with mixed valid/invalid transformation values | ||||
|         R"(<?xml version="1.0"?><LXFML versionMajor="5" versionMinor="0"><Meta></Meta><Bricks><Brick refID="0" designID="74340"><Part refID="0" designID="3679"><Bone refID="0" transformation="1,0,invalid,0,1,0,0,0,1,10,20,30"/></Part></Brick></Bricks></LXFML>)", | ||||
|          | ||||
|         // LXFML with no Bricks section (should return empty gracefully) | ||||
|         R"(<?xml version="1.0"?><LXFML versionMajor="5" versionMinor="0"><Meta></Meta></LXFML>)" | ||||
|     }; | ||||
|      | ||||
|     for (size_t i = 0; i < invalidTransformTests.size(); ++i) { | ||||
|         std::vector<Lxfml::Result> results; | ||||
|         EXPECT_NO_FATAL_FAILURE({ | ||||
|             results = Lxfml::Split(invalidTransformTests[i]); | ||||
|         }) << "Split should not crash on invalid transform test case " << i; | ||||
|          | ||||
|         // The function should handle invalid transforms gracefully | ||||
|         // May return empty results or skip invalid bricks | ||||
|     } | ||||
| } | ||||
|  | ||||
| TEST(LxfmlTests, MixedValidInvalidTransformsHandling) { | ||||
|     // Test LXFML with mix of valid and invalid transformation data | ||||
|     std::string mixedValidData = R"(<?xml version="1.0" encoding="UTF-8" standalone="no" ?> | ||||
| <LXFML versionMajor="5" versionMinor="0"> | ||||
|     <Meta> | ||||
|         <Application name="LEGO Universe" versionMajor="0" versionMinor="0"/> | ||||
|         <Brand name="LEGOUniverse"/> | ||||
|         <BrickSet version="457"/> | ||||
|     </Meta> | ||||
|     <Bricks> | ||||
|         <Brick refID="0" designID="74340"> | ||||
|             <Part refID="0" designID="3679" materials="23"> | ||||
|                 <Bone refID="0" transformation="1,0,0,0,1,0,0,0,1,0,0,0"/> | ||||
|             </Part> | ||||
|         </Brick> | ||||
|         <Brick refID="1" designID="41533"> | ||||
|             <Part refID="1" designID="41533" materials="23"> | ||||
|                 <Bone refID="1" transformation="invalid,transform,here,bad,values,foo,bar,baz,qux,0,0,0"/> | ||||
|             </Part> | ||||
|         </Brick> | ||||
|         <Brick refID="2" designID="74340"> | ||||
|             <Part refID="2" designID="3679" materials="23"> | ||||
|                 <Bone refID="2" transformation="1,0,0,0,1,0,0,0,1,10,20,30"/> | ||||
|             </Part> | ||||
|         </Brick> | ||||
|         <Brick refID="3" designID="41533"> | ||||
|             <Part refID="3" designID="41533" materials="23"> | ||||
|                 <Bone refID="3" transformation="1,2,3"/> | ||||
|             </Part> | ||||
|         </Brick> | ||||
|     </Bricks> | ||||
|     <RigidSystems> | ||||
|         <RigidSystem> | ||||
|             <Rigid boneRefs="0,2"/> | ||||
|         </RigidSystem> | ||||
|         <RigidSystem> | ||||
|             <Rigid boneRefs="1,3"/> | ||||
|         </RigidSystem> | ||||
|     </RigidSystems> | ||||
|     <GroupSystems> | ||||
|         <GroupSystem> | ||||
|             <Group partRefs="0,2"/> | ||||
|             <Group partRefs="1,3"/> | ||||
|         </GroupSystem> | ||||
|     </GroupSystems> | ||||
| </LXFML>)"; | ||||
|      | ||||
|     // The Split function should handle mixed valid/invalid transforms gracefully | ||||
|     std::vector<Lxfml::Result> results; | ||||
|     EXPECT_NO_FATAL_FAILURE({ | ||||
|         results = Lxfml::Split(mixedValidData); | ||||
|     }) << "Split should not crash on mixed valid/invalid transforms"; | ||||
|      | ||||
|     // Should process valid bricks and handle invalid ones gracefully | ||||
|     if (results.size() > 0) { | ||||
|         EXPECT_NO_FATAL_FAILURE({ | ||||
|             for (size_t i = 0; i < results.size(); ++i) { | ||||
|                 // Each result should have valid LXFML structure | ||||
|                 tinyxml2::XMLDocument doc; | ||||
|                 auto parseResult = doc.Parse(results[i].lxfml.c_str()); | ||||
|                 EXPECT_EQ(parseResult, tinyxml2::XML_SUCCESS)  | ||||
|                     << "Result " << i << " should produce valid XML"; | ||||
|                      | ||||
|                 if (parseResult == tinyxml2::XML_SUCCESS) { | ||||
|                     auto* lxfml = doc.FirstChildElement("LXFML"); | ||||
|                     EXPECT_NE(lxfml, nullptr) << "Result " << i << " should have LXFML root element"; | ||||
|                 } | ||||
|             } | ||||
|         }) << "Mixed valid/invalid transform processing should not cause fatal errors"; | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Aaron Kimbrell
					Aaron Kimbrell