// 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 #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 XMLWriterTest = MujocoTest; TEST_F(XMLWriterTest, SavesMemory) { { static constexpr char xml[] = R"( )"; 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"( )"; 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"( )"; 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"( )"; 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"( )"; 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"( )"; 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"( )"; 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"( )"; 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"( )"; mjModel* model = LoadModelFromString(xml); std::string saved_xml = SaveAndReadXml(model); EXPECT_THAT(saved_xml, HasSubstr(" )"; 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, DropsInertialIfFromGeom) { static constexpr char xml[] = R"( )"; 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, DoesNotKeepInferredJointLimited) { static constexpr char xml[] = R"( )"; 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\""))); mj_deleteModel(model); } TEST_F(XMLWriterTest, DoesNotKeepExplicitJointLimitedIfAutoLimits) { static constexpr char xml[] = R"( )"; mjModel* model = LoadModelFromString(xml); ASSERT_THAT(model, NotNull()); std::string saved_xml = SaveAndReadXml(model); EXPECT_THAT(saved_xml, 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, KeepsJointLimitedFalseIfAutoLimits) { static constexpr char xml[] = R"( )"; 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"( )"; mjModel* model = LoadModelFromString(xml); ASSERT_THAT(model, NotNull()); std::string saved_xml = SaveAndReadXml(model); EXPECT_THAT(saved_xml, 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, DoesNotKeepExplicitTendonLimitedIfAutoLimits) { static constexpr char xml[] = R"( )"; mjModel* model = LoadModelFromString(xml); ASSERT_THAT(model, NotNull()); std::string saved_xml = SaveAndReadXml(model); EXPECT_THAT(saved_xml, 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, KeepsTendonLimitedFalseIfAutoLimits) { static constexpr char xml[] = R"( )"; 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"( )"; mjModel* model = LoadModelFromString(xml); ASSERT_THAT(model, NotNull()); std::string saved_xml = SaveAndReadXml(model); EXPECT_THAT(saved_xml, 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, DoesNotKeepExplicitActlimitedIfAutoLimits) { static constexpr char xml[] = R"( )"; mjModel* model = LoadModelFromString(xml); ASSERT_THAT(model, NotNull()); std::string saved_xml = SaveAndReadXml(model); EXPECT_THAT(saved_xml, 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, KeepsActlimitedFalse) { static constexpr char xml[] = R"( )"; 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"( )"; 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, DoesNotKeepExplicitCtrllimitedIfAutoLimits) { static constexpr char xml[] = R"( )"; 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, KeepsCtrllimitedFalse) { static constexpr char xml[] = R"( )"; 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"( )"; mjModel* model = LoadModelFromString(xml); ASSERT_THAT(model, NotNull()); std::string saved_xml = SaveAndReadXml(model); EXPECT_THAT(saved_xml, 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, DoesNotKeepExplicitForcelimited) { static constexpr char xml[] = R"( )"; mjModel* model = LoadModelFromString(xml); ASSERT_THAT(model, NotNull()); std::string saved_xml = SaveAndReadXml(model); EXPECT_THAT(saved_xml, 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, KeepsForcelimitedFalse) { static constexpr char xml[] = R"( )"; 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"( )"; mjModel* model = LoadModelFromString(xml); ASSERT_THAT(model, NotNull()); std::string saved_xml = SaveAndReadXml(model); EXPECT_THAT(saved_xml, HasSubstr("gravcomp=\"0.25\"")); mj_deleteModel(model); } TEST_F(XMLWriterTest, UndefinedMassDensity) { static constexpr char xml[] = R"( )"; 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"( )"; 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("")); mj_deleteModel(model); } TEST_F(XMLWriterTest, WritesDensity) { static constexpr char xml[] = R"( )"; 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"( )"; 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"( )"; 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"( )"; 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"( )"; 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"( )"; 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"( )"; 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, SpringlengthOneValue) { static constexpr char xml[] = R"( )"; 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"( )"; 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"( )"; 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, ActdimDefaults) { static constexpr char xml[] = R"( )"; mjModel* model = LoadModelFromString(xml); ASSERT_THAT(model, NotNull()); std::string saved_xml = SaveAndReadXml(model); EXPECT_THAT(saved_xml, HasSubstr("actdim=\"2\"")); mj_deleteModel(model); } // check that no precision is lost when saving XMLs with FullFloatPrecision TEST_F(XMLWriterTest, SetPrecision) { static constexpr char xml[] = R"( )"; 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 { protected: char* old_locale; void SetUp() override { this->old_locale = std::setlocale(LC_ALL, nullptr); 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); } }; TEST_F(XMLWriterLocaleTest, IgnoresLocale) { static constexpr char xml[] = R"( )"; 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 ------------------ namespace mju = ::mujoco::util; static constexpr int kFieldSize = 500; // The maximum spacing between a normalised floating point number x and an // adjacent normalised number is 2 epsilon |x|; a factor 10 is added accounting // for losses during non-idempotent operations such as vector normalizations. template T Compare(T val1, T val2) { T error; if (mju_abs(val1) <= 1 || mju_abs(val2) <= 1) { // Asbolute precision for small numbers error = mju_abs(val1-val2); } else { // Relative precision for larger numbers T magnitude = mju_max(mju_abs(val1), mju_abs(val2)); error = mju_abs(val1/magnitude - val2/magnitude) / magnitude; } return error < 2*10*std::numeric_limits::epsilon() ? 0 : error; } mjtNum CompareModel(const mjModel* m1, const mjModel* m2, char (&field)[kFieldSize]) { mjtNum dif, maxdif = 0.0; // define symbols corresponding to number of columns // (needed in MJMODEL_POINTERS) MJMODEL_POINTERS_PREAMBLE(m1); // compare ints #define X(name) \ if (m1->name != m2->name) {maxdif = 1.0; mju::strcpy_arr(field, #name);} MJMODEL_INTS #undef X if (maxdif > 0) return maxdif; // compare arrays #define X(type, name, nr, nc) \ for (int r=0; r < m1->nr; r++) \ for (int c=0; c < nc; c++) { \ dif = Compare(m1->name[r*nc+c], m2->name[r*nc+c]); \ if (dif > maxdif) { maxdif = dif; mju::strcpy_arr(field, #name);} } MJMODEL_POINTERS #undef X // compare scalars in mjOption #define X(type, name) \ dif = Compare(m1->opt.name, m2->opt.name); \ if (dif > maxdif) {maxdif = dif; mju::strcpy_arr(field, #name);} MJOPTION_SCALARS #undef X // compare arrays in mjOption #define X(name, n) \ for (int c=0; c < n; c++) { \ dif = Compare(m1->opt.name[c], m2->opt.name[c]); \ if (dif > maxdif) {maxdif = dif; mju::strcpy_arr(field, #name);} } MJOPTION_VECTORS #undef X // Return largest difference and field name return maxdif; } TEST_F(XMLWriterTest, WriteReadCompare) { FullFloatPrecision increase_precision; // Loop over all xml files in data std::vector 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(), "plugin")) { continue; } // load model std::array 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(); } else { // compare and delete char field[kFieldSize] = ""; mjtNum result = CompareModel(m, mtemp, field); EXPECT_LE(result, 0) << "Loaded and saved models are different!\n" << "Affected file " << p.path().string() << '\n' << "Different field: " << field << '\n'; mj_deleteModel(mtemp); } // delete original structures mj_deleteData(d); mj_deleteModel(m); } } } } } // namespace } // namespace mujoco