Files
SDL3CPlusPlus/MaterialX/source/MaterialXTest/MaterialXFormat/XmlIo.cpp
2026-01-06 13:25:49 +00:00

374 lines
13 KiB
C++

//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//
#include <MaterialXTest/External/Catch/catch.hpp>
#include <MaterialXFormat/Environ.h>
#include <MaterialXFormat/Util.h>
#include <MaterialXFormat/XmlIo.h>
namespace mx = MaterialX;
TEST_CASE("Load content", "[xmlio]")
{
mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath();
mx::FilePath libraryPath = searchPath.find("libraries/stdlib");
mx::FilePath examplesPath = searchPath.find("resources/Materials/Examples/StandardSurface");
searchPath.append(libraryPath);
searchPath.append(examplesPath);
// Read the standard library.
std::vector<mx::DocumentPtr> libs;
for (const mx::FilePath& filename : libraryPath.getFilesInDirectory(mx::MTLX_EXTENSION))
{
mx::DocumentPtr lib = mx::createDocument();
mx::readFromXmlFile(lib, filename, searchPath);
libs.push_back(lib);
}
// Read and validate each example document.
for (const mx::FilePath& filename : examplesPath.getFilesInDirectory(mx::MTLX_EXTENSION))
{
mx::DocumentPtr doc = mx::createDocument();
mx::readFromXmlFile(doc, filename, searchPath);
for (mx::DocumentPtr lib : libs)
{
doc->importLibrary(lib);
}
std::string message;
bool docValid = doc->validate(&message);
if (!docValid)
{
WARN("[" + filename.asString() + "] " + message);
}
REQUIRE(docValid);
// Traverse the document tree
int valueElementCount = 0;
for (mx::ElementPtr elem : doc->traverseTree())
{
if (elem->isA<mx::ValueElement>())
{
valueElementCount++;
}
}
REQUIRE(valueElementCount > 0);
// Serialize to XML.
mx::XmlWriteOptions writeOptions;
writeOptions.writeXIncludeEnable = false;
std::string xmlString = mx::writeToXmlString(doc, &writeOptions);
// Verify that the serialized document is identical.
mx::DocumentPtr writtenDoc = mx::createDocument();
mx::readFromXmlString(writtenDoc, xmlString);
REQUIRE(*writtenDoc == *doc);
mx::readFromXmlBuffer(writtenDoc, xmlString.c_str());
REQUIRE(*writtenDoc == *doc);
// Flatten all subgraphs.
doc->flattenSubgraphs();
for (mx::NodeGraphPtr graph : doc->getNodeGraphs())
{
if (graph->getActiveSourceUri() == doc->getSourceUri())
{
graph->flattenSubgraphs();
}
}
REQUIRE(doc->validate());
// Verify that all referenced types and nodes are declared.
bool referencesValid = true;
for (mx::ElementPtr elem : doc->traverseTree())
{
if (elem->getActiveSourceUri() != doc->getSourceUri())
{
continue;
}
mx::TypedElementPtr typedElem = elem->asA<mx::TypedElement>();
if (typedElem && typedElem->hasType() && !typedElem->isMultiOutputType())
{
if (!typedElem->getTypeDef())
{
WARN("[" + typedElem->getActiveSourceUri() + "] TypedElement " + typedElem->getName() + " has no matching TypeDef");
referencesValid = false;
}
}
mx::NodePtr node = elem->asA<mx::Node>();
if (node)
{
if (!node->getNodeDefString().empty() && !node->getNodeDef())
{
WARN("[" + node->getActiveSourceUri() + "] Node " + node->getName() + " has no matching NodeDef for " + node->getNodeDefString());
referencesValid = false;
}
}
}
REQUIRE(referencesValid);
}
// Read the same document twice and verify that duplicate elements
// are skipped.
mx::DocumentPtr doc = mx::createDocument();
std::string filename = "standard_surface_look_brass_tiled.mtlx";
mx::readFromXmlFile(doc, filename, searchPath);
mx::readFromXmlFile(doc, filename, searchPath);
REQUIRE(doc->validate());
// Import libraries twice and verify that duplicate elements are
// skipped.
mx::DocumentPtr libDoc = doc->copy();
for (mx::DocumentPtr lib : libs)
{
libDoc->importLibrary(lib);
libDoc->importLibrary(lib);
}
REQUIRE(libDoc->validate());
// Read document with conflicting elements.
mx::DocumentPtr conflictDoc = doc->copy();
for (mx::ElementPtr elem : conflictDoc->traverseTree())
{
if (elem->isA<mx::Node>("image"))
{
elem->setFilePrefix("differentFolder/");
}
}
mx::readFromXmlFile(conflictDoc, filename, searchPath);
REQUIRE(conflictDoc->validate());
// Reread in clean document
doc = mx::createDocument();
mx::readFromXmlFile(doc, filename, searchPath);
// Read document without XIncludes.
mx::DocumentPtr flatDoc = mx::createDocument();
mx::XmlReadOptions readOptions;
readOptions.readXIncludeFunction = nullptr;
mx::readFromXmlFile(flatDoc, filename, searchPath, &readOptions);
readOptions.readXIncludeFunction = mx::readFromXmlFile;
REQUIRE(*flatDoc != *doc);
// Read document using environment search path.
mx::setEnviron(mx::MATERIALX_SEARCH_PATH_ENV_VAR, searchPath.asString());
mx::DocumentPtr envDoc = mx::createDocument();
mx::readFromXmlFile(envDoc, filename, mx::FileSearchPath(), &readOptions);
REQUIRE(*doc == *envDoc);
mx::removeEnviron(mx::MATERIALX_SEARCH_PATH_ENV_VAR);
REQUIRE_THROWS_AS(mx::readFromXmlFile(envDoc, filename, mx::FileSearchPath(), &readOptions), mx::ExceptionFileMissing);
// Serialize to XML with a custom predicate that skips images.
auto skipImages = [](mx::ConstElementPtr elem)
{
return !elem->isA<mx::Node>("image");
};
mx::XmlWriteOptions writeOptions;
writeOptions.writeXIncludeEnable = false;
writeOptions.elementPredicate = skipImages;
std::string xmlString = mx::writeToXmlString(doc, &writeOptions);
// Reconstruct and verify that the document contains no images.
mx::DocumentPtr writtenDoc = mx::createDocument();
mx::readFromXmlString(writtenDoc, xmlString, mx::FileSearchPath(), &readOptions);
REQUIRE(*writtenDoc != *doc);
unsigned imageElementCount = 0;
for (mx::ElementPtr elem : writtenDoc->traverseTree())
{
if (elem->isA<mx::Node>("image"))
{
imageElementCount++;
}
}
REQUIRE(imageElementCount == 0);
// Serialize to XML with a custom predicate to remove XIncludes.
auto skipLibIncludes = [libs](mx::ConstElementPtr elem)
{
if (elem->hasSourceUri())
{
for (auto lib : libs)
{
if (lib->getSourceUri() == elem->getSourceUri())
{
return false;
}
}
}
return true;
};
writeOptions.writeXIncludeEnable = true;
writeOptions.elementPredicate = skipLibIncludes;
xmlString = mx::writeToXmlString(writtenDoc, &writeOptions);
// Verify that the document contains no XIncludes.
writtenDoc = mx::createDocument();
mx::readFromXmlString(writtenDoc, xmlString, mx::FileSearchPath(), &readOptions);
bool hasSourceUri = false;
for (mx::ElementPtr elem : writtenDoc->traverseTree())
{
if (elem->hasSourceUri())
{
hasSourceUri = true;
break;
}
}
REQUIRE(!hasSourceUri);
// Read a document with a relative XInclude.
mx::DocumentPtr parentDoc = mx::createDocument();
mx::readFromXmlFile(parentDoc, "resources/Materials/TestSuite/libraries/metal/brass_wire_mesh.mtlx", searchPath);
REQUIRE(parentDoc->getNodeDef("ND_TestMetal") != nullptr);
// Read a string with a relative Xinclude
parentDoc = mx::createDocument();
std::string includeTest =
"<?xml version=\"1.0\"?> "
"<materialx version=\"1.38\">"
"<xi:include href=\"standard_surface_brass_tiled.mtlx\" />"
"<xi:include href=\"standard_surface_greysphere_calibration.mtlx\" />"
"</materialx>";
mx::readFromXmlString(parentDoc, includeTest, searchPath);
REQUIRE(parentDoc->getNodeGraph("NG_brass1") != nullptr);
REQUIRE(parentDoc->getNodeGraph("NG_Greysphere_Calibration") != nullptr);
// Read a non-existent document.
mx::DocumentPtr nonExistentDoc = mx::createDocument();
REQUIRE_THROWS_AS(mx::readFromXmlFile(nonExistentDoc, "NonExistent.mtlx", mx::FileSearchPath(), &readOptions), mx::ExceptionFileMissing);
}
TEST_CASE("Comments and newlines", "[xmlio]")
{
mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath();
mx::FilePath testPath = searchPath.find("resources/Materials/Examples/StandardSurface/standard_surface_chess_set.mtlx");
// Read the example file into an XML string buffer.
std::string origXml = mx::readFile(testPath);
// Convert the string to a document with comments and newlines preserved.
mx::DocumentPtr doc = mx::createDocument();
mx::XmlReadOptions readOptions;
readOptions.readComments = true;
readOptions.readNewlines = true;
readOptions.upgradeVersion = false;
mx::readFromXmlString(doc, origXml, mx::FileSearchPath(), &readOptions);
// Write the document to a new XML string buffer.
std::string newXml = mx::writeToXmlString(doc);
// Verify that the XML string buffers are identical.
REQUIRE(origXml == newXml);
}
TEST_CASE("Maximum tree depth", "[xmlio]")
{
// Create a document that exceeds the maximum tree depth.
mx::DocumentPtr doc = mx::createDocument();
mx::ElementPtr elem = doc;
for (int i = 0; i < mx::MAX_XML_TREE_DEPTH + 1; i++)
{
elem = elem->addChild<mx::NodeGraph>();
}
// Write the document to a string buffer.
std::string xmlString = mx::writeToXmlString(doc);
// Read the string buffer as a document, verifying that the correct
// exception is thrown.
mx::DocumentPtr newDoc = mx::createDocument();
REQUIRE_THROWS_AS(mx::readFromXmlString(newDoc, xmlString), mx::ExceptionParseError);
}
TEST_CASE("Fuzz testing", "[xmlio]")
{
mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath();
mx::FilePath examplesPath = searchPath.find("resources/Materials/Examples");
std::mt19937 rng(0);
std::uniform_int_distribution<size_t> randChar(0, 255);
for (const mx::FilePath& path : examplesPath.getSubDirectories())
{
for (const mx::FilePath& filename : path.getFilesInDirectory(mx::MTLX_EXTENSION))
{
// Read the example file into an XML string buffer.
const std::string origString = mx::readFile(path / filename);
REQUIRE(origString.size() > 0);
std::uniform_int_distribution<size_t> randPos(0, origString.size() - 1);
// Iterate over test runs.
for (size_t testRun = 0; testRun < 256; testRun++)
{
std::string editString = origString;
// Iterate over string edits.
for (size_t editIndex = 0; editIndex < 32; editIndex++)
{
// Randomly alter one character in the document.
size_t charIndex = randPos(rng);
size_t newChar = randChar(rng);
editString[charIndex] = (char) newChar;
// Attempt to interpret the edited string as a document, allowing only MaterialX exceptions.
mx::DocumentPtr doc = mx::createDocument();
try
{
mx::readFromXmlString(doc, editString, searchPath);
doc->validate();
}
catch (const mx::Exception&)
{
// On a MaterialX exception, proceed to the next test run.
break;
}
}
}
}
}
}
TEST_CASE("Locale region testing", "[xmlio]")
{
// In the United States, the thousands separator is a comma, while in Germany it is a period.
// Thus, one thousand twenty five is displayed as 1,025 in the United States and 1.025 in Germany.
//
// In a MaterialX document, a vector3 value of "1,1.5,2.0" should be interpreted as (1.0f, 1.5f, 2.0f).
// Set the locale to Germany, if supported on this platform, and note the original locale.
std::locale origLocale;
try
{
origLocale = std::locale::global(std::locale("de_DE"));
}
catch (const std::runtime_error& e)
{
WARN("Unable to change locale " << e.what());
return;
}
// Read and validate each example document.
mx::FilePath testPath("resources/Materials/TestSuite/locale");
for (const mx::FilePath& filename : testPath.getFilesInDirectory(mx::MTLX_EXTENSION))
{
mx::DocumentPtr doc = mx::createDocument();
mx::readFromXmlFile(doc, testPath / filename);
REQUIRE(doc->validate());
for (mx::ElementPtr elem : doc->traverseTree())
{
if (elem->isA<mx::ValueElement>())
{
if (elem->hasAttribute("uiname"))
{
REQUIRE(!elem->getAttribute("uiname").empty());
}
}
}
}
// Restore the original locale.
std::locale::global(origLocale);
}