// 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 #include #include #include #include #include #include #include #include "src/cc/array_safety.h" #include "src/engine/engine_util_errmem.h" #include "test/fixture.h" namespace mujoco { namespace { using ::std::string; using ::testing::Eq; using ::testing::HasSubstr; using ::testing::IsNan; using ::testing::IsNull; using ::testing::NotNull; using XMLReaderTest = MujocoTest; TEST_F(XMLReaderTest, MemorySize) { std::array error; { static constexpr char xml[] = R"( )"; 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"( )"; 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"( )"; 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"( )"; 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"( )"; 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"( )"; 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 error; { static constexpr char xml[] = R"( )"; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); } { static constexpr char xml[] = R"( )"; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); } { static constexpr char xml[] = R"( )"; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); } { static constexpr char xml[] = R"( )"; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); } } TEST_F(XMLReaderTest, InvalidNUserBody) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("nuser_body")); } TEST_F(XMLReaderTest, InvalidNUserJoint) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("nuser_jnt")); } TEST_F(XMLReaderTest, InvalidNUserGeom) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("nuser_geom")); } TEST_F(XMLReaderTest, InvalidNUserSite) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("nuser_site")); } TEST_F(XMLReaderTest, InvalidNUserCamera) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("nuser_cam")); } TEST_F(XMLReaderTest, InvalidNUserTendon) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("nuser_tendon")); } TEST_F(XMLReaderTest, InvalidNUserActuator) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("nuser_actuator")); } TEST_F(XMLReaderTest, InvalidNUserSensor) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("nuser_sensor")); } TEST_F(XMLReaderTest, CanParseInf) { static constexpr char xml[] = R"( )"; const double inf = std::numeric_limits::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"( )"; std::array 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"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("problem reading attribute 'axisangle'")); } TEST_F(XMLReaderTest, InvalidArrayLength) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("has too much data")); } TEST_F(XMLReaderTest, InvalidQuaternion) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("zero quaternion is not allowed")); } TEST_F(XMLReaderTest, InvalidNumber) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("problem reading attribute")); } TEST_F(XMLReaderTest, AllowsSpaces) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, NotNull()); mj_deleteModel(model); } TEST_F(XMLReaderTest, InvalidDoubleOrientation) { std::string prefix = "<"; std::string suffix = "/>"; std::vector 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 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 error; mjModel* model = LoadModelFromString(xml.c_str(), error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT( error.data(), HasSubstr("multiple orientation specifiers for the same field")); } } } } // ---------------------- test camera parsing --------------------------------- TEST_F(XMLReaderTest, CameraInvalidFovyAndSensorsize) { static constexpr char xml[] = R"( )"; std::array error; mjModel* m = LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(m, testing::IsNull()); EXPECT_THAT(error.data(), HasSubstr("either 'fovy' or 'sensorsize'")); } TEST_F(XMLReaderTest, CameraPricipalRequiresSensorsize) { static constexpr char xml[] = R"( )"; std::array error; mjModel* m = LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(m, testing::IsNull()); EXPECT_THAT(error.data(), HasSubstr("attribute missing: 'sensorsize'")); } TEST_F(XMLReaderTest, CameraSensorsizeRequiresResolution) { static constexpr char xml[] = R"( )"; std::array error; mjModel* m = LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(m, testing::IsNull()); EXPECT_THAT(error.data(), HasSubstr("attribute missing: 'resolution'")); } // ---------------------- test inertia parsing -------------------------------- TEST_F(XMLReaderTest, InvalidInertialOrientation) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("multiple orientation specifiers for the same field")); } TEST_F(XMLReaderTest, ReadShellParameter) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, NotNull()); mj_deleteModel(model); } TEST_F(XMLReaderTest, ReadsDamper) { static constexpr char xml[] = R"( )"; std::array 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(XMLReaderTest, RequiresPoisitiveDamping) { static constexpr char xml[] = R"( )"; std::array 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(XMLReaderTest, RequiresControlRange) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("invalid control range")); } TEST_F(XMLReaderTest, PositiveControlRange) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("control range cannot be negative")); } TEST_F(XMLReaderTest, ReadsSkinGroups) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, NotNull()); int geomid1 = mj_name2id(model, mjOBJ_GEOM, "B0G0_0_0"); int geomid2 = mj_name2id(model, mjOBJ_GEOM, "B1G0_0_0"); EXPECT_THAT(model->geom_group[geomid1], 2); EXPECT_THAT(model->skin_group[0], 4); EXPECT_THAT(model->geom_group[geomid2], 4); EXPECT_THAT(model->skin_group[1], 2); mj_deleteModel(model); } TEST_F(XMLReaderTest, InvalidSkinGroup) { static constexpr char xml[] = R"( )"; std::array 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")); mj_deleteModel(model); } // ------------- test relative frame sensor parsing ---------------------------- using RelativeFrameSensorParsingTest = MujocoTest; TEST_F(RelativeFrameSensorParsingTest, RefNameButNoType) { static constexpr char xml[] = R"( )"; std::array error; LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(error.data(), HasSubstr("but reftype is missing")); } TEST_F(RelativeFrameSensorParsingTest, RefTypeButNoName) { static constexpr char xml[] = R"( )"; std::array error; LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(error.data(), HasSubstr("attribute missing: 'refname'")); } // ------------- test actlimited parsing --------------------------------------- using ActuatorTest = MujocoTest; TEST_F(ActuatorTest, InvalidActlimited) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("unrecognized attribute")); } TEST_F(ActuatorTest, IncompleteActlimited) { static constexpr char xml[] = R"( )"; std::array 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"( )"; std::array 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, IntvelocityCheckEquivalence) { static constexpr char xml[] = R"( )"; std::array 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"( )"; std::array 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"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("invalid activation range for actuator")); } TEST_F(ActuatorParseTest, IntvelocityDefaultsPropagate) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, NotNull()); 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"( )"; std::array 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"( )"; std::array 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"( )"; std::array 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"( )"; std::array 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"( )"; std::array 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"( )"; std::array 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"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("muscle tausmooth cannot be negative")); } TEST_F(ActuatorParseTest, GroupDisable) { static constexpr char xml[] = R"( )"; std::array 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"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("must be non-negative")); } TEST_F(ActuatorParseTest, GroupDisableTooBig) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("cannot exceed 30")); } // ------------- test sensor parsing ------------------------------------------- using SensorParseTest = MujocoTest; TEST_F(SensorParseTest, UserObjTypeNoName) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("objtype 'site' given but")); } TEST_F(SensorParseTest, UserObjNameNoType) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("objname 'kevin' given but")); } TEST_F(SensorParseTest, UserNeedstageAcc) { static constexpr char xml[] = R"( )"; std::array 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"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("znear must be strictly positive")); } TEST_F(XMLReaderTest, ZnearNegativeNotAllowed) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("znear must be strictly positive")); } TEST_F(XMLReaderTest, ExtentZeroNotAllowed) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("extent must be strictly positive")); } TEST_F(XMLReaderTest, ExtentNegativeNotAllowed) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, IsNull()); EXPECT_THAT(error.data(), HasSubstr("extent must be strictly positive")); } } // namespace } // namespace mujoco