Files
Mujoco_WASM/test/xml/xml_native_reader_test.cc
T
Yuval Tassa 608115ab95 Add output attribute to camera elements.
This attribute allows specifying the intended output types (e.g., RGB, depth, normal) for each camera in the XML. The output types are stored as bit flags in `mjModel`.

PiperOrigin-RevId: 853316261
Change-Id: I1ad2e94102593cf402756f8f4c80509371ca86d2
2026-01-07 10:22:27 -08:00

3295 lines
97 KiB
C++

// Copyright 2021 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Tests for xml/xml_native_reader.cc.
#include <array>
#include <cmath>
#include <limits>
#include <memory>
#include <string>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <mujoco/mjmodel.h>
#include <mujoco/mjspec.h>
#include <mujoco/mujoco.h>
#include "src/cc/array_safety.h"
#include "src/engine/engine_util_errmem.h"
#include "src/xml/xml_api.h"
#include "test/fixture.h"
namespace mujoco {
namespace {
using ::std::string;
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::FloatEq;
using ::testing::HasSubstr;
using ::testing::IsNan;
using ::testing::IsNull;
using ::testing::NotNull;
using XMLReaderTest = MujocoTest;
TEST_F(XMLReaderTest, UniqueElementTest) {
std::array<char, 1024> error;
static constexpr char xml[] = R"(
<mujoco>
<option>
<flag sensor="disable"/>
<flag sensor="disable"/>
</option>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("unique element 'flag' found 2 times"));
}
TEST_F(XMLReaderTest, MemorySize) {
std::array<char, 1024> error;
{
static constexpr char xml[] = R"(
<mujoco>
<size memory="512"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->narena, 512);
mj_deleteModel(model);
}
{
static constexpr char xml[] = R"(
<mujoco>
<size memory="1K "/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->narena, 1024);
mj_deleteModel(model);
}
{
static constexpr char xml[] = R"(
<mujoco>
<size memory=" 10K"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->narena, 10240);
mj_deleteModel(model);
}
{
static constexpr char xml[] = R"(
<mujoco>
<size memory=" 4M "/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->narena, 4*1024*1024);
mj_deleteModel(model);
}
{
static constexpr char xml[] = R"(
<mujoco>
<size memory="1G"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->narena, 1024*1024*1024);
mj_deleteModel(model);
}
{
static constexpr char xml[] = R"(
<mujoco>
<size memory="1073741824"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->narena, 1024*1024*1024);
mj_deleteModel(model);
}
}
TEST_F(XMLReaderTest, InvalidMemorySize) {
std::array<char, 1024> error;
{
static constexpr char xml[] = R"(
<mujoco>
<size memory="-3"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
}
{
static constexpr char xml[] = R"(
<mujoco>
<size memory="1 M"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
}
{
static constexpr char xml[] = R"(
<mujoco>
<size memory="2X"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
}
{
static constexpr char xml[] = R"(
<mujoco>
<size memory="K"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
}
}
TEST_F(XMLReaderTest, InvalidNUserBody) {
static constexpr char xml[] = R"(
<mujoco>
<size nuser_body="-2"/>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("nuser_body"));
EXPECT_THAT(error.data(), HasSubstr("line 3"));
}
TEST_F(XMLReaderTest, InvalidNUserJoint) {
static constexpr char xml[] = R"(
<mujoco>
<size nuser_jnt="-2"/>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("nuser_jnt"));
EXPECT_THAT(error.data(), HasSubstr("line 3"));
}
TEST_F(XMLReaderTest, InvalidNUserGeom) {
static constexpr char xml[] = R"(
<mujoco>
<size nuser_geom="-2"/>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("nuser_geom"));
EXPECT_THAT(error.data(), HasSubstr("line 3"));
}
TEST_F(XMLReaderTest, InvalidNUserSite) {
static constexpr char xml[] = R"(
<mujoco>
<size nuser_site="-2"/>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("nuser_site"));
EXPECT_THAT(error.data(), HasSubstr("line 3"));
}
TEST_F(XMLReaderTest, InvalidNUserCamera) {
static constexpr char xml[] = R"(
<mujoco>
<size nuser_cam="-2"/>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("nuser_cam"));
EXPECT_THAT(error.data(), HasSubstr("line 3"));
}
TEST_F(XMLReaderTest, InvalidNUserTendon) {
static constexpr char xml[] = R"(
<mujoco>
<size nuser_tendon="-2"/>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("nuser_tendon"));
EXPECT_THAT(error.data(), HasSubstr("line 3"));
}
TEST_F(XMLReaderTest, InvalidNUserActuator) {
static constexpr char xml[] = R"(
<mujoco>
<size nuser_actuator="-2"/>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("nuser_actuator"));
EXPECT_THAT(error.data(), HasSubstr("line 3"));
}
TEST_F(XMLReaderTest, InvalidNUserSensor) {
static constexpr char xml[] = R"(
<mujoco>
<size nuser_sensor="-2"/>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("nuser_sensor"));
EXPECT_THAT(error.data(), HasSubstr("line 3"));
}
TEST_F(XMLReaderTest, CanParseInf) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1" pos="5e-1 -INF iNf"/>
<geom size="1" pos="inF -inf Inf"/>
</body>
</worldbody>
</mujoco>
)";
const double inf = std::numeric_limits<double>::infinity();
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
EXPECT_EQ(model->geom_pos[0], 0.5);
EXPECT_EQ(model->geom_pos[1], -inf);
EXPECT_THAT(model->geom_pos[2], inf);
EXPECT_EQ(model->geom_pos[3], inf);
EXPECT_EQ(model->geom_pos[4], -inf);
EXPECT_EQ(model->geom_pos[5], inf);
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, CanParseNanAndRaisesWarning) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1" pos="nan NaN NAN"/>
<geom size="1" pos="1.0 0.0 nAn"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
static char warning[1024];
warning[0] = '\0';
mju_user_warning = [](const char* msg) {
util::strcpy_arr(warning, msg);
};
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
EXPECT_THAT(warning, HasSubstr("XML contains a 'NaN'"));
EXPECT_THAT(model->geom_pos[0], IsNan());
EXPECT_THAT(model->geom_pos[1], IsNan());
EXPECT_THAT(model->geom_pos[2], IsNan());
EXPECT_EQ(model->geom_pos[3], 1);
EXPECT_EQ(model->geom_pos[4], 0);
EXPECT_THAT(model->geom_pos[5], IsNan());
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, InvalidArrayElement) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1" axisangle="1.0 0.0 0.0 [[1]]"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("bad format in attribute 'axisangle'"));
EXPECT_THAT(error.data(), HasSubstr("line 5"));
}
TEST_F(XMLReaderTest, InvalidArrayLength) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1" axisangle="1 0 0 0 1"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("has too much data"));
EXPECT_THAT(error.data(), HasSubstr("line 5"));
}
TEST_F(XMLReaderTest, InvalidQuaternion) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1" quat="0 0 0 0"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("zero quaternion is not allowed"));
EXPECT_THAT(error.data(), HasSubstr("line 5"));
}
TEST_F(XMLReaderTest, InvalidNumber) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1" axisangle="1 0.1.2.3"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("bad format in attribute 'axisangle'"));
EXPECT_THAT(error.data(), HasSubstr("line 5"));
}
TEST_F(XMLReaderTest, InvalidNumberRange) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="mesh" file="mesh.stl" face="100000000000000000000000"/>
</asset>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(),
HasSubstr("number is too large in attribute 'face'"));
EXPECT_THAT(error.data(), HasSubstr("line 4"));
}
TEST_F(XMLReaderTest, InvalidNumberOfAttributes) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body pos="0 0 .3">
<freejoint/>
<geom name="ellipsoid" type="ellipsoid" size=".1 .1"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("size 2 must be positive"));
EXPECT_THAT(error.data(), HasSubstr("line 6"));
}
TEST_F(XMLReaderTest, AllowsSpaces) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1" axisangle="1 0 0 0 "/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, InvalidDoubleOrientation) {
std::string prefix = "<mujoco><worldbody><";
std::string suffix = "/></worldbody></mujoco>";
std::vector<std::string> orientations = {
R"( quat="0 1 0 0" )",
R"( euler="1.7 2.9 0.1" )",
R"( zaxis="1.7 2.9 0.1" )",
R"( axisangle="1.7 2.9 0.1 0" )",
R"( xyaxes="1.7 2.9 0.1 0.4 1.4 0.6" )",
};
std::vector<std::string> fields = {
"geom", "body", "camera", "site"
};
for (auto const& field : fields) {
for (auto const& orient1 : orientations) {
for (auto const& orient2 : orientations) {
if (orient1 == orient2) continue;
std::string xml = prefix + field + orient1 + orient2 + suffix;
std::array<char, 1024> error;
mjModel* model =
LoadModelFromString(xml.c_str(), error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(
error.data(),
HasSubstr("multiple orientation specifiers are not allowed"));
}
}
}
}
TEST_F(XMLReaderTest, ClassOverridesChildclass) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<default class="size2">
<geom size="2"/>
</default>
<default class="size3">
<geom size="3"/>
</default>
</default>
<worldbody>
<frame childclass="size2">
<geom/>
<geom class="size3"/>
<body childclass="size2">
<geom/>
<geom class="size3"/>
</body>
</frame>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->geom_size[3*0], 2);
EXPECT_EQ(model->geom_size[3*1], 3);
EXPECT_EQ(model->geom_size[3*2], 2);
EXPECT_EQ(model->geom_size[3*3], 3);
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, RepeatedDefaultName) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<default class="sphere">
<geom type="sphere" size="1"/>
</default>
<default class="sphere">
<geom type="capsule" size="1 1"/>
</default>
</default>
<worldbody>
<body>
<geom class="sphere"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull()) << error.data();
EXPECT_THAT(error.data(), HasSubstr("repeated default class name"));
}
TEST_F(XMLReaderTest, InvalidDefaultClassName) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<default class="sphere">
<geom type="sphere" size="1"/>
</default>
</default>
<worldbody>
<body>
<geom class="invalid"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull()) << error.data();
EXPECT_THAT(error.data(),
AllOf(HasSubstr("unknown default class name 'invalid'"),
HasSubstr("Element 'geom'"), HasSubstr("line 10")));
}
TEST_F(XMLReaderTest, InvalidTopDefaultClassName) {
static constexpr char xml[] = R"(
<mujoco>
<default class="sphere">
<geom type="sphere" size="1"/>
</default>
<worldbody>
<body>
<geom class="sphere"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull()) << error.data();
EXPECT_THAT(error.data(),
HasSubstr("top-level default class 'main' cannot be renamed"));
}
TEST_F(XMLReaderTest, ValidTopDefaultClassName) {
static constexpr char xml[] = R"(
<mujoco>
<default class="main">
<geom type="sphere" size="1"/>
</default>
<worldbody>
<body>
<geom class="main"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
mj_deleteModel(model);
}
// ------------------------ test including -------------------------------------
// tiny RGB 2 x 3 PNG file
static constexpr unsigned char kTinyPng[] = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02,
0x08, 0x02, 0x00, 0x00, 0x00, 0x12, 0x16, 0xf1, 0x4d, 0x00, 0x00, 0x00,
0x1c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x78, 0xc1, 0xc0, 0xc0,
0xc0, 0xf0, 0xbf, 0xb8, 0xb8, 0x98, 0x81, 0xe1, 0x3f, 0xc3, 0xff, 0xff,
0xff, 0xc5, 0xc4, 0xc4, 0x00, 0x46, 0xd7, 0x07, 0x7f, 0xd2, 0x52, 0xa1,
0x41, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60,
0x82
};
// mesh OBJ file of a cube
static constexpr char kTinyObj[] = R"(
v -1 -1 1
v 1 -1 1
v -1 1 1
v 1 1 1
v -1 1 -1
v 1 1 -1
v -1 -1 -1
v 1 -1 -1)";
TEST_F(XMLReaderTest, IncludeTest) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<geom name="plane" type="plane" size="1 1 1"/>
<include file="model1.xml"/>
<include file="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="model3.xml"/>
</mujoco>)";
static constexpr char xml3[]= R"(
<mujoco>
<geom name="another_box" type="box" size="2 2 2"/>
</mujoco>)";
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
mj_addBufferVFS(vfs.get(), "model1.xml", xml1, sizeof(xml1));
mj_addBufferVFS(vfs.get(), "model2.xml", xml2, sizeof(xml2));
mj_addBufferVFS(vfs.get(), "model3.xml", xml3, sizeof(xml3));
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(),
error.size(), vfs.get());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "ball"), 2);
EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "another_box"), 3);
mj_deleteModel(model);
mj_deleteVFS(vfs.get());
}
TEST_F(XMLReaderTest, IncludeChildTest) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<geom name="plane" type="plane" size="1 1 1"/>
<include file="model1.xml">
<geom name="box" type="box" size="1 1 1"/>
</include>
</worldbody>
</mujoco>)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("Include element cannot have children"));
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, IncludeSameFileTest) {
static constexpr char xml[] = R"(
<mujoco>
<include file="model1.xml"/>
<include file="model1.xml"/>
</mujoco>)";
static constexpr char xml1[] = R"(
<mujoco>
<geom name="box" type="box" size="1 1 1"/>
</mujoco>)";
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
mj_addBufferVFS(vfs.get(), "model1.xml", xml1, sizeof(xml1));
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size(),
vfs.get());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("File 'model1.xml' already included"));
mj_deleteModel(model);
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>)";
mjVFS vfs;
mj_defaultVFS(&vfs);
mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml));
mj_addBufferVFS(&vfs, "submodels/model1.xml", xml1, sizeof(xml1));
mj_addBufferVFS(&vfs, "submodels/model2.xml", xml2, sizeof(xml2));
mj_addBufferVFS(&vfs, "submodels/subsubmodels/model3.xml", xml3,
sizeof(xml3));
std::array<char, 1024> error;
mjModel* model = mj_loadXML("model.xml", &vfs, error.data(),
error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "ball"), 2);
EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "another_box"), 3);
mj_deleteModel(model);
mj_deleteVFS(&vfs);
}
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>)";
mjVFS vfs;
mj_defaultVFS(&vfs);
mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml));
mj_addBufferVFS(&vfs, "model1.xml", xml1, sizeof(xml1));
mj_addBufferVFS(&vfs, "submodels/model2.xml", xml2, sizeof(xml2));
mj_addBufferVFS(&vfs, "subsubmodels/model3.xml", xml3, sizeof(xml3));
std::array<char, 1024> error;
mjModel* model = mj_loadXML("model.xml", &vfs,
error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "ball"), 2);
EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "another_box"), 3);
mj_deleteModel(model);
mj_deleteVFS(&vfs);
}
TEST_F(XMLReaderTest, MaterialTextureTest) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<texture file="tiny0.png" type="2d" name="tiny0"/>
<texture file="tiny1.png" type="2d" name="tiny1"/>
<material name="material">
<layer role="occlusion" texture="tiny0"/>
<layer role="roughness" texture="tiny0"/>
<layer role="metallic" texture="tiny0"/>
<layer role="rgb" texture="tiny1"/>
</material>
</asset>
<worldbody>
<geom type="plane" material="material" size="4 4 4"/>
</worldbody>
</mujoco>
)";
mjVFS vfs;
mj_defaultVFS(&vfs);
mj_addBufferVFS(&vfs, "tiny0.png", kTinyPng, sizeof(kTinyPng));
mj_addBufferVFS(&vfs, "tiny1.png", kTinyPng, sizeof(kTinyPng));
mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml));
char error[1024];
mjModel* model = mj_loadXML("model.xml", &vfs, error, 1024);
EXPECT_THAT(model, NotNull()) << error;
EXPECT_EQ(model->mat_texid[mjTEXROLE_RGB], 1);
EXPECT_EQ(model->mat_texid[mjTEXROLE_METALLIC], 0);
EXPECT_EQ(model->mat_texid[mjTEXROLE_ROUGHNESS], 0);
EXPECT_EQ(model->mat_texid[mjTEXROLE_OCCLUSION], 0);
mj_deleteModel(model);
mj_deleteVFS(&vfs);
}
TEST_F(XMLReaderTest, LegacyMaterialTextureTest) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<texture file="tiny0.png" type="2d" name="tiny0"/>
<texture file="tiny1.png" type="2d" name="tiny1"/>
<material name="material" texture="tiny1"/>
</asset>
<worldbody>
<geom type="plane" material="material" size="4 4 4"/>
</worldbody>
</mujoco>
)";
mjVFS vfs;
mj_defaultVFS(&vfs);
mj_addBufferVFS(&vfs, "tiny0.png", kTinyPng, sizeof(kTinyPng));
mj_addBufferVFS(&vfs, "tiny1.png", kTinyPng, sizeof(kTinyPng));
mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml));
char error[1024];
mjModel* model = mj_loadXML("model.xml", &vfs, error, 1024);
EXPECT_THAT(model, NotNull()) << error;
EXPECT_EQ(model->mat_texid[mjTEXROLE_RGB], 1);
mj_deleteModel(model);
mj_deleteVFS(&vfs);
}
TEST_F(XMLReaderTest, MaterialTextureFailTest) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<texture file="tiny0.png" type="2d" name="tiny0"/>
<texture file="tiny1.png" type="2d" name="tiny1"/>
<material name="material" texture="tiny1">
<layer role="rgb" texture="tiny1"/>
<layer role="occlusion" texture="tiny0"/>
</material>
</asset>
<worldbody>
<geom type="plane" material="material" size="4 4 4"/>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, IsNull());
EXPECT_THAT(error.data(), HasSubstr("A material with a texture attribute "
"cannot have layer sub-elements"));
}
TEST_F(XMLReaderTest, LargeTextureTest) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<!--
Use a texture width that exceeds the maximum texture size. For cube
textures, the height is ignored and set to width*6. The default number of
channels is 3.
The width in this test is chosen so that 6*width*width*3 is too large to
be represented as an integer.
-->
<texture name="tex" builtin="gradient" width="10923" height="2"/>
</asset>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(model, IsNull());
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, HugeTextureTest) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<!--
Use a texture width that exceeds the maximum texture size. For cube
textures, the height is ignored and set to width*6. The default number of
channels is 3.
The width in this test is chosen so that 6*width*width*3 is so large that
it overflows and becomes a positive integer.
-->
<texture name="tex" builtin="gradient" width="15447" height="2"/>
</asset>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(model, IsNull());
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"/>
<mesh name="cube" file="cube.obj"/>
<material name="submaterial" texture="subtiny"/>
</mujoco>
)";
mjVFS vfs;
mj_defaultVFS(&vfs);
mj_addBufferVFS(&vfs, "assets/tiny.png", kTinyPng, sizeof(kTinyPng));
mj_addBufferVFS(&vfs, "assets/subassets/subtiny.png", kTinyPng,
sizeof(kTinyPng));
mj_addBufferVFS(&vfs, "assets/subassets/cube.obj", kTinyObj,
sizeof(kTinyObj));
mj_addBufferVFS(&vfs, "assets/assets.xml", assets, sizeof(assets));
mj_addBufferVFS(&vfs, "assets/subassets/assets.xml", subassets,
sizeof(subassets));
mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml));
// loading the file should be successful
std::array<char, 1024> error;
mjModel* model = mj_loadXML("model.xml", &vfs, error.data(),
error.size());
ASSERT_THAT(model, NotNull()) << error.data();
mj_deleteModel(model);
mj_deleteVFS(&vfs);
}
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>
)";
mjVFS vfs;
mj_defaultVFS(&vfs);
mj_addBufferVFS(&vfs, "assets/tiny.png", kTinyPng, sizeof(kTinyPng));
// need to fallback for backwards compatibility
mj_addBufferVFS(&vfs, "subtiny.png", kTinyPng, sizeof(kTinyPng));
mj_addBufferVFS(&vfs, "assets/assets.xml", assets, sizeof(assets));
mj_addBufferVFS(&vfs, "assets/subassets/assets.xml", subassets,
sizeof(subassets));
mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml));
// loading the file should be successful
std::array<char, 1024> error;
mjModel* model = mj_loadXML("model.xml", &vfs,
error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
mj_deleteModel(model);
mj_deleteVFS(&vfs);
}
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());
ASSERT_THAT(model, NotNull()) << error.data();
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, IncludeAbsoluteMeshDirTest) {
static constexpr char xml[] = R"(
<mujoco>
<include file="assets.xml"/>
</mujoco>
)";
static constexpr char assets[] = R"(
<mujoco>
<compiler meshdir="mjMock.IncludeAbsoluteMeshDirTest:/assets"/>
<asset>
<mesh file="cube.obj"/>
</asset>
</mujoco>
)";
static constexpr char cube[] = R"(
v -0.500000 -0.500000 0.500000
v 0.500000 -0.500000 0.500000
v -0.500000 0.500000 0.500000
v 0.500000 0.500000 0.500000
v -0.500000 0.500000 -0.500000
v 0.500000 0.500000 -0.500000
v -0.500000 -0.500000 -0.500000
v 0.500000 -0.500000 -0.500000)";
MockFilesystem fs("IncludeAbsoluteMeshDirTest");
fs.AddFile("/assets/cube.obj", (const unsigned char*) cube, sizeof(cube));
fs.AddFile("assets.xml", (const unsigned char*) assets,
sizeof(assets));
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());
ASSERT_THAT(model, NotNull()) << error.data();
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, ParsePolycoef) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<joint name="0"/>
<geom size="1"/>
</body>
<body>
<joint name="1"/>
<geom size="1"/>
</body>
</worldbody>
<equality>
<joint joint1="0" joint2="1"/>
<joint joint1="0" joint2="1" polycoef="2"/>
<joint joint1="0" joint2="1" polycoef="3 4"/>
<joint joint1="0" joint2="1" polycoef="5 6 7 8 9"/>
</equality>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, NotNull()) << error.data();
EXPECT_THAT(AsVector(m->eq_data + 0*mjNEQDATA, 5),
ElementsAre(0, 1, 0, 0, 0));
EXPECT_THAT(AsVector(m->eq_data + 1*mjNEQDATA, 5),
ElementsAre(2, 1, 0, 0, 0));
EXPECT_THAT(AsVector(m->eq_data + 2*mjNEQDATA, 5),
ElementsAre(3, 4, 0, 0, 0));
EXPECT_THAT(AsVector(m->eq_data + 3*mjNEQDATA, 5),
ElementsAre(5, 6, 7, 8, 9));
mj_deleteModel(m);
}
TEST_F(XMLReaderTest, TendonArmature) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<site name="a"/>
<body pos="1 0 0">
<joint name="slide" type="slide"/>
<geom size=".1"/>
<site name="b"/>
</body>
</worldbody>
<tendon>
<spatial armature="1.5">
<site site="a"/>
<site site="b"/>
</spatial>
<fixed armature="2.5">
<joint joint="slide" coef="1"/>
</fixed>
<fixed>
<joint joint="slide" coef="2"/>
</fixed>
</tendon>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, NotNull()) << error.data();
EXPECT_EQ(m->ntendon, 3);
EXPECT_FLOAT_EQ(m->tendon_armature[0], 1.5);
EXPECT_FLOAT_EQ(m->tendon_armature[1], 2.5);
EXPECT_FLOAT_EQ(m->tendon_armature[2], 0);
mj_deleteModel(m);
}
TEST_F(XMLReaderTest, TendonArmatureNegative) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<site name="a"/>
<site name="b" pos="1 0 0"/>
</worldbody>
<tendon>
<spatial armature="-1.5">
<site site="a"/>
<site site="b"/>
</spatial>
</tendon>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, IsNull());
EXPECT_THAT(error.data(), HasSubstr("tendon armature cannot be negative"));
}
TEST_F(XMLReaderTest, TendonArmatureGeomWrap) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<site name="a"/>
<geom name="g" type="sphere" size=".1"/>
<site name="b" pos="1 0 0"/>
</worldbody>
<tendon>
<spatial armature="1.5">
<site site="a"/>
<geom geom="g"/>
<site site="b"/>
</spatial>
</tendon>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, IsNull());
EXPECT_THAT(error.data(), HasSubstr("geom wrapping not supported"));
}
// ------------------------ test frame parsing ---------------------------------
TEST_F(XMLReaderTest, ParseFrame) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<default class="frame">
<geom size=".1"/>
</default>
<default class="body">
<geom size=".2"/>
</default>
<default class="geom">
<geom size=".3"/>
</default>
</default>
<worldbody>
<frame euler="0 0 30">
<geom size=".5" euler="0 0 20"/>
</frame>
<body>
<frame pos="0 1 0" childclass="frame">
<geom pos="0 1 0"/>
<body pos="1 0 0" childclass="body">
<geom pos="0 0 1"/>
<geom pos="0 0 1" class="geom"/>
</body>
</frame>
</body>
<frame euler="0 0 30">
<frame euler="0 0 20">
<geom size=".6"/>
</frame>
</frame>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, NotNull()) << error.data();
EXPECT_THAT(m->geom_size[ 0], .5);
EXPECT_THAT(m->geom_size[ 3], .6);
EXPECT_THAT(m->geom_size[ 6], .1);
EXPECT_THAT(m->geom_size[ 9], .2);
EXPECT_THAT(m->geom_size[12], .3);
mj_deleteModel(m);
}
TEST_F(XMLReaderTest, DuplicateFrameName) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<frame name="frame1" euler="0 0 30">
<geom size=".1"/>
</frame>
<frame name="frame1" euler="0 0 30">
<geom size=".1"/>
</frame>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, IsNull()) << error.data();
EXPECT_THAT(error.data(), HasSubstr("repeated name 'frame1'"));
}
// ---------------------- test replicate parsing -------------------------------
TEST_F(XMLReaderTest, ParseReplicate) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<texture name="texture" type="2d" builtin="checker" width="32" height="32"/>
<material name="material" texture="texture" texrepeat="1 1" texuniform="true"/>
</asset>
<worldbody>
<replicate count="101" offset="3 0 .1" euler="0 0 1.8">
<body name="body" pos="0 -1 0">
<joint type="slide"/>
<geom name="g" size="1"/>
</body>
</replicate>
<replicate count="2" offset="1 0 0">
<replicate count="2" offset="0 1 0" sep="_">
<geom name="geom" size="1" pos="0 0 1" material="material"/>
<site name="site" pos="1 0 0"/>
</replicate>
</replicate>
</worldbody>
<sensor>
<framepos name="sensor" objtype="site" objname="site"/>
</sensor>
<keyframe>
<key name="keyframe" qpos="1"/>
</keyframe>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, testing::NotNull()) << error.data();
EXPECT_THAT(m->ngeom, 105);
EXPECT_THAT(m->nsensor, 4);
EXPECT_THAT(m->nbody, 102);
// check that the separator is used correctly
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
char geom_name[mjMAXUINAME] = "";
util::strcat_arr(geom_name, m->names + m->name_geomadr[2*i+j]);
EXPECT_THAT(m->geom_pos[6*i+3*j+0], i);
EXPECT_THAT(m->geom_pos[6*i+3*j+1], j);
EXPECT_THAT(m->geom_pos[6*i+3*j+2], 1);
EXPECT_THAT(std::string(geom_name),
"geom_" + std::to_string(j) + std::to_string(i));
}
}
// check that 3 digits are used if count > 99
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 10; ++j) {
for (int k = 0; k < 10; ++k) {
int ngeom = 100*i+10*j+k;
if (ngeom > 99) break;
char geom_name[mjMAXUINAME] = "";
util::strcat_arr(geom_name, m->names + m->name_geomadr[4+ngeom]);
EXPECT_THAT(
std::string(geom_name),
"g" + std::to_string(i) + std::to_string(j) + std::to_string(k));
}
}
}
// check body positions
mjtNum pos[2] = {0, 0};
for (int i = 1; i < 102; ++i) {
mjtNum theta = (i-1) * 1.8 * mjPI / 180;
EXPECT_NEAR(m->body_pos[3*i+0], pos[0] + sin(theta), 1e-8) << i;
EXPECT_NEAR(m->body_pos[3*i+1], pos[1] - cos(theta), 1e-8) << i;
EXPECT_NEAR(m->body_pos[3*i+2], (i-1) * .1, 1e-8);
pos[0] += 3 * cos(theta);
pos[1] += 3 * sin(theta);
}
// check that the final pose is correct
int n = m->nbody-1;
EXPECT_NEAR(m->body_quat[4*n+0], 0, 1e-8);
EXPECT_EQ(m->body_quat[4*n+1], 0);
EXPECT_EQ(m->body_quat[4*n+2], 0);
EXPECT_EQ(m->body_quat[4*n+3], 1);
// check that the keyframe is resized
EXPECT_THAT(m->nkey, 102);
EXPECT_THAT(m->nq, 101);
for (int i = 0; i < m->nkey; i++) {
for (int j = 0; j < m->nq; j++) {
EXPECT_THAT(m->key_qpos[i*m->nq+j], i == j ? 1 : 0) << i << " " << j;
}
}
mj_deleteModel(m);
}
TEST_F(XMLReaderTest, ParseReplicatePartialReference) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<geom name="floor" type="plane" size="1 1 1"/>
<site name="world"/>
<replicate count="2" offset="1 0 0">
<body name="replicated">
<freejoint/>
<site name="replicated"/>
<geom name="replicated" size="1"/>
</body>
</replicate>
</worldbody>
<contact>
<pair geom1="floor" geom2="replicated" condim="1"/>
<exclude body1="world" body2="replicated" />
</contact>
<tendon>
<spatial>
<site site="world"/>
<site site="replicated"/>
</spatial>
</tendon>
<sensor>
<framepos name="sensor" objtype="site" objname="replicated" reftype="site" refname="world"/>
</sensor>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, testing::NotNull()) << error.data();
EXPECT_THAT(m->nbody, 3);
EXPECT_THAT(m->ngeom, 3);
EXPECT_THAT(m->npair, 2);
EXPECT_THAT(m->nexclude, 2);
EXPECT_THAT(m->ntendon, 2);
EXPECT_THAT(m->nsensor, 2);
mj_deleteModel(m);
}
TEST_F(XMLReaderTest, ParseReplicateDefaultPropagate) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<default class="body">
<geom type="capsule"/>
</default>
</default>
<worldbody>
<replicate count="2" euler="0 0 16.36" sep="-">
<body name="torso" pos="-5 0 1.282" childclass="body">
<body name="head" pos="0 0 .19">
<geom name="head" type="sphere" size=".09"/>
</body>
</body>
</replicate>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjSpec* spec = mj_parseXMLString(xml, 0, error.data(), error.size());
EXPECT_THAT(spec, NotNull()) << error.data();
mjsBody* torso = mjs_findBody(spec, "torso-0");
EXPECT_THAT(torso, NotNull());
mjsDefault* def = mjs_getDefault(torso->element);
EXPECT_THAT(def, NotNull());
EXPECT_THAT(def->geom->type, mjGEOM_CAPSULE);
mjsBody* head = mjs_findBody(spec, "head-0");
EXPECT_THAT(head, NotNull());
def = mjs_getDefault(head->element);
EXPECT_THAT(def, NotNull());
EXPECT_THAT(def->geom->type, mjGEOM_CAPSULE);
mj_deleteSpec(spec);
}
TEST_F(XMLReaderTest, ParseReplicateRepeatedName) {
static constexpr char xml[] = R"(
<mujoco>
<compiler>
<lengthrange mode="all"/>
</compiler>
<worldbody>
<light pos="0 0 10"/>
<replicate offset="0 .1 0" count="2">
<site name="a" size=".02"/>
<body pos="1 0 1">
<joint axis="0 -1 0" range="0 90"/>
<geom type="capsule" size=".02" fromto="0 0 0 0 0 -1"/>
<site name="b" pos="0 0 -1"/>
</body>
</replicate>
</worldbody>
<tendon>
<spatial name="b">
<site site="a"/>
<site site="b"/>
</spatial>
</tendon>
<actuator>
<position name="b" tendon="b0" ctrlrange="0 3" kp="100" dampratio="1"/>
<position name="b" tendon="b1" ctrlrange="0 3" kp="100" dampratio="1"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjSpec* spec = mj_parseXMLString(xml, 0, error.data(), error.size());
EXPECT_THAT(spec, IsNull()) << error.data();
EXPECT_THAT(error.data(), HasSubstr("repeated name 'b' in actuator"));
EXPECT_THAT(error.data(), HasSubstr("Element 'position'"));
}
TEST_F(XMLReaderTest, RepeatedPrefix) {
static constexpr char parent[] = R"(
<mujoco>
<asset>
<model name="1" file="child_1.xml" content_type="text/xml" />
<model name="2" file="child_2.xml" content_type="text/xml" />
</asset>
<worldbody>
<attach model="1" body="1" prefix="prefix-"/>
<replicate count="1">
<attach model="2" body="2" prefix="prefix-"/>
</replicate>
</worldbody>
</mujoco>
)";
static constexpr char child_1[] = R"(
<mujoco>
<asset>
<model name="2" file="child_2.xml" content_type="text/xml"/>
</asset>
<worldbody>
<body name="1">
<body name="2">
<attach model="2" body="2" prefix="prefix2"/>
</body>
</body>
</worldbody>
</mujoco>
)";
static constexpr char child_2[] = R"(
<mujoco>
<worldbody>
<body name="2"/>
</worldbody>
</mujoco>
)";
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
mj_addBufferVFS(vfs.get(), "child_1.xml", child_1, sizeof(child_1));
mj_addBufferVFS(vfs.get(), "child_2.xml", child_2, sizeof(child_2));
std::array<char, 1024> err;
mjSpec* c2 = mj_parseXMLString(child_2, 0, err.data(), err.size());
EXPECT_THAT(c2, NotNull()) << err.data();
mjSpec* c1 = mj_parseXMLString(child_1, vfs.get(), err.data(), err.size());
EXPECT_THAT(c1, NotNull()) << err.data();
mj_deleteSpec(c1);
mj_deleteSpec(c2);
mjSpec* spec = mj_parseXMLString(parent, vfs.get(), err.data(), err.size());
EXPECT_THAT(spec, IsNull());
EXPECT_THAT(err.data(), HasSubstr("mismatched parents"));
mj_deleteSpec(spec);
mj_deleteVFS(vfs.get());
}
TEST_F(XMLReaderTest, ParseReplicateExcludeTendon) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body name="winch" pos="-.01 0 .35">
<joint name="winch" damping="1"/>
<geom type="cylinder" size=".015 .01"/>
<site name="anchor" pos=".1 0 .04"/>
</body>
<site name="pulley" pos=".1 0 .32"/>
<site name="hook_left" pos=".08 0 .3"/>
<site name="hook_right" pos=".12 0 .3"/>
<body name="sphere" pos=".1 0 .2">
<freejoint/>
<geom type="sphere" size=".03"/>
<site name="pin_left" pos="-.025 0 .025"/>
<site name="pin_right" pos=".025 0 .025"/>
</body>
<replicate count="4" offset=".025 0 0">
<replicate count="4" offset="0 .025 0">
<replicate count="4" offset="0 0 .025">
<body pos=".06 -.04 .05">
<geom type="sphere" size=".012"/>
</body>
</replicate>
</replicate>
</replicate>
</worldbody>
<tendon>
<spatial range="0 .19" limited="true">
<site site="anchor"/>
<site site="pulley"/>
<pulley divisor="3"/>
<site site="pulley"/>
<site site="hook_left"/>
<site site="pin_left"/>
<pulley divisor="3"/>
<site site="pulley"/>
<site site="hook_right"/>
<site site="pin_right"/>
</spatial>
</tendon>
<actuator>
<position name="winch" joint="winch" ctrlrange="-.7 .5" ctrllimited="true" kp="10"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjSpec* spec = mj_parseXMLString(xml, 0, error.data(), error.size());
EXPECT_THAT(spec, NotNull()) << error.data();
mjModel* m = mj_compile(spec, 0);
EXPECT_THAT(m, NotNull());
EXPECT_THAT(m->nbody, 67);
EXPECT_THAT(m->ngeom, 66);
EXPECT_THAT(m->nsite, 6);
EXPECT_THAT(m->nu, 1);
EXPECT_THAT(m->ntendon, 1);
mj_deleteModel(m);
mj_deleteSpec(spec);
}
TEST_F(XMLReaderTest, ParseReplicateWithTendon) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<replicate count="2" offset=".025 0 0">
<replicate count="2" offset="0 .025 0">
<replicate count="2" offset="0 0 .025">
<body name="winch" pos="-.01 0 .35">
<joint name="winch" damping="1"/>
<geom type="cylinder" size=".015 .01"/>
<site name="anchor" pos=".1 0 .04"/>
</body>
<site name="pulley" pos=".1 0 .32"/>
<site name="hook_left" pos=".08 0 .3"/>
<site name="hook_right" pos=".12 0 .3"/>
<body name="sphere" pos=".1 0 .2">
<freejoint/>
<geom type="sphere" size=".03"/>
<site name="pin_left" pos="-.025 0 .025"/>
<site name="pin_right" pos=".025 0 .025"/>
</body>
<body pos=".06 -.04 .05">
<geom type="sphere" size=".012"/>
</body>
</replicate>
</replicate>
</replicate>
</worldbody>
<tendon>
<spatial range="0 .19" limited="true" name="tendon">
<site site="anchor"/>
<site site="pulley"/>
<pulley divisor="3"/>
<site site="pulley"/>
<site site="hook_left"/>
<site site="pin_left"/>
<pulley divisor="3"/>
<site site="pulley"/>
<site site="hook_right"/>
<site site="pin_right"/>
</spatial>
</tendon>
<actuator>
<position name="winch" joint="winch" ctrlrange="-.7 .5" ctrllimited="true" kp="10"/>
<position name="tendon" tendon="tendon" ctrlrange="0 1" kp="100" dampratio="1"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjSpec* spec = mj_parseXMLString(xml, 0, error.data(), error.size());
EXPECT_THAT(spec, NotNull()) << error.data();
mjModel* m = mj_compile(spec, 0);
EXPECT_THAT(m, NotNull()) << mjs_getError(spec);
EXPECT_THAT(m->nbody, 25);
EXPECT_THAT(m->ngeom, 24);
EXPECT_THAT(m->nsite, 48);
EXPECT_THAT(m->nu, 16);
EXPECT_THAT(m->ntendon, 8);
mj_deleteModel(m);
mj_deleteSpec(spec);
}
// ---------------------- test spec assets parsing -----------------------------
TEST_F(XMLReaderTest, ParseSpecAssets) {
std::array<char, 1024> er;
static const char* const kParentPath =
"xml/testdata/parent.xml";
static const char* const kChildPath =
"xml/testdata/child.xml";
const std::string xml_parent = GetTestDataFilePath(kParentPath);
const std::string xml_child = GetTestDataFilePath(kChildPath);
mjModel* parent = mj_loadXML(xml_parent.c_str(), 0, er.data(), er.size());
EXPECT_THAT(parent, NotNull()) << er.data();
mjModel* child = mj_loadXML(xml_child.c_str(), 0, er.data(), er.size());
EXPECT_THAT(child, NotNull()) << er.data();
mj_deleteModel(parent);
mj_deleteModel(child);
}
TEST_F(XMLReaderTest, AttachSpecAssets) {
static constexpr char xml_parent[] = R"(
<mujoco>
<asset>
<model name="child" file="xml_child.xml"/>
</asset>
<worldbody>
<body name="parent">
<geom name="geom" size="2"/>
<replicate count="2" offset="0 0 1">
<attach model="child" body="body" prefix="other"/>
</replicate>
</body>
</worldbody>
</mujoco>
)";
static constexpr char xml_child[] = R"(
<mujoco>
<worldbody>
<body name="body">
<geom name="geom" size="1" pos="2 0 0"/>
</body>
</worldbody>
</mujoco>
)";
static constexpr char xml_expected[] = R"(
<mujoco>
<worldbody>
<body name="parent">
<geom name="geom" size="2"/>
<body name="otherbody0">
<geom name="othergeom0" size="1" pos="2 0 0"/>
</body>
<body name="otherbody1" pos="0 0 1">
<geom name="othergeom1" size="1" pos="2 0 0"/>
</body>
</body>
</worldbody>
</mujoco>
)";
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
mj_addBufferVFS(vfs.get(), "xml_child.xml", xml_child, sizeof(xml_child));
std::array<char, 1024> er;
mjModel* model =
LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get());
EXPECT_THAT(model, NotNull()) << er.data();
mjModel* expected = LoadModelFromString(xml_expected, er.data(), er.size());
EXPECT_THAT(expected, NotNull()) << er.data();
mjtNum tol = 0;
std::string field = "";
EXPECT_LE(CompareModel(model, expected, field), tol)
<< "Expected and attached models are different!\n"
<< "Different field: " << field << '\n';;
mj_deleteModel(model);
mj_deleteModel(expected);
mj_deleteVFS(vfs.get());
}
TEST_F(XMLReaderTest, InvalidAttach) {
static constexpr char xml_parent[] = R"(
<mujoco>
<asset>
<model name="other" file="child.xml"/>
</asset>
<worldbody>
<body name="parent">
<joint name="joint1"/>
<geom size="2"/>
<attach model="other" body="body" prefix="_"/>
</body>
</worldbody>
<actuator>
<motor name="_actuator" joint="joint1"/>
</actuator>
</mujoco>
)";
static constexpr char xml_child[] = R"(
<mujoco model="child">
<worldbody>
<body name="body">
<joint name="joint2"/>
<geom size="1" pos="2 0 0"/>
</body>
</worldbody>
<actuator>
<motor name="actuator" joint="joint2"/>
</actuator>
</mujoco>
)";
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
mj_addBufferVFS(vfs.get(), "child.xml", xml_child, sizeof(xml_child));
std::array<char, 1024> er;
mjModel* model =
LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get());
EXPECT_THAT(model, IsNull()) << er.data();
EXPECT_THAT(er.data(), HasSubstr("repeated name '_actuator' in actuator"));
EXPECT_THAT(er.data(), HasSubstr("Element 'attach'"));
mj_deleteVFS(vfs.get());
}
TEST_F(XMLReaderTest, LookupCompilerOptionWithoutSpecCopy) {
static constexpr char child_xml[] = R"(
<mujoco>
<compiler angle="radian"/>
<worldbody>
<body name="child">
<geom type="box" size="1 1 1"/>
<joint name="child_joint" range="-3.1415926 3.1415926"/>
</body>
</worldbody>
</mujoco>
)";
static constexpr char parent_xml[] = R"(
<mujoco>
<asset>
<model name="child" file="child.xml"/>
</asset>
<worldbody>
<body name="parent">
<geom type="box" size="1 1 1"/>
<joint name="parent_joint" range="-180 180"/>
<attach model="child" body="child" prefix="child_"/>
</body>
</worldbody>
</mujoco>
)";
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
mj_addBufferVFS(vfs.get(), "child.xml", child_xml, sizeof(child_xml));
mj_addBufferVFS(vfs.get(), "parent.xml", parent_xml, sizeof(parent_xml));
std::array<char, 1024> error;
auto* spec = mj_parseXMLString(parent_xml, vfs.get(), error.data(),
error.size());
ASSERT_THAT(spec, NotNull()) << error.data();
mjModel* model = mj_compile(spec, vfs.get());
EXPECT_THAT(model, NotNull());
EXPECT_THAT(model->njnt, 2);
EXPECT_NEAR(model->jnt_range[0], -3.14159, 1e-5);
EXPECT_NEAR(model->jnt_range[1], 3.14159, 1e-5);
EXPECT_NEAR(model->jnt_range[2], -3.14159, 1e-5);
EXPECT_NEAR(model->jnt_range[3], 3.14159, 1e-5);
mjSpec* copied_spec = mj_copySpec(spec);
ASSERT_THAT(copied_spec, NotNull());
mjModel* copied_model = mj_compile(copied_spec, vfs.get());
EXPECT_THAT(copied_model, NotNull());
EXPECT_THAT(copied_model->njnt, 2);
EXPECT_NEAR(copied_model->jnt_range[0], -3.14159, 1e-5);
EXPECT_NEAR(copied_model->jnt_range[1], 3.14159, 1e-5);
EXPECT_NEAR(copied_model->jnt_range[2], -3.14159, 1e-5);
EXPECT_NEAR(copied_model->jnt_range[3], 3.14159, 1e-5);
mj_deleteSpec(spec);
mj_deleteSpec(copied_spec);
mj_deleteModel(model);
mj_deleteModel(copied_model);
mj_deleteVFS(vfs.get());
}
TEST_F(XMLReaderTest, ResizeKeyframeAfterParsing) {
static constexpr char parent_xml[] = R"(
<mujoco>
<asset>
<model name="child" file="child.xml"/>
</asset>
<worldbody>
<attach model="child" body="world" prefix="child_"/>
<body name="body">
<joint name="joint"/>
<geom size="1"/>
</body>
</worldbody>
<keyframe>
<key name="key" qpos="1"/>
</keyframe>
</mujoco>
)";
static constexpr char child_xml[] = R"(
<mujoco>
<worldbody>
<body name="body">
<joint name="joint"/>
<geom size="1"/>
</body>
</worldbody>
</mujoco>
)";
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
mj_addBufferVFS(vfs.get(), "child.xml", child_xml, sizeof(child_xml));
std::array<char, 1024> error;
mjModel* m =
LoadModelFromString(parent_xml, error.data(), error.size(), vfs.get());
EXPECT_THAT(m, NotNull()) << error.data();
mj_deleteModel(m);
mj_deleteVFS(vfs.get());
}
// ----------------------- test camera parsing ---------------------------------
TEST_F(XMLReaderTest, CameraInvalidFovyAndSensorsize) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<camera fovy="1" sensorsize="1 1" resolution="100 100"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, IsNull());
EXPECT_THAT(error.data(), HasSubstr("either 'fovy' or 'sensorsize'"));
EXPECT_THAT(error.data(), HasSubstr("line 6"));
}
TEST_F(XMLReaderTest, CameraPrincipalRequiresSensorsize) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<camera principal="1 1"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, IsNull());
EXPECT_THAT(error.data(), HasSubstr("focal/principal require sensorsize"));
EXPECT_THAT(error.data(), HasSubstr("line 6"));
}
TEST_F(XMLReaderTest, CameraSensorsizeRequiresResolution) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<camera sensorsize="1 1" resolution="0 0"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, IsNull());
EXPECT_THAT(error.data(), HasSubstr("requires positive resolution"));
EXPECT_THAT(error.data(), HasSubstr("line 6"));
}
// ----------------------- test inertia parsing --------------------------------
TEST_F(XMLReaderTest, InvalidInertialOrientation) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<inertial pos="0 0 0" mass="1" quat="1 0 0 0" fullinertia="1 1 1 0 0 0"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(
error.data(),
HasSubstr(
"fullinertia and inertial orientation cannot both be specified"));
}
TEST_F(XMLReaderTest, ReadShellParameter) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="example_mesh"
vertex="0 0 0 1 0 0 0 1 0 0 0 1"
face="0 2 1 2 0 3" inertia="shell"/>
</asset>
<worldbody>
<geom type="mesh" mesh="example_mesh"/>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
mj_deleteModel(model);
}
// ----------------------- test builtin mesh parsing ---------------------------
TEST_F(XMLReaderTest, ReadWedgeMesh) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="wedge" builtin="wedge" params="25 25 180 90 0"/>
</asset>
<worldbody>
<geom type="mesh" mesh="wedge"/>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, BuiltinAndFile) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="wedge" builtin="wedge" vertex="0 0 0 1 0 0 0 1 0 0 1 0"
params="25 25 180 90 0"/>
</asset>
<worldbody>
<geom type="mesh" mesh="wedge"/>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(),
HasSubstr("builtin mesh cannot be used with user vertex data"));
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, MakePlateNoParameters) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="plate" builtin="plate"/>
</asset>
<worldbody>
<geom type="mesh" mesh="plate" contype="0" conaffinity="0"/>
</worldbody>
</mujoco>)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("required attribute missing: 'params'"));
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, MakePlateTooFewParameters) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="plate" builtin="plate" params="1"/>
</asset>
<worldbody>
<geom type="mesh" mesh="plate" contype="0" conaffinity="0"/>
</worldbody>
</mujoco>)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(
error.data(),
HasSubstr("Plate builtin mesh type requires 2 parameters"));
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, MakePlateInvalidParameters) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="plate" builtin="plate" params="-1 -1"/>
</asset>
<worldbody>
<geom type="mesh" mesh="plate" contype="0" conaffinity="0"/>
</worldbody>
</mujoco>)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("resolutions must be positive"));
mj_deleteModel(model);
}
// ----------------------- test skin parsing --------------------------------
TEST_F(XMLReaderTest, ReadsSkinGroups) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body pos="1 -1 .6" name="B0_parent">
<flexcomp name="B0" type="grid" count="4 4 1" spacing=".2 .2 .2" group="2" radius=".1" dim="2">
<edge equality="true"/>
<contact internal="false" selfcollide="none"/>
</flexcomp>
</body>
<body pos="-1 1 .6" name="B1_parent">
<flexcomp name="B1" type="grid" count="4 4 1" spacing=".2 .2 .2" group="4" radius=".1" dim="2">
<edge equality="true"/>
<contact internal="false" selfcollide="none"/>
</flexcomp>
</body>
</worldbody>
<deformable>
<skin name="B0Skin" group="4" rgba="1 1 0 1" inflate="0.1"
vertex="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
face="0 4 5 0 5 1 1 5 6 1 6 2 2 6 7 2 7 3 4 8 9 4 9 5 5 9 10 5 10 6 6 10 11 6 11 7 8
12 13 8 13 9 9 13 14 9 14 10 10 14 15 10 15 11 16 21 20 16 17 21 17 22 21 17 18
22 18 23 22 18 19 23 20 25 24 20 21 25 21 26 25 21 22 26 22 27 26 22 23 27 24 29
28 24 25 29 25 30 29 25 26 30 26 31 30 26 27 31 0 20 4 0 16 20 4 24 8 4 20 24 8
28 12 8 24 28 3 7 23 3 23 19 7 11 27 7 27 23 11 15 31 11 31 27 0 1 17 0 17 16 1
2 18 1 18 17 2 3 19 2 19 18 12 29 13 12 28 29 13 30 14 13 29 30 14 31 15 14 30 31">
<bone body="B0_0" bindpos="0 0 0" bindquat="1 0 0 0" vertid="0 16" vertweight="1 1"/>
<bone body="B0_1" bindpos="0 0 0" bindquat="1 0 0 0" vertid="1 17" vertweight="1 1"/>
<bone body="B0_2" bindpos="0 0 0" bindquat="1 0 0 0" vertid="2 18" vertweight="1 1"/>
<bone body="B0_3" bindpos="0 0 0" bindquat="1 0 0 0" vertid="3 19" vertweight="1 1"/>
<bone body="B0_4" bindpos="0 0 0" bindquat="1 0 0 0" vertid="4 20" vertweight="1 1"/>
<bone body="B0_5" bindpos="0 0 0" bindquat="1 0 0 0" vertid="5 21" vertweight="1 1"/>
<bone body="B0_6" bindpos="0 0 0" bindquat="1 0 0 0" vertid="6 22" vertweight="1 1"/>
<bone body="B0_7" bindpos="0 0 0" bindquat="1 0 0 0" vertid="7 23" vertweight="1 1"/>
<bone body="B0_8" bindpos="0 0 0" bindquat="1 0 0 0" vertid="8 24" vertweight="1 1"/>
<bone body="B0_9" bindpos="0 0 0" bindquat="1 0 0 0" vertid="9 25" vertweight="1 1"/>
<bone body="B0_10" bindpos="0 0 0" bindquat="1 0 0 0" vertid="10 26" vertweight="1 1"/>
<bone body="B0_11" bindpos="0 0 0" bindquat="1 0 0 0" vertid="11 27" vertweight="1 1"/>
<bone body="B0_12" bindpos="0 0 0" bindquat="1 0 0 0" vertid="12 28" vertweight="1 1"/>
<bone body="B0_13" bindpos="0 0 0" bindquat="1 0 0 0" vertid="13 29" vertweight="1 1"/>
<bone body="B0_14" bindpos="0 0 0" bindquat="1 0 0 0" vertid="14 30" vertweight="1 1"/>
<bone body="B0_15" bindpos="0 0 0" bindquat="1 0 0 0" vertid="15 31" vertweight="1 1"/>
</skin>
<skin name="B1Skin" group="2" rgba="0 1 1 1" inflate="0.1"
vertex="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
face="0 4 5 0 5 1 1 5 6 1 6 2 2 6 7 2 7 3 4 8 9 4 9 5 5 9 10 5 10 6 6 10 11 6 11 7 8
12 13 8 13 9 9 13 14 9 14 10 10 14 15 10 15 11 16 21 20 16 17 21 17 22 21 17 18
22 18 23 22 18 19 23 20 25 24 20 21 25 21 26 25 21 22 26 22 27 26 22 23 27 24 29
28 24 25 29 25 30 29 25 26 30 26 31 30 26 27 31 0 20 4 0 16 20 4 24 8 4 20 24 8
28 12 8 24 28 3 7 23 3 23 19 7 11 27 7 27 23 11 15 31 11 31 27 0 1 17 0 17 16 1
2 18 1 18 17 2 3 19 2 19 18 12 29 13 12 28 29 13 30 14 13 29 30 14 31 15 14 30 31">
<bone body="B1_0" bindpos="0 0 0" bindquat="1 0 0 0" vertid="0 16" vertweight="1 1"/>
<bone body="B1_1" bindpos="0 0 0" bindquat="1 0 0 0" vertid="1 17" vertweight="1 1"/>
<bone body="B1_2" bindpos="0 0 0" bindquat="1 0 0 0" vertid="2 18" vertweight="1 1"/>
<bone body="B1_3" bindpos="0 0 0" bindquat="1 0 0 0" vertid="3 19" vertweight="1 1"/>
<bone body="B1_4" bindpos="0 0 0" bindquat="1 0 0 0" vertid="4 20" vertweight="1 1"/>
<bone body="B1_5" bindpos="0 0 0" bindquat="1 0 0 0" vertid="5 21" vertweight="1 1"/>
<bone body="B1_6" bindpos="0 0 0" bindquat="1 0 0 0" vertid="6 22" vertweight="1 1"/>
<bone body="B1_7" bindpos="0 0 0" bindquat="1 0 0 0" vertid="7 23" vertweight="1 1"/>
<bone body="B1_8" bindpos="0 0 0" bindquat="1 0 0 0" vertid="8 24" vertweight="1 1"/>
<bone body="B1_9" bindpos="0 0 0" bindquat="1 0 0 0" vertid="9 25" vertweight="1 1"/>
<bone body="B1_10" bindpos="0 0 0" bindquat="1 0 0 0" vertid="10 26" vertweight="1 1"/>
<bone body="B1_11" bindpos="0 0 0" bindquat="1 0 0 0" vertid="11 27" vertweight="1 1"/>
<bone body="B1_12" bindpos="0 0 0" bindquat="1 0 0 0" vertid="12 28" vertweight="1 1"/>
<bone body="B1_13" bindpos="0 0 0" bindquat="1 0 0 0" vertid="13 29" vertweight="1 1"/>
<bone body="B1_14" bindpos="0 0 0" bindquat="1 0 0 0" vertid="14 30" vertweight="1 1"/>
<bone body="B1_15" bindpos="0 0 0" bindquat="1 0 0 0" vertid="15 31" vertweight="1 1"/>
</skin>
</deformable>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
int flexid1 = mj_name2id(model, mjOBJ_FLEX, "B0");
int flexid2 = mj_name2id(model, mjOBJ_FLEX, "B1");
EXPECT_THAT(model->flex_group[flexid1], 2);
EXPECT_THAT(model->skin_group[0], 4);
EXPECT_THAT(model->flex_group[flexid2], 4);
EXPECT_THAT(model->skin_group[1], 2);
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, InvalidSkinGroup) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<composite prefix="B0" type="grid" count="6 6 1">
<geom size=".1"/>
<skin group="6"/>
</composite>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(model, IsNull());
EXPECT_THAT(
error.data(),
HasSubstr("skin group must be between 0 and 5\nElement 'skin', line 7"));
}
TEST_F(XMLReaderTest, Orthographic) {
static constexpr char xml[] = R"(
<mujoco>
<visual>
<global orthographic="true" fovy="5"/>
<map znear="0.01"/>
</visual>
<default>
<camera projection="orthographic"/>
</default>
<worldbody>
<camera name="fovy=1" pos=".5 .5 2" projection="orthographic" fovy="1"/>
<camera name="fovy=2" pos="0 0 2" fovy="2"/>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->vis.global.orthographic, 1);
EXPECT_EQ(model->cam_projection[0], mjPROJ_ORTHOGRAPHIC);
EXPECT_EQ(model->cam_projection[1], mjPROJ_ORTHOGRAPHIC);
EXPECT_EQ(model->cam_fovy[0], 1);
EXPECT_EQ(model->cam_fovy[1], 2);
mj_deleteModel(model);
}
// ------------- test height-field parsing -------------------------------------
using HfieldParsingTest = MujocoTest;
TEST_F(HfieldParsingTest, NoData) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<hfield name="hf" nrow="4" ncol="3" size="0.5 0.5 1 0.1"/>
</asset>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->hfield_nrow[0], 4);
EXPECT_EQ(model->hfield_ncol[0], 3);
EXPECT_EQ(model->hfield_size[0], 0.5);
EXPECT_EQ(model->hfield_size[1], 0.5);
EXPECT_EQ(model->hfield_size[2], 1);
EXPECT_EQ(model->hfield_size[3], 0.1);
mj_deleteModel(model);
}
TEST_F(HfieldParsingTest, HasDataBadSize) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<hfield name="hf" nrow="3" ncol="2" size="0.5 0.5 1 0.1"
elevation="1 2
3 4
5 6 7"/>
</asset>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("data length must match nrow*ncol"));
EXPECT_THAT(error.data(), HasSubstr("line 4"));
}
TEST_F(HfieldParsingTest, HasData) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<hfield name="hf" nrow="3" ncol="2" size="0.5 0.5 1 0.1"
elevation="1 2
3 4
5 6"/>
</asset>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->hfield_nrow[0], 3);
EXPECT_EQ(model->hfield_ncol[0], 2);
EXPECT_EQ(model->hfield_size[0], 0.5);
EXPECT_EQ(model->hfield_size[1], 0.5);
EXPECT_EQ(model->hfield_size[2], 1);
EXPECT_EQ(model->hfield_size[3], 0.1);
// offset (minimum) and scaling (maximum) from normalizing operation
float offset = 1.0;
float scale = 6.0 - offset;
// compare data, note: reverse row order
EXPECT_THAT(model->hfield_data[0], FloatEq((5-offset)/scale));
EXPECT_THAT(model->hfield_data[1], FloatEq((6-offset)/scale));
EXPECT_THAT(model->hfield_data[2], FloatEq((3-offset)/scale));
EXPECT_THAT(model->hfield_data[3], FloatEq((4-offset)/scale));
EXPECT_THAT(model->hfield_data[4], FloatEq((1-offset)/scale));
EXPECT_THAT(model->hfield_data[5], FloatEq((2-offset)/scale));
mj_deleteModel(model);
}
// ------------- test relative frame sensor parsing ----------------------------
using RelativeFrameSensorParsingTest = MujocoTest;
TEST_F(RelativeFrameSensorParsingTest, RefNameButNoType) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<site name="reference"/>
<body name="sensorized"/>
</worldbody>
<sensor>
<framepos objname="sensorized" objtype="body" refname="reference"/>
</sensor>
</mujoco>
)";
std::array<char, 1024> error;
LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(error.data(), HasSubstr("but reftype is missing"));
EXPECT_THAT(error.data(), HasSubstr("line 8"));
}
TEST_F(RelativeFrameSensorParsingTest, RefTypeButNoName) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<site name="reference"/>
<body name="sensorized"/>
</worldbody>
<sensor>
<framepos objname="sensorized" objtype="body" reftype="site"/>
</sensor>
</mujoco>
)";
std::array<char, 1024> error;
LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(error.data(), HasSubstr("attribute missing: 'refname'"));
EXPECT_THAT(error.data(), HasSubstr("line 8"));
}
// ------------- test actlimited parsing ---------------------------------------
using ActuatorTest = MujocoTest;
TEST_F(ActuatorTest, InvalidActlimited) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<motor joint="hinge" actlimited="invalid" actrange="-1 1"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("unrecognized attribute"));
EXPECT_THAT(error.data(), HasSubstr("line 10"));
}
TEST_F(ActuatorTest, IncompleteActlimited) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<general joint="hinge" actlimited="true" dyntype="filter" actrange="-1"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(),
HasSubstr("attribute 'actrange' does not have enough data"));
}
TEST_F(ActuatorTest, ReadsByte) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<general joint="hinge" dyntype="filter" actlimited="true" actrange="-1 1"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
EXPECT_EQ(*(model->actuator_actlimited), (mjtByte)(1 & 0xFF));
mj_deleteModel(model);
}
// ---------------- test actuator parsing --------------------------------------
using ActuatorParseTest = MujocoTest;
TEST_F(ActuatorParseTest, PositionTimeconst) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<joint name="jnt"/>
</body>
</worldbody>
<actuator>
<position joint="jnt" timeconst="2"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
ASSERT_NEAR(model->actuator_dynprm[0], 2.0, 1e-6);
EXPECT_THAT(model->actuator_dyntype[0], Eq(mjDYN_FILTEREXACT));
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, PositionTimeconstInheritrange) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<joint name="jnt" range="-1 1"/>
</body>
</worldbody>
<actuator>
<position joint="jnt" inheritrange="1" timeconst="2"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, PositionTimeconstDefault) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<position timeconst="1"/>
</default>
<worldbody>
<body>
<geom size="1"/>
<joint name="jnt"/>
</body>
</worldbody>
<actuator>
<position joint="jnt"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
ASSERT_NEAR(model->actuator_dynprm[0], 1.0, 1e-6);
EXPECT_THAT(model->actuator_dyntype[0], Eq(mjDYN_FILTEREXACT));
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, PositionTimeconstDefaultOverride) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<position timeconst="1"/>
</default>
<worldbody>
<body>
<geom size="1"/>
<joint name="jnt"/>
</body>
</worldbody>
<actuator>
<position joint="jnt" timeconst="0"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
EXPECT_FALSE(model->actuator_dynprm[0]);
EXPECT_THAT(model->actuator_dyntype[0], Eq(mjDYN_NONE));
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, ReadsDamper) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<joint name="jnt" type="slide" axis="1 0 0" range="-10 10"/>
</body>
</worldbody>
<actuator>
<damper joint="jnt" ctrlrange="0 1"/>
<general joint="jnt" ctrllimited="true" ctrlrange="0 1" gaintype="affine" biastype="none"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
EXPECT_THAT(model->actuator_gaintype[0], Eq(mjGAIN_AFFINE));
EXPECT_THAT(model->actuator_gaintype[1], Eq(mjGAIN_AFFINE));
EXPECT_THAT(model->actuator_biastype[0], Eq(mjBIAS_NONE));
EXPECT_THAT(model->actuator_biastype[1], Eq(mjBIAS_NONE));
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, DamperRequiresPositiveDamping) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<joint name="jnt" type="slide" axis="1 0 0" range="-10 10"/>
</body>
</worldbody>
<actuator>
<damper joint="jnt" kv="-1"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(),
HasSubstr("damping coefficient cannot be negative"));
}
TEST_F(ActuatorParseTest, DamperRequiresControlRange) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<joint name="jnt" type="slide" axis="1 0 0" range="-10 10" limited="true"/>
</body>
</worldbody>
<actuator>
<damper joint="jnt" kv="1"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("invalid control range"));
EXPECT_THAT(error.data(), HasSubstr("line 10"));
}
TEST_F(ActuatorParseTest, DamperPositiveControlRange) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<joint name="jnt" type="slide" axis="1 0 0" range="-10 10"/>
</body>
</worldbody>
<actuator>
<damper joint="jnt" kv="1" ctrlrange="-1 0"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("control range cannot be negative"));
EXPECT_THAT(error.data(), HasSubstr("line 10"));
}
TEST_F(ActuatorParseTest, ReadsPositionIntvelKv) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<joint name="jnt" type="slide" axis="1 0 0"/>
</body>
</worldbody>
<actuator>
<position joint="jnt" kv="2"/>
<intvelocity joint="jnt" actrange="-1 1" kv="3"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
EXPECT_THAT(model->actuator_biasprm[0*mjNBIAS + 2], Eq(-2.0));
EXPECT_THAT(model->actuator_biasprm[1*mjNBIAS + 2], Eq(-3.0));
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, RequirePositiveKv) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<joint name="jnt" type="slide" axis="1 0 0"/>
</body>
</worldbody>
<actuator>
<position joint="jnt" kv="-2"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("kv cannot be negative"));
EXPECT_THAT(error.data(), HasSubstr("line 10"));
}
TEST_F(ActuatorParseTest, PositionIntvelocityVelocityDefaultsPropagate) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<default class="position">
<position kp="3" kv="4" inheritrange="2"/>
</default>
<default class="intvelocity">
<intvelocity kp="5" kv="6" inheritrange="0.5"/>
</default>
<default class="velocity">
<velocity kv="7"/>
</default>
</default>
<worldbody>
<body>
<geom size="1"/>
<joint name="jnt" type="slide" axis="1 0 0" range="0 2"/>
</body>
</worldbody>
<actuator>
<position joint="jnt" class="position"/>
<intvelocity joint="jnt" class="intvelocity"/>
<velocity joint="jnt" class="velocity"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->actuator_gainprm[0*mjNGAIN + 0], 3.0);
EXPECT_EQ(model->actuator_biasprm[0*mjNBIAS + 1], -3.0);
EXPECT_EQ(model->actuator_biasprm[0*mjNBIAS + 2], -4.0);
EXPECT_EQ(model->actuator_gainprm[1*mjNGAIN + 0], 5.0);
EXPECT_EQ(model->actuator_biasprm[1*mjNBIAS + 1], -5.0);
EXPECT_EQ(model->actuator_biasprm[1*mjNBIAS + 2], -6.0);
EXPECT_EQ(model->actuator_gainprm[2*mjNGAIN + 0], 7.0);
EXPECT_EQ(model->actuator_biasprm[2*mjNBIAS + 1], 0.0);
EXPECT_EQ(model->actuator_biasprm[2*mjNBIAS + 2], -7.0);
for (int i = 0; i < model->nu; i++) {
for (int j = 3; j < mjNBIAS; j++) {
EXPECT_EQ(model->actuator_biasprm[i*mjNBIAS + j], 0.0);
}
for (int j = 3; j < mjNGAIN; j++) {
EXPECT_EQ(model->actuator_gainprm[i*mjNGAIN + j], 0.0);
}
}
EXPECT_EQ(model->actuator_ctrlrange[0*2 + 0], -1.0);
EXPECT_EQ(model->actuator_ctrlrange[0*2 + 1], 3.0);
EXPECT_EQ(model->actuator_actrange[1*2 + 0], 0.5);
EXPECT_EQ(model->actuator_actrange[1*2 + 1], 1.5);
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, IntvelocityCheckEquivalence) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<intvelocity joint="hinge" kp="2.5" actrange="-1.57 1.57"/>
<general joint="hinge" actlimited="true" actrange="-1.57 1.57"
dyntype="integrator" biastype="affine" gainprm="2.5" biasprm="0 -2.5 0"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
// same actlimited
EXPECT_EQ(model->actuator_actlimited[0], 1);
EXPECT_EQ(model->actuator_actlimited[1], 1);
// same dyntype
EXPECT_EQ(model->actuator_dyntype[0], mjDYN_INTEGRATOR);
EXPECT_EQ(model->actuator_dyntype[1], mjDYN_INTEGRATOR);
// same biastype
EXPECT_EQ(model->actuator_biastype[0], mjBIAS_AFFINE);
EXPECT_EQ(model->actuator_biastype[1], mjBIAS_AFFINE);
// same gaintype
EXPECT_EQ(model->actuator_gaintype[0], mjGAIN_FIXED);
EXPECT_EQ(model->actuator_gaintype[1], mjGAIN_FIXED);
// same gainprm
EXPECT_DOUBLE_EQ(model->actuator_gainprm[0], 2.5);
EXPECT_DOUBLE_EQ(model->actuator_gainprm[mjNGAIN], 2.5);
// same biasprm
EXPECT_DOUBLE_EQ(model->actuator_biasprm[0], 0.0);
EXPECT_DOUBLE_EQ(model->actuator_biasprm[1], -2.5);
EXPECT_DOUBLE_EQ(model->actuator_biasprm[2], 0.0);
EXPECT_DOUBLE_EQ(model->actuator_biasprm[mjNBIAS], 0.0);
EXPECT_DOUBLE_EQ(model->actuator_biasprm[mjNBIAS + 1], -2.5);
EXPECT_DOUBLE_EQ(model->actuator_biasprm[mjNBIAS + 2], 0.0);
// same actrange
EXPECT_DOUBLE_EQ(model->actuator_actrange[0 + 0], -1.57);
EXPECT_DOUBLE_EQ(model->actuator_actrange[0 + 1], 1.57);
EXPECT_DOUBLE_EQ(model->actuator_actrange[0 + 2], -1.57);
EXPECT_DOUBLE_EQ(model->actuator_actrange[0 + 3], 1.57);
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, IntvelocityCheckDefaultsIfNotSpecified) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<intvelocity joint="hinge" actrange="-1 1"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
// check that by default kp = 1
EXPECT_DOUBLE_EQ(model->actuator_gainprm[0], 1.0);
// check that biasprm is (0, -1, 0)
EXPECT_DOUBLE_EQ(model->actuator_biasprm[0], 0.0);
EXPECT_DOUBLE_EQ(model->actuator_biasprm[1], -1.0);
EXPECT_DOUBLE_EQ(model->actuator_biasprm[2], 0.0);
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, IntvelocityNoActrangeThrowsError) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<intvelocity joint="hinge"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("invalid actrange for actuator"));
EXPECT_THAT(error.data(), HasSubstr("line 10"));
}
TEST_F(ActuatorParseTest, IntvelocityDefaultsPropagate) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<intvelocity kp="5"/>
<default class="withactrange">
<intvelocity kp="1" actrange="-1 1"/>
</default>
</default>
<worldbody>
<body>
<joint name="hinge1"/>
<joint name="hinge2"/>
<geom type="box" size=".025 .025 .025"/>
</body>
</worldbody>
<actuator>
<intvelocity joint="hinge1" actrange="0 1"/>
<intvelocity joint="hinge2" class="withactrange"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_DOUBLE_EQ(model->actuator_gainprm[0], 5);
EXPECT_DOUBLE_EQ(model->actuator_gainprm[mjNGAIN], 1);
EXPECT_DOUBLE_EQ(model->actuator_actrange[0 + 0], 0);
EXPECT_DOUBLE_EQ(model->actuator_actrange[0 + 1], 1);
EXPECT_DOUBLE_EQ(model->actuator_actrange[0 + 2], -1);
EXPECT_DOUBLE_EQ(model->actuator_actrange[0 + 3], 1);
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, AdhesionDefaultsPropagate) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<adhesion ctrlrange="0 3"/>
</default>
<worldbody>
<body name="sphere">
<geom size="1"/>
</body>
</worldbody>
<actuator>
<adhesion body="sphere"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
EXPECT_EQ(model->actuator_ctrlrange[0], 0);
EXPECT_EQ(model->actuator_ctrlrange[1], 3);
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, ErrorBadAdhesionDefaults) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<adhesion ctrlrange="-1 0"/>
</default>
<worldbody>
<body name="sphere">
<geom size="1"/>
</body>
</worldbody>
<actuator>
<adhesion body="sphere"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(),
HasSubstr("adhesion control range cannot be negative"));
}
// make sure range requirement is not enforced at parse time
TEST_F(ActuatorParseTest, DampersDontRequireRange) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<damper ctrlrange="0 2"/>
</default>
<worldbody>
<body name="sphere">
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<damper joint="hinge"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
EXPECT_EQ(model->actuator_ctrlrange[0], 0);
EXPECT_EQ(model->actuator_ctrlrange[1], 2);
mj_deleteModel(model);
}
// adhesion actuators inherit from general defaults
TEST_F(ActuatorParseTest, AdhesionInheritsFromGeneral) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<general dyntype="filter" dynprm="123" gainprm="5"/>
<adhesion ctrlrange="0 2"/>
</default>
<worldbody>
<body name="sphere">
<geom name="sphere" size="1"/>
</body>
</worldbody>
<actuator>
<adhesion name="adhere" body="sphere"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
// expect that gainprm was inherited from the general default
EXPECT_EQ(model->actuator_gainprm[0], 5);
// expect that dynprm was inherited from the general default
EXPECT_EQ(model->actuator_dynprm[0], 123);
// expect that dyntype was inherited from the general default
EXPECT_EQ(model->actuator_dyntype[0], mjDYN_FILTER);
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, ActdimDefaultsPropagate) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<general actdim="2"/>
</default>
<worldbody>
<body>
<geom size="1"/>
<joint name="hinge"/>
</body>
</worldbody>
<actuator>
<general joint="hinge" dyntype="user"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
// expect that actdim was inherited from the general default
EXPECT_EQ(model->actuator_actnum[0], 2);
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, MusclesParseSmoothdyn) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<joint name="hinge" limited="true" range="-1 1"/>
</body>
</worldbody>
<actuator>
<muscle joint="hinge"/>
<muscle joint="hinge" tausmooth="0.4"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->actuator_dynprm[2], 0.0);
EXPECT_EQ(model->actuator_dynprm[mjNDYN + 2], 0.4);
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, MusclesSmoothdynNegative) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<joint name="hinge" limited="true" range="-1 1"/>
</body>
</worldbody>
<actuator>
<muscle joint="hinge" tausmooth="-0.4"/>
</actuator>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("muscle tausmooth cannot be negative"));
EXPECT_THAT(error.data(), HasSubstr("line 10"));
}
TEST_F(ActuatorParseTest, GroupDisable) {
static constexpr char xml[] = R"(
<mujoco>
<option actuatorgroupdisable="0 3 8 3"/>
<!-- note: repeated numbers are okay -->
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull());
EXPECT_EQ(model->opt.disableactuator, (1<<0) + (1<<3) + (1<<8));
mj_deleteModel(model);
}
TEST_F(ActuatorParseTest, GroupDisableNegative) {
static constexpr char xml[] = R"(
<mujoco>
<option actuatorgroupdisable="0 -3 5"/>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("must be non-negative"));
EXPECT_THAT(error.data(), HasSubstr("line 3"));
}
TEST_F(ActuatorParseTest, GroupDisableTooBig) {
static constexpr char xml[] = R"(
<mujoco>
<option actuatorgroupdisable="0 31"/>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("cannot exceed 30"));
EXPECT_THAT(error.data(), HasSubstr("line 3"));
}
// ------------- test sensor parsing -------------------------------------------
using SensorParseTest = MujocoTest;
TEST_F(SensorParseTest, UserObjTypeNoName) {
static constexpr char xml[] = R"(
<mujoco>
<sensor>
<user dim="1" needstage="vel" objtype="site"/>
</sensor>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("objtype 'site' given but"));
EXPECT_THAT(error.data(), HasSubstr("line 4"));
}
TEST_F(SensorParseTest, UserObjNameNoType) {
static constexpr char xml[] = R"(
<mujoco>
<sensor>
<user dim="1" needstage="vel" objname="kevin"/>
</sensor>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("objname 'kevin' given but"));
EXPECT_THAT(error.data(), HasSubstr("line 4"));
}
TEST_F(SensorParseTest, UserNeedstageAcc) {
static constexpr char xml[] = R"(
<mujoco>
<sensor>
<user dim="1" needstage="vel"/>
<user dim="1"/>
<user dim="1" needstage="pos"/>
</sensor>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->sensor_needstage[0], mjSTAGE_VEL);
EXPECT_EQ(model->sensor_needstage[1], mjSTAGE_ACC);
EXPECT_EQ(model->sensor_needstage[2], mjSTAGE_POS);
mj_deleteModel(model);
}
// ------------- test general parsing ------------------------------------------
TEST_F(XMLReaderTest, ZnearZeroNotAllowed) {
static constexpr char xml[] = R"(
<mujoco>
<visual>
<map znear="0"/>
</visual>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("znear must be strictly positive"));
EXPECT_THAT(error.data(), HasSubstr("line 4"));
}
TEST_F(XMLReaderTest, ZnearNegativeNotAllowed) {
static constexpr char xml[] = R"(
<mujoco>
<visual>
<map znear="-1"/>
</visual>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("znear must be strictly positive"));
EXPECT_THAT(error.data(), HasSubstr("line 4"));
}
TEST_F(XMLReaderTest, ExtentZeroNotAllowed) {
static constexpr char xml[] = R"(
<mujoco>
<statistic extent="0"/>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("extent must be strictly positive"));
EXPECT_THAT(error.data(), HasSubstr("line 3"));
}
TEST_F(XMLReaderTest, ExtentNegativeNotAllowed) {
static constexpr char xml[] = R"(
<mujoco>
<statistic extent="-1"/>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, IsNull());
EXPECT_THAT(error.data(), HasSubstr("extent must be strictly positive"));
EXPECT_THAT(error.data(), HasSubstr("line 3"));
}
TEST_F(XMLReaderTest, LightRadius) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<default class="r1">
<light bulbradius="1"/>
</default>
</default>
<worldbody>
<light/>
<light class="r1"/>
<light class="r1" bulbradius="2"/>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_FLOAT_EQ(model->light_bulbradius[0], 0.02);
EXPECT_FLOAT_EQ(model->light_bulbradius[1], 1);
EXPECT_FLOAT_EQ(model->light_bulbradius[2], 2);
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, CameraOutput) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<camera name="default_cam"/>
<camera name="single_cam" output="depth"/>
<camera name="multi_cam" output="rgb normal segmentation"/>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->ncam, 3);
EXPECT_EQ(model->cam_output[0], mjCAMOUT_RGB); // default
EXPECT_EQ(model->cam_output[1], mjCAMOUT_DEPTH);
EXPECT_EQ(model->cam_output[2],
mjCAMOUT_RGB | mjCAMOUT_NORMAL | mjCAMOUT_SEG);
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, CameraOutputDefault) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<default class="multi">
<camera output="depth distance"/>
</default>
</default>
<worldbody>
<camera name="default_cam"/>
<camera name="class_cam" class="multi"/>
<camera name="override_cam" class="multi" output="normal"/>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
EXPECT_EQ(model->ncam, 3);
EXPECT_EQ(model->cam_output[0], mjCAMOUT_RGB); // main default
EXPECT_EQ(model->cam_output[1], mjCAMOUT_DEPTH | mjCAMOUT_DIST);
EXPECT_EQ(model->cam_output[2], mjCAMOUT_NORMAL);
mj_deleteModel(model);
}
} // namespace
} // namespace mujoco