mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2025-10-11 09:58:05 +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) {
|
Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition) {
|
||||||
Result toReturn;
|
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;
|
tinyxml2::XMLDocument doc;
|
||||||
const auto err = doc.Parse(data.data());
|
const auto err = doc.Parse(nullTerminatedData.c_str());
|
||||||
if (err != tinyxml2::XML_SUCCESS) {
|
if (err != tinyxml2::XML_SUCCESS) {
|
||||||
LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data());
|
|
||||||
return toReturn;
|
return toReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +52,6 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
|
|||||||
|
|
||||||
auto lxfml = reader["LXFML"];
|
auto lxfml = reader["LXFML"];
|
||||||
if (!lxfml) {
|
if (!lxfml) {
|
||||||
LOG("Failed to find LXFML element.");
|
|
||||||
return toReturn;
|
return toReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,14 +80,17 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
|
|||||||
// Calculate the lowest and highest points on the entire model
|
// Calculate the lowest and highest points on the entire model
|
||||||
for (const auto& transformation : transformations | std::views::values) {
|
for (const auto& transformation : transformations | std::views::values) {
|
||||||
auto split = GeneralUtils::SplitString(transformation, ',');
|
auto split = GeneralUtils::SplitString(transformation, ',');
|
||||||
if (split.size() < 12) {
|
if (split.size() < 12) continue;
|
||||||
LOG("Not enough in the split?");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto x = GeneralUtils::TryParse<float>(split[9]).value();
|
auto xOpt = GeneralUtils::TryParse<float>(split[9]);
|
||||||
auto y = GeneralUtils::TryParse<float>(split[10]).value();
|
auto yOpt = GeneralUtils::TryParse<float>(split[10]);
|
||||||
auto z = GeneralUtils::TryParse<float>(split[11]).value();
|
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 (x < lowest.x) lowest.x = x;
|
||||||
if (y < lowest.y) lowest.y = y;
|
if (y < lowest.y) lowest.y = y;
|
||||||
if (z < lowest.z) lowest.z = z;
|
if (z < lowest.z) lowest.z = z;
|
||||||
@@ -111,13 +121,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
|
|||||||
for (auto& transformation : transformations | std::views::values) {
|
for (auto& transformation : transformations | std::views::values) {
|
||||||
auto split = GeneralUtils::SplitString(transformation, ',');
|
auto split = GeneralUtils::SplitString(transformation, ',');
|
||||||
if (split.size() < 12) {
|
if (split.size() < 12) {
|
||||||
LOG("Not enough in the split?");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto x = GeneralUtils::TryParse<float>(split[9]).value() - newRootPos.x + curPosition.x;
|
auto xOpt = GeneralUtils::TryParse<float>(split[9]);
|
||||||
auto y = GeneralUtils::TryParse<float>(split[10]).value() - newRootPos.y + curPosition.y;
|
auto yOpt = GeneralUtils::TryParse<float>(split[10]);
|
||||||
auto z = GeneralUtils::TryParse<float>(split[11]).value() - newRootPos.z + curPosition.z;
|
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;
|
std::stringstream stream;
|
||||||
for (int i = 0; i < 9; i++) {
|
for (int i = 0; i < 9; i++) {
|
||||||
stream << split[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<Lxfml::Result> Lxfml::Split(const std::string_view data, const NiPoint3& curPosition) {
|
||||||
std::vector<Result> results;
|
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;
|
tinyxml2::XMLDocument doc;
|
||||||
const auto err = doc.Parse(data.data());
|
const auto err = doc.Parse(nullTerminatedData.c_str());
|
||||||
if (err != tinyxml2::XML_SUCCESS) {
|
if (err != tinyxml2::XML_SUCCESS) {
|
||||||
LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data());
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* lxfml = doc.FirstChildElement("LXFML");
|
auto* lxfml = doc.FirstChildElement("LXFML");
|
||||||
if (!lxfml) {
|
if (!lxfml) {
|
||||||
LOG("Failed to find LXFML element.");
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +313,13 @@ std::vector<Lxfml::Result> Lxfml::Split(const std::string_view data, const NiPoi
|
|||||||
tinyxml2::XMLPrinter printer;
|
tinyxml2::XMLPrinter printer;
|
||||||
outDoc.Print(&printer);
|
outDoc.Print(&printer);
|
||||||
// Normalize position and compute center using existing helper
|
// 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;
|
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
|
// Iteratively include any RigidSystems that reference any boneRefsIncluded
|
||||||
bool changed = true;
|
bool changed = true;
|
||||||
std::vector<tinyxml2::XMLElement*> rigidSystemsToInclude;
|
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;
|
changed = false;
|
||||||
|
iteration++;
|
||||||
for (auto* rs : rigidSystems) {
|
for (auto* rs : rigidSystems) {
|
||||||
if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue;
|
if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue;
|
||||||
// parse boneRefs of this rigid system (from its <Rigid> children)
|
// parse boneRefs of this rigid system (from its <Rigid> children)
|
||||||
@@ -358,6 +396,11 @@ 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
|
// include bricks from bricksIncluded into used set
|
||||||
for (const auto& b : bricksIncluded) usedBrickRefs.insert(b);
|
for (const auto& b : bricksIncluded) usedBrickRefs.insert(b);
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ set(DCOMMONTEST_SOURCES
|
|||||||
"TestLUString.cpp"
|
"TestLUString.cpp"
|
||||||
"TestLUWString.cpp"
|
"TestLUWString.cpp"
|
||||||
"dCommonDependencies.cpp"
|
"dCommonDependencies.cpp"
|
||||||
"LxfmlSplitTests.cpp"
|
"LxfmlTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
add_subdirectory(dEnumsTests)
|
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