// // Copyright Contributors to the MaterialX Project // SPDX-License-Identifier: Apache-2.0 // #include #include #include #include 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 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()) { 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(); 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(); 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("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("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("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 = " " "" "" "" ""; 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(); } // 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 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 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()) { if (elem->hasAttribute("uiname")) { REQUIRE(!elem->getAttribute("uiname").empty()); } } } } // Restore the original locale. std::locale::global(origLocale); }