Files
Mujoco_WASM/test/xml/xml_native_writer_test.cc
T
Google DeepMind 4f0293c6cb Added support for msh2.2
Now we can load flex with format 2.2 binary and ASCII.
There are two different parsing strategies. One for gmsh app and the other is for ftetwild.

Used the shark model from Scanned Objects by Google Research:
https://app.gazebosim.org/GoogleResearch/fuel/models/Weisshai_Great_White_Shark

PiperOrigin-RevId: 619229499
Change-Id: I6eb876afcbc8a00631914b093c45bde10c0caaf6
2024-03-26 10:12:13 -07:00

1333 lines
37 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_writer.cc.
#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))
#include <unistd.h>
#endif
#include <array>
#include <clocale>
#include <cstdio>
#include <filesystem>
#include <string>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <absl/strings/match.h>
#include <mujoco/mjmodel.h>
#include <mujoco/mjtnum.h>
#include <mujoco/mujoco.h>
#include "src/cc/array_safety.h"
#include "src/xml/xml_numeric_format.h"
#include "test/fixture.h"
namespace mujoco {
namespace {
using ::testing::HasSubstr;
using ::testing::Not;
using ::testing::NotNull;
using ::testing::FloatEq;
using XMLWriterTest = PluginTest;
TEST_F(XMLWriterTest, EmptyModel) {
static constexpr char xml[] = "<mujoco/>";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("default")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, SavesMemory) {
{
static constexpr char xml[] = R"(
<mujoco>
<size memory=" 1023 "/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("memory=\"1023\""));
mj_deleteModel(model);
}
{
static constexpr char xml[] = R"(
<mujoco>
<size memory="1024"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("memory=\"1K\""));
mj_deleteModel(model);
}
{
static constexpr char xml[] = R"(
<mujoco>
<size memory="4096"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("memory=\"4K\""));
mj_deleteModel(model);
}
{
static constexpr char xml[] = R"(
<mujoco>
<size memory="1048576"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("memory=\"1M\""));
mj_deleteModel(model);
}
{
static constexpr char xml[] = R"(
<mujoco>
<size memory="1047552"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("memory=\"1023K\""));
mj_deleteModel(model);
}
}
TEST_F(XMLWriterTest, SavesDisableSensor) {
static constexpr char xml[] = R"(
<mujoco>
<option>
<flag sensor="disable"/>
</option>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("sensor=\"disable\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, EmptyUserSensor) {
static constexpr char xml[] = R"(
<mujoco>
<sensor>
<user dim="2" needstage="vel"/>
</sensor>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("dim=\"2\""));
EXPECT_THAT(saved_xml, HasSubstr("needstage=\"vel\""));
EXPECT_THAT(saved_xml, HasSubstr("datatype=\"real\""));
EXPECT_THAT(saved_xml, Not(HasSubstr("objtype")));
EXPECT_THAT(saved_xml, Not(HasSubstr("objname")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsEmptyClasses) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<default class="empty_referenced"/>
<default class="empty_unreferenced"/>
<default class="regular">
<geom size="0.3"/>
</default>
</default>
<worldbody>
<geom class="regular"/>
<geom class="empty_referenced" size="0.2"/>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("default class=\"regular\""));
EXPECT_THAT(saved_xml, HasSubstr("default class=\"empty_referenced\""));
EXPECT_THAT(saved_xml, HasSubstr("default class=\"empty_unreferenced\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsExplicitInertial) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="0.2"/>
<inertial pos="0 1 2" mass="3"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("<inertial pos=\"0 1 2\" mass=\"3\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, NotAddsInertial) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="0.2"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("inertial")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsBoundMassInertia) {
static constexpr char xml[] = R"(
<mujoco>
<compiler boundmass="0.1" boundinertia="0.2"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("boundmass=\"0.1\""));
EXPECT_THAT(saved_xml, HasSubstr("boundinertia=\"0.2\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, DropsZeroBoundMassInertia) {
static constexpr char xml[] = R"(
<mujoco>
<compiler boundmass="0" boundinertia="0"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("boundmass")));
EXPECT_THAT(saved_xml, Not(HasSubstr("boundinertia")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, DropsInertialIfFromGeom) {
static constexpr char xml[] = R"(
<mujoco>
<compiler inertiafromgeom="true"/>
<worldbody>
<body>
<inertial pos="0 1 2" mass="3"/>
<geom size="0.2"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("inertial")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsAutoLimitsFalse) {
static constexpr char xml[] = R"(
<mujoco>
<compiler autolimits="false"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("autolimits=\"false\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, DoesNotKeepInferredJointLimited) {
static constexpr char xml[] = R"(
<mujoco>
<compiler angle="radian" autolimits="true" />
<worldbody>
<body>
<joint name="hinge" range="-1 1"/>
<geom size="1"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("range=\"-1 1\""));
EXPECT_THAT(saved_xml, Not(HasSubstr("limited=\"true\"")));
EXPECT_THAT(saved_xml, Not(HasSubstr("autolimits=\"true\"")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsExplicitJointLimited) {
static constexpr char xml[] = R"(
<mujoco>
<compiler angle="radian" autolimits="true" />
<worldbody>
<body>
<joint name="hinge" limited="true" range="-1 1"/>
<geom size="1"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("autolimits=\"true\"")));
EXPECT_THAT(saved_xml, HasSubstr("range=\"-1 1\""));
EXPECT_THAT(saved_xml, HasSubstr("limited=\"true\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsJointLimitedFalseIfAutoLimits) {
static constexpr char xml[] = R"(
<mujoco>
<compiler angle="radian" autolimits="true" />
<worldbody>
<body>
<joint name="hinge" limited="false" range="-1 1"/>
<geom size="1"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("limited=\"false\" range=\"-1 1\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, DoesNotKeepInferredTendonLimited) {
static constexpr char xml[] = R"(
<mujoco>
<compiler angle="radian" autolimits="true" />
<worldbody>
<body>
<joint type="slide"/>
<geom size="1"/>
<site name="s1"/>
</body>
<site name="s2"/>
</worldbody>
<tendon>
<spatial range="-1 1">
<site site="s1"/>
<site site="s2"/>
</spatial>
</tendon>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("autolimits=\"true\"")));
EXPECT_THAT(saved_xml, HasSubstr("range=\"-1 1\""));
EXPECT_THAT(saved_xml, Not(HasSubstr("limited=\"true\"")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsExplicitTendonLimitedIfAutoLimits) {
static constexpr char xml[] = R"(
<mujoco>
<compiler angle="radian" autolimits="true" />
<worldbody>
<body>
<joint type="slide"/>
<geom size="1"/>
<site name="s1"/>
</body>
<site name="s2"/>
</worldbody>
<tendon>
<spatial limited="true" range="-1 1">
<site site="s1"/>
<site site="s2"/>
</spatial>
</tendon>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("autolimits=\"true\"")));
EXPECT_THAT(saved_xml, HasSubstr("range=\"-1 1\""));
EXPECT_THAT(saved_xml, HasSubstr("limited=\"true\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsTendonLimitedFalseIfAutoLimits) {
static constexpr char xml[] = R"(
<mujoco>
<compiler angle="radian" autolimits="true" />
<worldbody>
<body>
<joint type="slide"/>
<geom size="1"/>
<site name="s1"/>
</body>
<site name="s2"/>
</worldbody>
<tendon>
<spatial limited="false" range="-1 1">
<site site="s1"/>
<site site="s2"/>
</spatial>
</tendon>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("limited=\"false\" range=\"-1 1\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, DoesNotKeepInferredActlimited) {
static constexpr char xml[] = R"(
<mujoco>
<compiler autolimits="true" />
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<general dyntype="filter" joint="hinge" actrange="-1 1"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("autolimits=\"true\"")));
EXPECT_THAT(saved_xml, HasSubstr("actrange=\"-1 1\""));
EXPECT_THAT(saved_xml, Not(HasSubstr("actlimited=\"true\"")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsExplicitActlimitedIfAutoLimits) {
static constexpr char xml[] = R"(
<mujoco>
<compiler autolimits="true" />
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<general dyntype="filter" joint="hinge" actlimited="true" actrange="-1 1"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("autolimits=\"true\"")));
EXPECT_THAT(saved_xml, HasSubstr("actrange=\"-1 1\""));
EXPECT_THAT(saved_xml, HasSubstr("actlimited=\"true\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsActlimitedFalse) {
static constexpr char xml[] = R"(
<mujoco>
<compiler autolimits="true" />
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<general dyntype="filter" joint="hinge" actlimited="false" actrange="-1 1"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("actlimited=\"false\" actrange=\"-1 1\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, DoesNotKeepInferredCtrllimited) {
static constexpr char xml[] = R"(
<mujoco>
<compiler autolimits="true" />
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<general joint="hinge" ctrlrange="-1 1"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("ctrlrange=\"-1 1\""));
EXPECT_THAT(saved_xml, Not(HasSubstr("ctrllimited=\"true\"")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsExplicitCtrllimitedIfAutoLimits) {
static constexpr char xml[] = R"(
<mujoco>
<compiler autolimits="true" />
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<general joint="hinge" ctrllimited="true" ctrlrange="-1 1"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("ctrlrange=\"-1 1\""));
EXPECT_THAT(saved_xml, HasSubstr("ctrllimited=\"true\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsCtrllimitedFalse) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<general joint="hinge" ctrllimited="false" ctrlrange="-1 1"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("ctrllimited=\"false\" ctrlrange=\"-1 1\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, DoesNotKeepInferredForcelimited) {
static constexpr char xml[] = R"(
<mujoco>
<compiler autolimits="true" />
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<general joint="hinge" forcerange="-1 1"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("autolimits=\"true\"")));
EXPECT_THAT(saved_xml, HasSubstr("forcerange=\"-1 1\""));
EXPECT_THAT(saved_xml, Not(HasSubstr("forcelimited=\"true\"")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsExplicitForcelimited) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<general joint="hinge" forcelimited="true" forcerange="-1 1"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("autolimits=\"true\"")));
EXPECT_THAT(saved_xml, HasSubstr("forcerange=\"-1 1\""));
EXPECT_THAT(saved_xml, HasSubstr("forcelimited=\"true\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, KeepsForcelimitedFalse) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<joint name="hinge"/>
<geom size="1"/>
</body>
</worldbody>
<actuator>
<general joint="hinge" forcelimited="false" forcerange="-1 1"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml,
HasSubstr("forcelimited=\"false\" forcerange=\"-1 1\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, WritesGravComp) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body gravcomp=".25"/>
<body/>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("gravcomp=\"0.25\""));
EXPECT_THAT(saved_xml, Not(HasSubstr("gravcomp=\"0\"")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, WritesBodyPos) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body pos="1 2 3"/>
<body pos="0 0 0"/>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("pos=\"1 2 3\""));
EXPECT_THAT(saved_xml, Not(HasSubstr("pos=\"0 0 0\"")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, UndefinedMassDensity) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom type="box" size=".05 .05 .05"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("density")));
EXPECT_THAT(saved_xml, Not(HasSubstr("mass")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, WritesDefaults) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<geom density="100"/>
</default>
<worldbody>
<body>
<geom type="box" size=".05 .05 .05"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("mass")));
EXPECT_THAT(saved_xml, HasSubstr("<geom density=\"100\"/>"));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, WritesActuatorDefaults) {
static constexpr char xml[] = R"(
<mujoco>
<default>
<default class="position">
<position kp="3" kv="4" />
</default>
<default class="intvelocity">
<intvelocity kp="5" kv="6" />
</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" actrange="-1 1"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("mass")));
EXPECT_THAT(saved_xml, HasSubstr(
"<general biastype=\"affine\" gainprm=\"3\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, WritesFrameDefaults) {
static constexpr char xml[] = R"(
<mujoco>
<default class="dframe">
<geom size=".1"/>
</default>
<worldbody>
<frame name="f1" euler="0 0 30">
<geom size=".5" euler="0 0 20"/>
</frame>
<body>
<frame pos="0 1 0" name="f2" childclass="dframe">
<geom pos="0 1 0"/>
<body pos="1 0 0">
<geom pos="0 0 1"/>
</body>
</frame>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(model, NotNull()) << error.data();
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("frame name=\"f1\""));
EXPECT_THAT(saved_xml, HasSubstr("frame name=\"f2\" childclass=\"dframe\""));
EXPECT_THAT(saved_xml, Not(HasSubstr("<frame>")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, WritesDensity) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom type="box" size=".05 .05 .05" density="100"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("density=\"100\""));
EXPECT_THAT(saved_xml, Not(HasSubstr("mass")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, WritesMass) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom type="box" size=".05 .05 .05" mass="0.1"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("density")));
EXPECT_THAT(saved_xml, HasSubstr("mass=\"0.1\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, ZeroMass) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom type="box" size=".05 .05 .05" mass="0"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("density")));
EXPECT_THAT(saved_xml, HasSubstr("mass=\"0\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, OverwritesDensity) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="0.2" density="100" mass="100"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("density")));
EXPECT_THAT(saved_xml, HasSubstr("mass=\"100\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, SaveDefaultMass) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="example"
vertex="0 0 0 1 0 0 0 1 0 0 0 1"
face="2 0 3 0 1 3 1 2 3 0 2 1" />
</asset>
<default class="main">
<geom type="mesh" mass="1"/>
</default>
<worldbody>
<body>
<geom mesh="example" size=".1 .2 .3"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
EXPECT_THAT(model, NotNull());
std::string content = SaveAndReadXml(model);
EXPECT_THAT(content, HasSubstr("mass=\"1\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, UsesTwoSpaces) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr(" "));
EXPECT_THAT(saved_xml, Not(HasSubstr(" ")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, WritesSkin) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<composite type="grid" count="2 2 1" spacing="0.05">
<skin texcoord="true"/>
<geom size=".01"/>
</composite>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
EXPECT_THAT(model->nskin, 1);
mjModel* mtemp = LoadModelFromString(SaveAndReadXml(model));
ASSERT_THAT(mtemp, NotNull());
EXPECT_THAT(mtemp->nskin, 1);
mj_deleteModel(model);
mj_deleteModel(mtemp);
}
TEST_F(XMLWriterTest, WritesHfield) {
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>
)";
// load model
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
int size = model->hfield_nrow[0]*model->hfield_ncol[0];
EXPECT_EQ(size, 6);
// save and read, compare data
mjModel* mtemp = LoadModelFromString(SaveAndReadXml(model));
ASSERT_THAT(mtemp, NotNull());
for (int i = 0; i < size; ++i) {
EXPECT_THAT(mtemp->hfield_data[i], FloatEq(model->hfield_data[i]));
}
mj_deleteModel(mtemp);
// modify data, save read and compare
model->hfield_data[0] = 0.25;
model->hfield_data[1] = 0.0;
model->hfield_data[2] = 0.5;
model->hfield_data[3] = 0.0;
model->hfield_data[4] = 1.0;
model->hfield_data[5] = 0.0;
mtemp = LoadModelFromString(SaveAndReadXml(model));
ASSERT_THAT(mtemp, NotNull());
for (int i = 0; i < size; ++i) {
EXPECT_THAT(mtemp->hfield_data[i], FloatEq(model->hfield_data[i]));
}
mj_deleteModel(mtemp);
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, SpringlengthOneValue) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<site name="1"/>
<site name="2" pos="0 0 1"/>
</worldbody>
<tendon>
<spatial springlength="0.5">
<site site="1"/>
<site site="2"/>
</spatial>
</tendon>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("springlength=\"0.5\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, SpringlengthTwoValues) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<site name="1"/>
<site name="2" pos="0 0 1"/>
</worldbody>
<tendon>
<spatial springlength="0 0.5">
<site site="1"/>
<site site="2"/>
</spatial>
</tendon>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("springlength=\"0 0.5\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, Actdim) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1"/>
<joint name="hinge"/>
</body>
</worldbody>
<actuator>
<general joint="hinge" dyntype="user" actdim="2"/>
<general joint="hinge" dyntype="filter" dynprm="1"/>
<motor joint="hinge"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("actdim=\"2\""));
EXPECT_THAT(saved_xml, Not(HasSubstr("actdim=\"1\"")));
EXPECT_THAT(saved_xml, Not(HasSubstr("actdim=\"0\"")));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, ActdimDefaults) {
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>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("actdim=\"2\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, TrimsDefaults) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="1" friction="0.9" solref="0.1" solimp="0.1 0.2"/>
<joint name="jnt" type="slide" axis="1 0 0" range="0 2"/>
</body>
</worldbody>
<actuator>
<position joint="jnt" kp="3" kv="4"/>
<damper joint="jnt" kv="10" ctrlrange="0 1"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("friction=\"0.9\""));
EXPECT_THAT(saved_xml, HasSubstr("solref=\"0.1\""));
EXPECT_THAT(saved_xml, HasSubstr("solimp=\"0.1 0.2\""));
EXPECT_THAT(saved_xml, HasSubstr("gainprm=\"3\" biasprm=\"0 -3 -4\""));
EXPECT_THAT(saved_xml, HasSubstr("gainprm=\"0 0 -10\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, InheritrangeSavedAsRange) {
static constexpr char xml[] = R"(
<mujoco>
<compiler angle="radian"/>
<worldbody>
<body>
<geom size="1"/>
<joint name="slide" type="slide" range="0 2"/>
</body>
<body>
<geom size="1"/>
<joint name="hinge" type="hinge" range="-2 0"/>
</body>
</worldbody>
<actuator>
<position joint="slide" inheritrange="2"/>
<intvelocity joint="hinge" inheritrange="0.5"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("ctrlrange=\"-1 3\""));
EXPECT_THAT(saved_xml, HasSubstr("actrange=\"-1.5 -0.5\""));
mj_deleteModel(model);
}
TEST_F(XMLWriterTest, InheritedInheritrangeSavedAsRange) {
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>
<worldbody>
<body>
<geom size="1"/>
<joint name="jnt" type="slide" range="0 2"/>
</body>
</worldbody>
<actuator>
<position joint="jnt" class="position"/>
<intvelocity joint="jnt" class="intvelocity"/>
</actuator>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("inheritrange")));
EXPECT_THAT(saved_xml, HasSubstr("ctrlrange=\"-1 3\""));
EXPECT_THAT(saved_xml, HasSubstr("actrange=\"0.5 1.5\""));
mj_deleteModel(model);
}
// check that no precision is lost when saving XMLs with FullFloatPrecision
TEST_F(XMLWriterTest, SetPrecision) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<geom type="box" size="0.1 0.123456 0.1234567812345678"/>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
// save to XML and re-load, expect to lose precision
mjModel* model_lo = LoadModelFromString(SaveAndReadXml(model));
ASSERT_THAT(model_lo, NotNull());
EXPECT_EQ(model->geom_size[1], model_lo->geom_size[1]);
EXPECT_NE(model->geom_size[2], model_lo->geom_size[2]);
{
// save to XML and re-load with FullFloatPrecision
// expect to maintain precision
FullFloatPrecision increase_precision;
mjModel* model_hi = LoadModelFromString(SaveAndReadXml(model));
EXPECT_EQ(model->geom_size[2], model_hi->geom_size[2]);
mj_deleteModel(model_hi);
}
mj_deleteModel(model_lo);
mj_deleteModel(model);
}
class XMLWriterLocaleTest : public MujocoTest {
public:
XMLWriterLocaleTest() : old_locale_(std::setlocale(LC_ALL, nullptr)) {}
protected:
void SetUp() override {
if (!std::setlocale(LC_ALL, "de_DE.UTF-8")) {
GTEST_SKIP() << "This system doesn't support the de_DE.UTF-8 locale";
}
}
void TearDown() override {
std::setlocale(LC_ALL, old_locale_.c_str());
}
private:
std::string old_locale_;
};
TEST_F(XMLWriterLocaleTest, IgnoresLocale) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<geom type="box" size="0.1 1.23 2.345"/>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("0.1 1.23 2.345"));
mj_deleteModel(model);
// Test that MuJoCo doesn't override locales for subsequent calls.
char formatted[8];
std::snprintf(formatted, sizeof(formatted), "%.4f", 3.9375);
EXPECT_EQ(std::string(formatted), "3,9375");
}
// ------------------- test loading and saving multiple files ------------------
TEST_F(XMLWriterTest, WriteReadCompare) {
// full precision float printing
FullFloatPrecision increase_precision;
// loop over all xml files in data
std::vector<std::string> paths = {GetTestDataFilePath("."),
GetModelPath(".")};
std::string ext(".xml");
for (auto const& path : paths) {
for (auto &p : std::filesystem::recursive_directory_iterator(path)) {
if (p.path().extension() == ext) {
std::string xml = p.path().string();
// if file is meant to fail, skip it
if (absl::StrContains(p.path().string(), "malformed_") ||
absl::StrContains(p.path().string(), "touch_grid") ||
absl::StrContains(p.path().string(), "gmsh_") ||
absl::StrContains(p.path().string(), "shark_") ||
absl::StrContains(p.path().string(), "cow")) {
continue;
}
// load model
std::array<char, 1000> error;
mjModel* m = mj_loadXML(
xml.c_str(), nullptr, error.data(), error.size());
ASSERT_THAT(m, NotNull())
<< "Failed to load " << xml.c_str() << ": " << error.data();
// make data
mjData* d = mj_makeData(m);
ASSERT_THAT(d, testing::NotNull()) << "Failed to create data\n";
// save and load back
mjModel* mtemp =
LoadModelFromString(SaveAndReadXml(m), error.data(), error.size());
if (!mtemp) {
// if failing because assets are missing, accept the test
ASSERT_THAT(error.data(), HasSubstr("file"))
<< error.data() << " from " << xml.c_str();
} else {
// for a particularly difficult example, relax the tolerance
mjtNum tol =
absl::StrContains(p.path().string(), "belt.xml") ? 1e-13 : 0;
// compare and delete
std::string field = "";
mjtNum result = CompareModel(m, mtemp, field);
EXPECT_LE(result, tol)
<< "Loaded and saved models are different!\n"
<< "Affected file " << p.path().string() << '\n'
<< "Different field: " << field << '\n';
mj_deleteModel(mtemp);
}
// check for stack memory leak
mj_step(m, d);
EXPECT_EQ(d->pstack, 0) << "mjData stack memory leak detected in " <<
p.path().string() << '\n';
// delete original structures
mj_deleteData(d);
mj_deleteModel(m);
}
}
}
}
// ---------------- test CopyBack functionality (decompiler) ------------------
using DecompilerTest = MujocoTest;
TEST_F(DecompilerTest, SavesStatitics) {
static constexpr char xml[] = R"(
<mujoco>
<statistic meansize="2" extent="3" center="4 5 6" meanmass="7" meaninertia="8"/>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
model->stat.meansize = 7;
model->stat.extent = 8;
model->stat.center[0] = 9;
model->stat.center[1] = 10;
model->stat.center[2] = 11;
model->stat.meanmass = 12;
model->stat.meaninertia = 13;
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, HasSubstr("meansize=\"7\""));
EXPECT_THAT(saved_xml, HasSubstr("extent=\"8\""));
EXPECT_THAT(saved_xml, HasSubstr("center=\"9 10 11\""));
EXPECT_THAT(saved_xml, HasSubstr("meanmass=\"12\""));
EXPECT_THAT(saved_xml, HasSubstr("meaninertia=\"13\""));
mj_deleteModel(model);
}
TEST_F(DecompilerTest, DoesntSaveInferredStatitics) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom size="0.2"/>
</body>
</worldbody>
</mujoco>
)";
mjModel* model = LoadModelFromString(xml);
ASSERT_THAT(model, NotNull());
std::string saved_xml = SaveAndReadXml(model);
EXPECT_THAT(saved_xml, Not(HasSubstr("meansize")));
EXPECT_THAT(saved_xml, Not(HasSubstr("meanmass")));
EXPECT_THAT(saved_xml, Not(HasSubstr("meaninertia")));
EXPECT_THAT(saved_xml, Not(HasSubstr("center")));
EXPECT_THAT(saved_xml, Not(HasSubstr("extent")));
EXPECT_THAT(saved_xml, Not(HasSubstr("statistic")));
mj_deleteModel(model);
}
TEST_F(DecompilerTest, VeryLargeNumbers) {
static constexpr char xml[] = R"(
<mujoco>
<compiler angle="radian"/>
<worldbody>
<camera focal="16777217 1" sensorsize="1 1" resolution="100 100"/>
<body pos="1e+20 0 0">
<geom size="1"/>
<joint axis="1 0 0" range="-1e+10 1e+10"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << error.data();
std::string saved_xml = SaveAndReadXml(model);
// note, focal is float and loses precision 16777217 -> 16777216
EXPECT_THAT(saved_xml, HasSubstr("focal=\"16777216 1\""));
EXPECT_THAT(saved_xml, HasSubstr("pos=\"1e+20 0 0\""));
EXPECT_THAT(saved_xml, HasSubstr("range=\"-1e+10 1e+10\""));
mj_deleteModel(model);
}
} // namespace
} // namespace mujoco