Improve include meta-element to load included resourced from the directory relative to the included XML file, fully backwards compatible.
PiperOrigin-RevId: 608312069 Change-Id: I80eadb32d7d78fb35bd24975f2e514021c2aa009
This commit is contained in:
committed by
Copybara-Service
parent
b77dfc683a
commit
24eb4c9f09
@@ -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;
|
||||
}
|
||||
|
||||
+75
-18
@@ -23,6 +23,7 @@
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
|
||||
#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<string>& included) {
|
||||
static void mjIncludeXML(mjXReader& reader, XMLElement* elem,
|
||||
std::string_view dir, const mjVFS* vfs,
|
||||
std::unordered_set<std::string>& 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<char, 1024> 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<char, 1024> 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<string> included = {filename};
|
||||
mjIncludeXML(root, model->modelfiledir, vfs, included);
|
||||
std::unordered_set<std::string> 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);
|
||||
}
|
||||
|
||||
@@ -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<string> 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_;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#define MUJOCO_SRC_XML_XML_NATIVE_READER_H_
|
||||
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#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
|
||||
|
||||
+44
-3
@@ -32,8 +32,11 @@
|
||||
|
||||
#include "tinyxml2.h"
|
||||
|
||||
#include <mujoco/mujoco.h>
|
||||
#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<std::string> mjXUtil::ReadAttrStr(XMLElement* elem, const char* attr,
|
||||
bool required) {
|
||||
std::optional<std::string>
|
||||
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<std::string> mjXUtil::ReadAttrStr(XMLElement* elem, const char* at
|
||||
return std::string(pstr);
|
||||
}
|
||||
|
||||
|
||||
// if attribute is present, return attribute as a filename
|
||||
std::optional<std::string>
|
||||
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<typename T>
|
||||
|
||||
+4
-1
@@ -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<std::string> ReadAttrStr(tinyxml2::XMLElement* elem, const char* attr,
|
||||
bool required = false);
|
||||
|
||||
// if attribute is present, return attribute as a filename
|
||||
static std::optional<std::string> ReadAttrFile(tinyxml2::XMLElement* elem, const char* attr,
|
||||
const std::string& dir = "", bool required = false);
|
||||
|
||||
// if attribute is present, return numerical value of attribute
|
||||
template<typename T>
|
||||
static std::optional<T> ReadAttrNum(tinyxml2::XMLElement* elem, const char* attr,
|
||||
|
||||
@@ -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
|
||||
|
||||
+121
@@ -14,6 +14,8 @@
|
||||
|
||||
#include "test/fixture.h"
|
||||
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
@@ -21,12 +23,15 @@
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <absl/base/const_init.h>
|
||||
#include <absl/strings/str_cat.h>
|
||||
#include <absl/strings/str_join.h>
|
||||
#include <absl/synchronization/mutex.h>
|
||||
#include <mujoco/mjmodel.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
@@ -167,4 +172,120 @@ std::vector<mjtNum> 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<MockFilesystem*>(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<MockFilesystem*>(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<MockFilesystem*>(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<std::string> 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
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <absl/container/flat_hash_map.h>
|
||||
#include <absl/container/flat_hash_set.h>
|
||||
#include <mujoco/mjmodel.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
@@ -99,6 +101,51 @@ std::string SaveAndReadXml(const mjModel* model);
|
||||
std::vector<mjtNum> 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<std::string> filenames_;
|
||||
absl::flat_hash_map<std::string, std::vector<unsigned char>> data_;
|
||||
std::string prefix_;
|
||||
std::string dir_; // current directory
|
||||
};
|
||||
|
||||
// Installs all plugins
|
||||
class PluginTest : public MujocoTest {
|
||||
public:
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
#include "test/fixture.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest-spi.h>
|
||||
#include <gtest/gtest.h>
|
||||
@@ -49,5 +51,22 @@ TEST_F(MujocoErrorTestGuardTest, NestedErrorGuards) {
|
||||
EXPECT_THAT(mju_user_warning, IsNull());
|
||||
}
|
||||
|
||||
TEST_F(MujocoTestTest, MockFilesystemTest) {
|
||||
MockFilesystem fs("MockFilesystemTest");
|
||||
std::array<unsigned char, 3> 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
|
||||
|
||||
@@ -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"(
|
||||
<mujoco>
|
||||
@@ -542,7 +557,6 @@ TEST_F(XMLReaderTest, IncludeSameFileTest) {
|
||||
<geom name="box" type="box" size="1 1 1"/>
|
||||
</mujoco>)";
|
||||
|
||||
|
||||
auto vfs = std::make_unique<mjVFS>();
|
||||
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"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<geom name="plane" type="plane" size="1 1 1"/>
|
||||
<include file="submodels/model1.xml"/>
|
||||
<include file="submodels/model2.xml"/>
|
||||
</worldbody>
|
||||
</mujoco>)";
|
||||
|
||||
static constexpr char xml1[] = R"(
|
||||
<mujoco>
|
||||
<geom name="box" type="box" size="1 1 1"/>
|
||||
</mujoco>)";
|
||||
|
||||
static constexpr char xml2[]= R"(
|
||||
<mujoco>
|
||||
<geom name="ball" type="sphere" size="2"/>
|
||||
<include file="subsubmodels/model3.xml"/>
|
||||
</mujoco>)";
|
||||
|
||||
static constexpr char xml3[]= R"(
|
||||
<mujoco>
|
||||
<geom name="another_box" type="box" size="2 2 2"/>
|
||||
</mujoco>)";
|
||||
|
||||
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"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<geom name="plane" type="plane" size="1 1 1"/>
|
||||
<include file="model1.xml"/>
|
||||
<include file="submodels/model2.xml"/>
|
||||
</worldbody>
|
||||
</mujoco>)";
|
||||
|
||||
static constexpr char xml1[] = R"(
|
||||
<mujoco>
|
||||
<geom name="box" type="box" size="1 1 1"/>
|
||||
</mujoco>)";
|
||||
|
||||
static constexpr char xml2[]= R"(
|
||||
<mujoco>
|
||||
<geom name="ball" type="sphere" size="2"/>
|
||||
<include file="subsubmodels/model3.xml"/>
|
||||
</mujoco>)";
|
||||
|
||||
static constexpr char xml3[]= R"(
|
||||
<mujoco>
|
||||
<geom name="another_box" type="box" size="2 2 2"/>
|
||||
</mujoco>)";
|
||||
|
||||
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<char, 1024> 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"(
|
||||
<mujoco>
|
||||
<include file="assets/assets.xml"/>
|
||||
<worldbody>
|
||||
<geom type="plane" material="material" size="4 4 4"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
static constexpr char assets[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<texture file="tiny.png" type="2d"/>
|
||||
<material name="material" texture="tiny"/>
|
||||
<include file="subassets/assets.xml"/>
|
||||
</asset>
|
||||
</mujoco>
|
||||
)";
|
||||
static constexpr char subassets[] = R"(
|
||||
<mujoco>
|
||||
<texture file="subtiny.png" type="2d"/>
|
||||
<material name="submaterial" texture="subtiny"/>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
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"(
|
||||
<mujoco>
|
||||
<include file="assets/assets.xml"/>
|
||||
<worldbody>
|
||||
<geom type="plane" material="material" size="4 4 4"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
static constexpr char assets[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<texture file="tiny.png" type="2d"/>
|
||||
<material name="material" texture="tiny"/>
|
||||
<include file="subassets/assets.xml"/>
|
||||
</asset>
|
||||
</mujoco>
|
||||
)";
|
||||
static constexpr char subassets[] = R"(
|
||||
<mujoco>
|
||||
<texture file="subtiny.png" type="2d"/>
|
||||
<material name="submaterial" texture="subtiny"/>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
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<char, 1024> 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"(
|
||||
<mujoco>
|
||||
<include file="assets/assets.xml"/>
|
||||
<worldbody>
|
||||
<geom type="plane" material="material" size="4 4 4"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
static constexpr char assets[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<texture file="tiny.png" type="2d"/>
|
||||
<material name="material" texture="tiny"/>
|
||||
<include file="subassets/assets.xml"/>
|
||||
</asset>
|
||||
</mujoco>
|
||||
)";
|
||||
static constexpr char subassets[] = R"(
|
||||
<mujoco>
|
||||
<texture file="MjMock.IncludeAbsoluteTest:assets/subtiny.png" type="2d"/>
|
||||
<material name="submaterial" texture="subtiny"/>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
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<char, 1024> 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"(
|
||||
<mujoco>
|
||||
|
||||
Reference in New Issue
Block a user