diff --git a/src/engine/engine_resource.c b/src/engine/engine_resource.c index df94efd2..c1dd3174 100644 --- a/src/engine/engine_resource.c +++ b/src/engine/engine_resource.c @@ -219,7 +219,7 @@ int mju_dirnamelen(const char* path) { } int pos = -1; - for (int i = 0; path[i] && i >= 0; ++i) { + for (int i = 0; path[i]; ++i) { if (path[i] == '/' || path[i] == '\\') { pos = i; } diff --git a/src/xml/xml.cc b/src/xml/xml.cc index be768c20..a8f29685 100644 --- a/src/xml/xml.cc +++ b/src/xml/xml.cc @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "tinyxml2.h" @@ -34,6 +35,7 @@ #include "engine/engine_resource.h" #include "engine/engine_vfs.h" #include "user/user_model.h" +#include "user/user_util.h" #include "xml/xml_native_reader.h" #include "xml/xml_native_writer.h" #include "xml/xml_urdf.h" @@ -41,7 +43,6 @@ namespace { -using std::string; using tinyxml2::XMLDocument; using tinyxml2::XMLElement; using tinyxml2::XMLNode; @@ -97,7 +98,7 @@ class LocaleOverride { } // namespace // Main writer function - calls mjXWrite -string mjWriteXML(mjCModel* model, char* error, int error_sz) { +std::string mjWriteXML(mjCModel* model, char* error, int error_sz) { LocaleOverride locale_override; // check for empty model @@ -114,13 +115,32 @@ string mjWriteXML(mjCModel* model, char* error, int error_sz) { // find include elements recursively, replace them with subtree from xml file -static void mjIncludeXML(XMLElement* elem, string dir, const mjVFS* vfs, - std::unordered_set& included) { +static void mjIncludeXML(mjXReader& reader, XMLElement* elem, + std::string_view dir, const mjVFS* vfs, + std::unordered_set& included) { + // capture directory defaults on first pass of XML tree + if (!strcasecmp(elem->Value(), "compiler")) { + auto assetdir_attr = mjXUtil::ReadAttrStr(elem, "assetdir"); + if (assetdir_attr.has_value()) { + reader.SetAssetDir(assetdir_attr.value()); + } + + auto texturedir_attr = mjXUtil::ReadAttrStr(elem, "texturedir"); + if (texturedir_attr.has_value()) { + reader.SetTextureDir(texturedir_attr.value()); + } + + auto meshdir_attr = mjXUtil::ReadAttrStr(elem, "meshdir"); + if (meshdir_attr.has_value()) { + reader.SetMeshDir(meshdir_attr.value()); + } + } + // not an include, recursively go through all children if (strcasecmp(elem->Value(), "include")) { XMLElement* child = elem->FirstChildElement(); for (; child; child = child->NextSiblingElement()) { - mjIncludeXML(child, dir, vfs, included); + mjIncludeXML(reader, child, dir, vfs, included); } return; } @@ -131,26 +151,60 @@ static void mjIncludeXML(XMLElement* elem, string dir, const mjVFS* vfs, } // get filename - string filename; - mjXUtil::ReadAttrTxt(elem, "file", filename, true); - filename = dir + filename; + auto file_attr = mjXUtil::ReadAttrStr(elem, "file", true); + if (!file_attr.has_value()) { + throw mjXError(elem, "Include element missing file attribute"); + } + std::string filename = file_attr.value(); + // block repeated include files if (included.find(filename) != included.end()) { throw mjXError(elem, "File '%s' already included", filename.c_str()); } - // get data source - mjResource *resource = mju_openVfsResource(filename.c_str(), vfs); + // TODO: b/325905702 - We have a messy wrapper here to remain backwards + // compatible, which will be removed in the near future. + std::string fullname; + if (!mjuu_isabspath(filename)) { + fullname = reader.ModelFileDir() + filename; + } else { + fullname = filename; + } + mjResource *resource = mju_openVfsResource(fullname.c_str(), vfs); if (!resource) { // load from provider or OS filesystem std::array error; - resource = mju_openResource(filename.c_str(), error.data(), error.size()); + resource = mju_openResource(fullname.c_str(), error.data(), error.size()); if (!resource) { - throw mjXError(elem, "%s", error.data()); + if (!mjuu_isabspath(filename)) { + fullname = std::string(dir) + filename; + } else { + fullname = filename; + } + + // load from provider or OS filesystem + std::array error; + resource = mju_openResource(fullname.c_str(), error.data(), error.size()); + if (!resource) { + throw mjXError(elem, "%s", error.data()); + } } } + if (!mjuu_isabspath(filename)) { + filename = std::string(dir) + filename; + } + + const char* include_dir = nullptr; + int ninclude_dir = 0; + mju_getResourceDir(resource, &include_dir, &ninclude_dir); + std::string next_dir = std::string(include_dir, ninclude_dir); + if (!mjuu_isabspath(filename)) { + next_dir = std::string(dir) + next_dir; + } + elem->SetAttribute("dir", next_dir.data()); + const char* xmlstring = nullptr; int buffer_size = mju_readResource(resource, (const void**) &xmlstring); if (buffer_size < 0) { @@ -210,14 +264,15 @@ static void mjIncludeXML(XMLElement* elem, string dir, const mjVFS* vfs, // recursively run include child = include->FirstChildElement(); for (; child; child = child->NextSiblingElement()) { - mjIncludeXML(child, dir, vfs, included); + mjIncludeXML(reader, child, next_dir, vfs, included); } } // Main parser function -mjCModel* mjParseXML(const char* filename, const mjVFS* vfs, char* error, int error_sz) { +mjCModel* mjParseXML(const char* filename, const mjVFS* vfs, + char* error, int error_sz) { LocaleOverride locale_override; // check arguments @@ -251,7 +306,8 @@ mjCModel* mjParseXML(const char* filename, const mjVFS* vfs, char* error, int er int buffer_size = mju_readResource(resource, (const void**) &xmlstring); if (buffer_size < 0) { if (error) { - std::snprintf(error, error_sz, "mjParseXML: error reading file '%s'", filename); + std::snprintf(error, error_sz, + "mjParseXML: error reading file '%s'", filename); } mju_closeResource(resource); return nullptr; @@ -304,11 +360,12 @@ mjCModel* mjParseXML(const char* filename, const mjVFS* vfs, char* error, int er try { if (!strcasecmp(root->Value(), "mujoco")) { // find include elements, replace them with subtree from xml file - std::unordered_set included = {filename}; - mjIncludeXML(root, model->modelfiledir, vfs, included); + std::unordered_set included = {filename}; + mjXReader parser; + parser.SetModelFileDir(model->modelfiledir); + mjIncludeXML(parser, root, model->modelfiledir, vfs, included); // parse MuJoCo model - mjXReader parser; parser.SetModel(model); parser.Parse(root); } diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 35b990ef..8c713cc1 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -1349,7 +1349,7 @@ void mjXReader::OneFlex(XMLElement* elem, mjmFlex* pflex) { // mesh element parser void mjXReader::OneMesh(XMLElement* elem, mjmMesh* pmesh) { int n; - string text, name, classname, content_type, file; + string text, name, classname, content_type; // read attributes if (ReadAttrTxt(elem, "name", name)) { @@ -1361,8 +1361,9 @@ void mjXReader::OneMesh(XMLElement* elem, mjmMesh* pmesh) { if (ReadAttrTxt(elem, "content_type", content_type)) { mjm_setString(pmesh->content_type, content_type.c_str()); } - if (ReadAttrTxt(elem, "file", file)) { - mjm_setString(pmesh->file, file.c_str()); + auto file = ReadAttrFile(elem, "file", MeshDir()); + if (file) { + mjm_setString(pmesh->file, file->c_str()); } ReadAttr(elem, "refpos", 3, pmesh->refpos, text); ReadAttr(elem, "refpos", 4, pmesh->refquat, text); @@ -1418,19 +1419,20 @@ void mjXReader::OneMesh(XMLElement* elem, mjmMesh* pmesh) { // skin element parser void mjXReader::OneSkin(XMLElement* elem, mjmSkin* pskin) { - string text, name, file, material; + string text, name, material; float data[4]; // read attributes if (ReadAttrTxt(elem, "name", name)) { mjm_setString(pskin->name, name.c_str()); } - if (ReadAttrTxt(elem, "file", file)) { - mjm_setString(pskin->file, file.c_str()); + auto file = ReadAttrFile(elem, "file", AssetDir()); + if (file.has_value()) { + mjm_setString(pskin->file, file->c_str()); } if (ReadAttrTxt(elem, "material", material)) { mjm_setString(pskin->material, material.c_str()); - } +} ReadAttrInt(elem, "group", &pskin->group); if (pskin->group<0 || pskin->group>=mjNGROUP) { throw mjXError(elem, "skin group must be between 0 and 5"); @@ -2507,7 +2509,7 @@ void mjXReader::OneFlexcomp(XMLElement* elem, mjmBody* pbody) { ReadAttr(elem, "scale", 3, fcomp.scale, text); ReadAttr(elem, "mass", 1, &fcomp.mass, text); ReadAttr(elem, "inertiabox", 1, &fcomp.inertiabox, text); - ReadAttrTxt(elem, "file", fcomp.file); + fcomp.file = ReadAttrFile(elem, "file", modelfiledir_).value_or(""); if (ReadAttrTxt(elem, "material", material)) { mjm_setString(dflex.material, material.c_str()); } @@ -3067,7 +3069,7 @@ void mjXReader::Visual(XMLElement* section) { // asset section parser void mjXReader::Asset(XMLElement* section) { int n; - string text, name, texname, content_type, file; + string text, name, texname, content_type; XMLElement* elem; // iterate over child elements @@ -3101,8 +3103,9 @@ void mjXReader::Asset(XMLElement* section) { if (ReadAttrTxt(elem, "content_type", content_type)) { mjm_setString(ptex->content_type, content_type.c_str()); } - if (ReadAttrTxt(elem, "file", file)) { - mjm_setString(ptex->file, file.c_str()); + auto file = ReadAttrFile(elem, "file", TextureDir()); + if (file.has_value()) { + mjm_setString(ptex->file, file->c_str()); } ReadAttrInt(elem, "width", &ptex->width); ReadAttrInt(elem, "height", &ptex->height); @@ -3139,12 +3142,12 @@ void mjXReader::Asset(XMLElement* section) { // separate files std::vector cubefiles(6); - ReadAttrTxt(elem, "fileright", cubefiles[0]); - ReadAttrTxt(elem, "fileleft", cubefiles[1]); - ReadAttrTxt(elem, "fileup", cubefiles[2]); - ReadAttrTxt(elem, "filedown", cubefiles[3]); - ReadAttrTxt(elem, "filefront", cubefiles[4]); - ReadAttrTxt(elem, "fileback", cubefiles[5]); + cubefiles[0] = ReadAttrFile(elem, "fileright", TextureDir()).value_or(""); + cubefiles[1] = ReadAttrFile(elem, "fileleft", TextureDir()).value_or(""); + cubefiles[2] = ReadAttrFile(elem, "fileup", TextureDir()).value_or(""); + cubefiles[3] = ReadAttrFile(elem, "filedown", TextureDir()).value_or(""); + cubefiles[4] = ReadAttrFile(elem, "filefront", TextureDir()).value_or(""); + cubefiles[5] = ReadAttrFile(elem, "fileback", TextureDir()).value_or(""); for (int i = 0; i < cubefiles.size(); i++) { mjm_setInStringVec(ptex->cubefiles, i, cubefiles[i].c_str()); } @@ -3181,22 +3184,23 @@ void mjXReader::Asset(XMLElement* section) { std::string("line = " + std::to_string(elem->GetLineNum()) + ", column = -1").c_str()); // read attributes - string name, content_type, file; + string name, content_type; if (ReadAttrTxt(elem, "name", name)) { mjm_setString(phf->name, name.c_str()); } if (ReadAttrTxt(elem, "content_type", content_type)) { mjm_setString(phf->content_type, content_type.c_str()); } - if (ReadAttrTxt(elem, "file", file)) { - mjm_setString(phf->file, file.c_str()); + auto file = ReadAttrFile(elem, "file", AssetDir()); + if (file.has_value()) { + mjm_setString(phf->file, file->c_str()); } ReadAttrInt(elem, "nrow", &phf->nrow); ReadAttrInt(elem, "ncol", &phf->ncol); ReadAttr(elem, "size", 4, phf->size, text, true); // allocate buffer for dynamic hfield, copy user data if given - if (file.empty() && phf->nrow>0 && phf->ncol>0) { + if (!file.has_value() && phf->nrow>0 && phf->ncol>0) { int nrow = phf->nrow; int ncol = phf->ncol; @@ -4034,3 +4038,54 @@ mjmDefault* mjXReader::GetClass(XMLElement* section) { void mjXReader::GetXMLPos(XMLElement* elem, mjCBase* obj) { obj->info = "line = " + std::to_string(elem->GetLineNum()); } + +// return true if c is a directory path separator (i.e. '/' or '\' on windows) +static bool IsSeperator(char c) { + return c == '/' || c == '\\'; +} + +void mjXReader::SetModelFileDir(std::string modelfiledir) { + modelfiledir_ = modelfiledir; + if (!modelfiledir_.empty() && !IsSeperator(modelfiledir_.back())) { + modelfiledir_.append("/"); + } +} + +void mjXReader::SetAssetDir(std::string assetdir) { + assetdir_ = assetdir; + if (!assetdir_.empty() && !IsSeperator(assetdir_.back())) { + assetdir_.append("/"); + } +} + +void mjXReader::SetMeshDir(std::string meshdir) { + meshdir_ = meshdir; + if (!meshdir_.empty() && !IsSeperator(meshdir_.back())) { + meshdir_.append("/"); + } +} + +void mjXReader::SetTextureDir(std::string texturedir) { + texturedir_ = texturedir; + if (!texturedir_.empty() && !IsSeperator(texturedir_.back())) { + texturedir_.append("/"); + } +} + +std::string mjXReader::AssetDir() const { + return modelfiledir_ + assetdir_; +} + +std::string mjXReader::MeshDir() const { + if (meshdir_.empty()) { + return AssetDir(); + } + return modelfiledir_ + meshdir_; +} + +std::string mjXReader::TextureDir() const { + if (texturedir_.empty()) { + return AssetDir(); + } + return modelfiledir_ + texturedir_; +} diff --git a/src/xml/xml_native_reader.h b/src/xml/xml_native_reader.h index cd8d8208..3c225011 100644 --- a/src/xml/xml_native_reader.h +++ b/src/xml/xml_native_reader.h @@ -16,6 +16,7 @@ #define MUJOCO_SRC_XML_XML_NATIVE_READER_H_ #include +#include #include "tinyxml2.h" @@ -34,6 +35,14 @@ class mjXReader : public mjXBase { void Parse(tinyxml2::XMLElement* root); // parse XML document void PrintSchema(std::stringstream& str, bool html, bool pad); // print text or HTML schema + void SetModelFileDir(std::string modelfiledir); + const std::string& ModelFileDir() const { return modelfiledir_; } + + // setters for directory defaults + void SetAssetDir(std::string assetdir); + void SetMeshDir(std::string meshdir); + void SetTextureDir(std::string texturedir); + // XML sections embedded in all formats static void Compiler(tinyxml2::XMLElement* section, mjCModel* mod); // compiler section static void Option(tinyxml2::XMLElement* section, mjOption* opt); // option section @@ -80,6 +89,16 @@ class mjXReader : public mjXBase { static void GetXMLPos(tinyxml2::XMLElement* elem, mjCBase* obj); // get xml position bool readingdefaults; // true while reading defaults + + // accessors for directory defaults + std::string AssetDir() const; + std::string MeshDir() const; + std::string TextureDir() const; + + std::string modelfiledir_; + std::string assetdir_; + std::string meshdir_; + std::string texturedir_; }; // MJCF schema diff --git a/src/xml/xml_util.cc b/src/xml/xml_util.cc index 91ab9858..2b230a73 100644 --- a/src/xml/xml_util.cc +++ b/src/xml/xml_util.cc @@ -32,8 +32,11 @@ #include "tinyxml2.h" +#include #include "cc/array_safety.h" +#include "engine/engine_resource.h" #include "engine/engine_util_errmem.h" +#include "user/user_util.h" #include "xml/xml_util.h" #include "xml/xml_numeric_format.h" @@ -156,6 +159,35 @@ XMLElement* NextSiblingElement(XMLElement* e, const char* name) { return nullptr; } +static std::string ResolveFilePath(XMLElement* e, std::string filename, + const std::string& dir) { + std::string path = ""; + if (mjuu_isabspath(filename)) { + return filename; + } + + // TODO(kylebayes): We first look in the base model directory for files to + // remain backwards compatible. + std::string full_filename = dir + filename; + mjResource *resource = mju_openResource(full_filename.c_str(), nullptr, 0); + if (resource != nullptr) { + mju_closeResource(resource); + return filename; + } + + XMLElement* parent = e->Parent()->ToElement(); + for (; parent; parent = parent->Parent()->ToElement()) { + if (!std::strcmp(parent->Value(), "include")) { + auto file_attr = mjXUtil::ReadAttrStr(parent, "dir", false); + if (file_attr.has_value()) { + path = file_attr.value(); + } + break; + } + } + return path + filename; +} + // constructor mjXSchema::mjXSchema(const char* schema[][mjXATTRNUM], unsigned nrow) { // set name and type @@ -562,8 +594,8 @@ mjXUtil::ReadAttrVec(XMLElement* elem, const char* attr, bool required); // if attribute is present, return attribute as a string -std::optional mjXUtil::ReadAttrStr(XMLElement* elem, const char* attr, - bool required) { +std::optional +mjXUtil::ReadAttrStr(XMLElement* elem, const char* attr, bool required) { const char* pstr = elem->Attribute(attr); // check if attribute exists @@ -578,7 +610,16 @@ std::optional mjXUtil::ReadAttrStr(XMLElement* elem, const char* at return std::string(pstr); } - +// if attribute is present, return attribute as a filename +std::optional +mjXUtil::ReadAttrFile(XMLElement* elem, const char* attr, + const std::string& dir, bool required) { + auto maybe_str = ReadAttrStr(elem, attr, required); + if (!maybe_str.has_value()) { + return std::nullopt; + } + return ResolveFilePath(elem, maybe_str.value(), dir); +} // if attribute is present, return numerical value of attribute template diff --git a/src/xml/xml_util.h b/src/xml/xml_util.h index c38d3890..1d819041 100644 --- a/src/xml/xml_util.h +++ b/src/xml/xml_util.h @@ -35,7 +35,6 @@ using tinyxml2::XMLElement; XMLElement* FirstChildElement(XMLElement* e, const char* name = nullptr); XMLElement* NextSiblingElement(XMLElement* e, const char* name = nullptr); - // XML Error info class [[nodiscard]] mjXError { public: @@ -109,6 +108,10 @@ class mjXUtil { static std::optional ReadAttrStr(tinyxml2::XMLElement* elem, const char* attr, bool required = false); + // if attribute is present, return attribute as a filename + static std::optional ReadAttrFile(tinyxml2::XMLElement* elem, const char* attr, + const std::string& dir = "", bool required = false); + // if attribute is present, return numerical value of attribute template static std::optional ReadAttrNum(tinyxml2::XMLElement* elem, const char* attr, diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 046bdadf..122760a9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -58,6 +58,8 @@ target_link_libraries( fixture PUBLIC absl::core_headers absl::synchronization + absl::flat_hash_map + absl::flat_hash_set gtest gmock mujoco::mujoco diff --git a/test/fixture.cc b/test/fixture.cc index 4fdad189..a0c8eb25 100644 --- a/test/fixture.cc +++ b/test/fixture.cc @@ -14,6 +14,8 @@ #include "test/fixture.h" +#include +#include #include #include #include @@ -21,12 +23,15 @@ #include #include #include + +#include #include #include #include #include #include +#include #include #include #include @@ -167,4 +172,120 @@ std::vector GetCtrlNoise(const mjModel* m, int nsteps, return ctrl; } +MockFilesystem::MockFilesystem(std::string unit_test_name) { + prefix_ = absl::StrCat("MjMock.", unit_test_name); + dir_ = "/"; + if (mjp_getResourceProvider(prefix_.c_str()) != nullptr) { + return; + } + + mjpResourceProvider resourceProvider; + mjp_defaultResourceProvider(&resourceProvider); + resourceProvider.prefix = prefix_.c_str(); + resourceProvider.data = (void *) this; + + resourceProvider.open = +[](mjResource* resource) { + MockFilesystem *fs = static_cast(resource->provider->data); + std::string filename = fs->StripPrefix(resource->name); + return fs->FileExists(filename) ? 1 : 0; + }; + + resourceProvider.read =+[](mjResource* resource, const void** buffer) { + MockFilesystem *fs = static_cast(resource->provider->data); + std::string filename = fs->StripPrefix(resource->name); + return (int) fs->GetFile(filename, (const unsigned char**) buffer); + }; + + resourceProvider.getdir = +[](mjResource* resource, const char** dir, + int* ndir) { + MockFilesystem *fs = static_cast(resource->provider->data); + *dir = resource->name; + + // find last directory path separator + int length = fs->Prefix().size() + 1; + for (int i = length; resource->name[i]; ++i) { + if (resource->name[i] == '/' || resource->name[i] == '\\') { + length = i + 1; + } + } + *ndir = length; + }; + + resourceProvider.close = +[](mjResource* resource) {}; + mjp_registerResourceProvider(&resourceProvider); +} + +bool MockFilesystem::AddFile(std::string filename, const unsigned char* data, + std::size_t ndata) { + std::string fullfilename = PathReduce(dir_, filename); + auto [it, inserted] = filenames_.insert(fullfilename); + if (inserted) { + data_[fullfilename] = std::vector(data, data + ndata); + } + return inserted; +} + +bool MockFilesystem::FileExists(const std::string& filename) { + std::string fullfilename = PathReduce(dir_, filename); + return filenames_.find(fullfilename) != filenames_.end(); +} + +std::size_t MockFilesystem::GetFile(const std::string& filename, + const unsigned char** buffer) const { + std::string fullfilename = PathReduce(dir_, filename); + auto it = data_.find(fullfilename); + if (it == data_.end()) { + return 0; + } + *buffer = it->second.data(); + return it->second.size(); +} + +void MockFilesystem::ChangeDirectory(std::string dir) { + if (dir.empty()) { + return; + } + + dir_ = PathReduce(dir_, dir); + if (dir_.back() != '/') { + dir_ = absl::StrCat(dir_, "/"); + } +} + +std::string MockFilesystem::FullPath(const std::string& path) const { + return absl::StrCat(prefix_, ":", PathReduce(dir_, path)); +} + +std::string MockFilesystem::StripPrefix(const char* path) const { + return &path[prefix_.size() + 1]; +} + +std::string MockFilesystem::PathReduce(const std::string& current_dir, + const std::string& path) { + std::stringstream stream; + if (!path.empty() && path[0] != '/') { + stream = std::stringstream(absl::StrCat(current_dir, path)); + } else { + stream = std::stringstream(path); + } + + std::string temp; + std::vector dirs; + while (std::getline(stream, temp, '/')) { + if (temp == ".." && !dirs.empty()) { + dirs.pop_back(); + continue; + } + + if (temp != "." && !temp.empty()) { + dirs.push_back(temp); + } + } + if (dirs.empty()) { + return "/"; + } + + return absl::StrJoin(dirs, "/"); +} + } // namespace mujoco diff --git a/test/fixture.h b/test/fixture.h index 6091a2a5..98fad470 100644 --- a/test/fixture.h +++ b/test/fixture.h @@ -22,6 +22,8 @@ #include #include +#include +#include #include #include @@ -99,6 +101,51 @@ std::string SaveAndReadXml(const mjModel* model); std::vector GetCtrlNoise(const mjModel* m, int nsteps, mjtNum ctrlnoise = 0.01); +// Installs a mock filesystem via a resource provider. To obtain thread safety, +// each filesystem is scoped for individual unit tests with destructive +// operations not permitted. +class MockFilesystem { + public: + // constructs mock filesystem. A unique name (normally the unit test name) + // should be passed in. + MockFilesystem(std::string unit_test_name); + + // Move and copy operations are forbidden. + MockFilesystem(MockFilesystem&& other) = delete; + MockFilesystem& operator=(MockFilesystem&& other) = delete; + MockFilesystem(const MockFilesystem& other) = delete; + MockFilesystem& operator=(const MockFilesystem& other) = delete; + + // Returns the prefix registered for the resource provider. + const std::string& Prefix() const { return prefix_; } + + // Adds file to the current directory. Returns false if file already exists. + bool AddFile(std::string filename, const unsigned char* data, + std::size_t ndata); + + // Returns true if mock filesystem has file. + bool FileExists(const std::string& filename); + + // Change the current directory. + void ChangeDirectory(std::string dir); + + // Helper functions for resource provider callbacks. + std::size_t GetFile(const std::string& filename, + const unsigned char** buffer) const; + std::string FullPath(const std::string& path) const; + + + private: + std::string StripPrefix(const char* path) const; + static std::string PathReduce(const std::string& current_dir, + const std::string& path); + + absl::flat_hash_set filenames_; + absl::flat_hash_map> data_; + std::string prefix_; + std::string dir_; // current directory +}; + // Installs all plugins class PluginTest : public MujocoTest { public: diff --git a/test/fixture_test.cc b/test/fixture_test.cc index 4fd31cf0..e893024c 100644 --- a/test/fixture_test.cc +++ b/test/fixture_test.cc @@ -14,6 +14,8 @@ #include "test/fixture.h" +#include + #include #include #include @@ -49,5 +51,22 @@ TEST_F(MujocoErrorTestGuardTest, NestedErrorGuards) { EXPECT_THAT(mju_user_warning, IsNull()); } +TEST_F(MujocoTestTest, MockFilesystemTest) { + MockFilesystem fs("MockFilesystemTest"); + std::array data = {'a', 'b', 'c'}; + fs.ChangeDirectory("tmp"); + + fs.AddFile("../tmp2/file2", data.data(), data.size()); + fs.AddFile("./file1", data.data(), data.size()); + + ASSERT_TRUE(fs.FileExists("/tmp/file1")); + ASSERT_TRUE(fs.FileExists("/tmp2/file2")); + + fs.ChangeDirectory("../tmp2"); + + ASSERT_TRUE(fs.FileExists("../tmp/file1")); + ASSERT_TRUE(fs.FileExists("file2")); +} + } // namespace } // namespace mujoco diff --git a/test/xml/xml_native_reader_test.cc b/test/xml/xml_native_reader_test.cc index 5ce35ec2..ae45a4a9 100644 --- a/test/xml/xml_native_reader_test.cc +++ b/test/xml/xml_native_reader_test.cc @@ -464,6 +464,21 @@ TEST_F(XMLReaderTest, RepeatedDefaultName) { // ------------------------ test including ------------------------------------- +// credit: https://www.mjt.me.uk/posts/smallest-png/ +static constexpr unsigned char kTinyPng[] = + { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, + 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x03, 0x00, + 0x00, 0x00, 0x66, 0xBC, 0x3A, 0x25, 0x00, 0x00, 0x00, + 0x03, 0x50, 0x4C, 0x54, 0x45, 0xB5, 0xD0, 0xD0, 0x63, + 0x04, 0x16, 0xEA, 0x00, 0x00, 0x00, 0x1F, 0x49, 0x44, + 0x41, 0x54, 0x68, 0x81, 0xED, 0xC1, 0x01, 0x0D, 0x00, + 0x00, 0x00, 0xC2, 0xA0, 0xF7, 0x4F, 0x6D, 0x0E, 0x37, + 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xBE, 0x0D, 0x21, 0x00, 0x00, 0x01, 0x9A, 0x60, 0xE1, + 0xD5, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, + 0xAE, 0x42, 0x60, 0x82 }; + TEST_F(XMLReaderTest, IncludeTest) { static constexpr char xml[] = R"( @@ -542,7 +557,6 @@ TEST_F(XMLReaderTest, IncludeSameFileTest) { )"; - auto vfs = std::make_unique(); mj_defaultVFS(vfs.get()); @@ -558,7 +572,229 @@ TEST_F(XMLReaderTest, IncludeSameFileTest) { mj_deleteVFS(vfs.get()); } +TEST_F(XMLReaderTest, IncludePathTest) { + static constexpr char xml[] = R"( + + + + + + + )"; + + static constexpr char xml1[] = R"( + + + )"; + + static constexpr char xml2[]= R"( + + + + )"; + + static constexpr char xml3[]= R"( + + + )"; + + MockFilesystem fs("IncludePathTest"); + fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); + std::string modelpath = fs.FullPath("model.xml"); + + fs.ChangeDirectory("submodels/"); + fs.AddFile("model1.xml", (const unsigned char*) xml1, sizeof(xml1)); + fs.AddFile("model2.xml", (const unsigned char*) xml2, sizeof(xml2)); + fs.AddFile("subsubmodels/model3.xml", (const unsigned char*) xml3, sizeof(xml3)); + fs.ChangeDirectory("/"); + + mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, + nullptr, 0); + ASSERT_THAT(model, NotNull()); + EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "ball"), 2); + EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "another_box"), 3); + mj_deleteModel(model); +} + +TEST_F(XMLReaderTest, FallbackIncludePathTest) { + static constexpr char xml[] = R"( + + + + + + + )"; + + static constexpr char xml1[] = R"( + + + )"; + + static constexpr char xml2[]= R"( + + + + )"; + + static constexpr char xml3[]= R"( + + + )"; + + MockFilesystem fs("FallbackIncludePathTest"); + fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); + std::string modelpath = fs.FullPath("model.xml"); + + fs.AddFile("model1.xml", (const unsigned char*) xml1, sizeof(xml1)); + fs.AddFile("submodels/model2.xml", (const unsigned char*) xml2, sizeof(xml2)); + fs.AddFile("subsubmodels/model3.xml", (const unsigned char*) xml3, + sizeof(xml3)); + + std::array error; + mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, + error.data(), error.size()); + ASSERT_THAT(model, NotNull()); + EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "ball"), 2); + EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "another_box"), 3); + mj_deleteModel(model); +} + +TEST_F(XMLReaderTest, IncludeAssetsTest) { + static constexpr char xml[] = R"( + + + + + + + )"; + static constexpr char assets[] = R"( + + + + + + + + )"; + static constexpr char subassets[] = R"( + + + + + )"; + + MockFilesystem fs("IncludeAssetsTest"); + fs.AddFile("assets/tiny.png", kTinyPng, sizeof(kTinyPng)); + fs.AddFile("assets/subassets/subtiny.png", kTinyPng, sizeof(kTinyPng)); + fs.AddFile("assets/assets.xml", (const unsigned char*) assets, + sizeof(assets)); + fs.AddFile("assets/subassets/assets.xml", (const unsigned char*) subassets, + sizeof(subassets)); + fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); + std::string modelpath = fs.FullPath("model.xml"); + + // loading the file should be successful + mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, nullptr, 0); + + EXPECT_THAT(model, NotNull()); + + mj_deleteModel(model); +} + +TEST_F(XMLReaderTest, FallbackIncludeAssetsTest) { + static constexpr char xml[] = R"( + + + + + + + )"; + static constexpr char assets[] = R"( + + + + + + + + )"; + static constexpr char subassets[] = R"( + + + + + )"; + + MockFilesystem fs("FallbackIncludeAssetsTest"); + fs.AddFile("assets/tiny.png", kTinyPng, sizeof(kTinyPng)); + + // need to fallback for backwards compatibility + fs.AddFile("subtiny.png", kTinyPng, sizeof(kTinyPng)); + + fs.AddFile("assets/assets.xml", (const unsigned char*) assets, + sizeof(assets)); + fs.AddFile("assets/subassets/assets.xml", (const unsigned char*) subassets, + sizeof(subassets)); + fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); + std::string modelpath = fs.FullPath("model.xml"); + + // loading the file should be successful + std::array error; + mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, + error.data(), error.size()); + EXPECT_THAT(model, NotNull()); + + mj_deleteModel(model); +} + +TEST_F(XMLReaderTest, IncludeAbsoluteTest) { + static constexpr char xml[] = R"( + + + + + + + )"; + static constexpr char assets[] = R"( + + + + + + + + )"; + static constexpr char subassets[] = R"( + + + + + )"; + + MockFilesystem fs("IncludeAbsoluteTest"); + fs.AddFile("assets/tiny.png", kTinyPng, sizeof(kTinyPng)); + fs.AddFile("assets/subtiny.png", kTinyPng, sizeof(kTinyPng)); + fs.AddFile("assets/assets.xml", (const unsigned char*) assets, + sizeof(assets)); + fs.AddFile("assets/subassets/assets.xml", (const unsigned char*) subassets, + sizeof(subassets)); + fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); + std::string modelpath = fs.FullPath("model.xml"); + + std::array error; + // loading the file should be successful + mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, + error.data(), error.size()); + EXPECT_THAT(model, NotNull()); + + mj_deleteModel(model); +} // ------------------------ test frame parsing --------------------------------- + TEST_F(XMLReaderTest, ParseFrame) { static constexpr char xml[] = R"(