// 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 user/user_objects.cc. #include #include #include #include #include #include #include #include #include #include #include #include #include "test/fixture.h" namespace mujoco { namespace { std::vector AsVector(const mjtNum* array, int n) { return std::vector(array, array + n); } using ::testing::ElementsAre; using ::testing::HasSubstr; using ::testing::IsNull; using ::testing::NotNull; // -------------------- test OS filesystem fallback ---------------------------- using VfsTest = MujocoTest; TEST_F(VfsTest, HFieldPngWithVFS) { static constexpr char xml[] = R"( )"; char error[1024]; size_t error_sz = 1024; // load VFS on the heap auto vfs = std::make_unique(); mj_defaultVFS(vfs.get()); // should fallback to OS filesystem mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); EXPECT_THAT(model, IsNull()); EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); } TEST_F(VfsTest, HFieldCustomWithVFS) { static constexpr char xml[] = R"( )"; char error[1024]; size_t error_sz = 1024; // load VFS on the heap auto vfs = std::make_unique(); mj_defaultVFS(vfs.get()); // should fallback to OS filesystem mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); EXPECT_THAT(model, IsNull()); EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); } TEST_F(VfsTest, TexturePngWithVFS) { static constexpr char xml[] = R"( )"; char error[1024]; size_t error_sz = 1024; // load VFS on the heap auto vfs = std::make_unique(); mj_defaultVFS(vfs.get()); // should fallback to OS filesystem mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); EXPECT_THAT(model, IsNull()); EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); } TEST_F(VfsTest, TextureCustomWithVFS) { static constexpr char xml[] = R"( )"; char error[1024]; size_t error_sz = 1024; // load VFS on the heap auto vfs = std::make_unique(); mj_defaultVFS(vfs.get()); // should fallback to OS filesystem mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); EXPECT_THAT(model, IsNull()); EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); } // ------------------------ test content_type attribute ------------------------ using ContentTypeTest = MujocoTest; TEST_F(ContentTypeTest, HFieldPngWithContentType) { static constexpr char xml[] = R"( )"; char error[1024]; size_t error_sz = 1024; // load VFS on the heap auto vfs = std::make_unique(); mj_defaultVFS(vfs.get()); // should try loading the file mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); EXPECT_THAT(model, IsNull()); EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); } TEST_F(ContentTypeTest, HFieldCustomWithContentType) { static constexpr char xml[] = R"( )"; char error[1024]; size_t error_sz = 1024; // load VFS on the heap auto vfs = std::make_unique(); mj_defaultVFS(vfs.get()); // should try loading the file mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); EXPECT_THAT(model, IsNull()); EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); } TEST_F(ContentTypeTest, HFieldWithContentTypeError) { static constexpr char xml[] = R"( )"; char error[1024]; size_t error_sz = 1024; // load VFS on the heap auto vfs = std::make_unique(); mj_defaultVFS(vfs.get()); // should try loading the file mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); EXPECT_THAT(model, IsNull()); EXPECT_THAT(error, HasSubstr("unsupported content type: 'image/jpeg'")); } TEST_F(ContentTypeTest, TexturePngWithContentType) { static constexpr char xml[] = R"( )"; char error[1024]; size_t error_sz = 1024; // load VFS on the heap auto vfs = std::make_unique(); mj_defaultVFS(vfs.get()); // should try loading the file mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); EXPECT_THAT(model, IsNull()); EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); } TEST_F(ContentTypeTest, TextureCustomWithContentType) { static constexpr char xml[] = R"( )"; char error[1024]; size_t error_sz = 1024; // load VFS on the heap auto vfs = std::make_unique(); mj_defaultVFS(vfs.get()); // should try loading the file mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); EXPECT_THAT(model, IsNull()); EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); } TEST_F(ContentTypeTest, TextureWithContentTypeError) { static constexpr char xml[] = R"( )"; char error[1024]; size_t error_sz = 1024; // load VFS on the heap auto vfs = std::make_unique(); mj_defaultVFS(vfs.get()); // should try loading the file mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); EXPECT_THAT(model, IsNull()); EXPECT_THAT(error, HasSubstr("unsupported content type: 'image/jpeg'")); } TEST_F(ContentTypeTest, TextureLoadPng) { static constexpr char filename[] = "tiny"; static constexpr char xml[] = R"( )"; // credit: https://www.mjt.me.uk/posts/smallest-png/ static constexpr unsigned char tiny[] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x66, 0xBC, 0x3A, 0x25, 0x00, 0x00, 0x00, 0x03, 0x50, 0x4C, 0x54, 0x45, 0xB5, 0xD0, 0xD0, 0x63, 0x04, 0x16, 0xEA, 0x00, 0x00, 0x00, 0x1F, 0x49, 0x44, 0x41, 0x54, 0x68, 0x81, 0xED, 0xC1, 0x01, 0x0D, 0x00, 0x00, 0x00, 0xC2, 0xA0, 0xF7, 0x4F, 0x6D, 0x0E, 0x37, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBE, 0x0D, 0x21, 0x00, 0x00, 0x01, 0x9A, 0x60, 0xE1, 0xD5, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 }; size_t tiny_sz = sizeof(tiny); char error[1024]; size_t error_sz = 1024; // load VFS on the heap auto vfs = std::make_unique(); mj_defaultVFS(vfs.get()); mj_makeEmptyFileVFS(vfs.get(), filename, 105); int i = mj_findFileVFS(vfs.get(), filename); memcpy(vfs->filedata[i], tiny, tiny_sz); // loading the file should be successful mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); EXPECT_THAT(model, NotNull()); mj_deleteModel(model); mj_deleteFileVFS(vfs.get(), filename); } // ------------------------ test keyframes ------------------------------------- using KeyframeTest = MujocoTest; constexpr char kKeyframePath[] = "user/testdata/keyframe.xml"; TEST_F(KeyframeTest, CheckValues) { const std::string xml_path = GetTestDataFilePath(kKeyframePath); mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); ASSERT_THAT(model, NotNull()); EXPECT_EQ(model->nkey, 7); EXPECT_EQ(model->key_time[0 * 1], 0.1); EXPECT_EQ(model->key_qpos[1 * model->nq], 0.2); EXPECT_EQ(model->key_qvel[2 * model->nv], 0.3); EXPECT_EQ(model->key_act[3 * model->na], 0.4); EXPECT_THAT(AsVector(model->key_ctrl + 4*model->nu, model->nu), ElementsAre(0.5, 0.6)); EXPECT_THAT(AsVector(model->key_mpos + 3*model->nmocap*5, 3), ElementsAre(.1, .2, .3)); EXPECT_THAT(AsVector(model->key_mquat + 4*model->nmocap*6, 4), ElementsAre(.5, .5, .5, .5)); mj_deleteModel(model); } TEST_F(KeyframeTest, ResetDataKeyframe) { const std::string xml_path = GetTestDataFilePath(kKeyframePath); mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); ASSERT_THAT(model, NotNull()); mjData* data = mj_makeData(model); mj_resetDataKeyframe(model, data, 0); EXPECT_EQ(data->time, 0.1); mj_resetDataKeyframe(model, data, 1); EXPECT_EQ(data->qpos[0], 0.2); mj_resetDataKeyframe(model, data, 2); EXPECT_EQ(data->qvel[0], 0.3); mj_resetDataKeyframe(model, data, 3); EXPECT_EQ(data->act[0], 0.4); mj_resetDataKeyframe(model, data, 4); EXPECT_EQ(data->ctrl[0], 0.5); EXPECT_EQ(data->ctrl[1], 0.6); mj_resetDataKeyframe(model, data, 5); EXPECT_THAT(AsVector(data->mocap_pos, 3), ElementsAre(.1, .2, .3)); mj_resetDataKeyframe(model, data, 6); EXPECT_THAT(AsVector(data->mocap_quat, 4), ElementsAre(.5, .5, .5, .5)); mj_deleteData(data); mj_deleteModel(model); } TEST_F(KeyframeTest, BadSize) { static constexpr char xml[] = R"( )"; char error[1024]; size_t error_sz = 1024; mjModel* model = LoadModelFromString(xml, error, error_sz); EXPECT_THAT(model, IsNull()); EXPECT_THAT(error, HasSubstr("invalid qpos size, expected length 0")); } // ------------- test relative frame sensor compilation------------------------- using RelativeFrameSensorParsingTest = MujocoTest; TEST_F(RelativeFrameSensorParsingTest, RefTypeNotRequired) { static constexpr char xml[] = R"( )"; mjModel* model = LoadModelFromString(xml, 0, 0); ASSERT_THAT(model, NotNull()); mj_deleteModel(model); } TEST_F(RelativeFrameSensorParsingTest, ReferenceBodyFound) { static constexpr char xml[] = R"( )"; mjModel* model = LoadModelFromString(xml, 0, 0); ASSERT_THAT(model, NotNull()); ASSERT_EQ(model->sensor_reftype[0], mjOBJ_XBODY); ASSERT_EQ(model->sensor_refid[0], 1); mj_deleteModel(model); } TEST_F(RelativeFrameSensorParsingTest, MissingRefname) { static constexpr char xml[] = R"( )"; std::array error; LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(error.data(), HasSubstr("missing name of reference frame")); } TEST_F(RelativeFrameSensorParsingTest, BadRefName) { static constexpr char xml[] = R"( )"; std::array error; LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(error.data(), HasSubstr("unrecognized name of reference frame")); } TEST_F(RelativeFrameSensorParsingTest, BadRefType) { static constexpr char xml[] = R"( )"; std::array error; LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(error.data(), HasSubstr("reference frame object must be")); } // ------------- sensor compilation -------------------------------------------- using SensorTest = MujocoTest; TEST_F(SensorTest, OjbtypeParsedButNotRequired) { static constexpr char xml[] = R"( )"; mjModel* model = LoadModelFromString(xml, 0, 0); ASSERT_THAT(model, NotNull()); EXPECT_EQ(model->sensor_datatype[0], mjDATATYPE_AXIS); EXPECT_EQ(model->sensor_objtype[0], mjOBJ_UNKNOWN); EXPECT_EQ(model->sensor_dim[0], 3); EXPECT_EQ(model->sensor_datatype[1], mjDATATYPE_REAL); EXPECT_EQ(model->sensor_objtype[1], mjOBJ_BODY); EXPECT_EQ(model->sensor_objid[1], 0); mj_deleteModel(model); } // ------------- test capsule inertias ----------------------------------------- static const char* const kCapsuleInertiaPath = "user/testdata/capsule_inertia.xml"; using MjCGeomTest = MujocoTest; static constexpr int kSphereBodyId = 1, kCylinderBodyId = 2, kCapsuleBodyId = 3, kCapsuleGeomId = 2; TEST_F(MjCGeomTest, CapsuleMass) { const std::string xml_path = GetTestDataFilePath(kCapsuleInertiaPath); mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, 0, 0); // Mass of capsule should equal mass of cylinder + mass of sphere. mjtNum sphere_cylinder_mass = model->body_mass[kSphereBodyId] + model->body_mass[kCylinderBodyId]; mjtNum capsule_mass = model->body_mass[kCapsuleBodyId]; EXPECT_DOUBLE_EQ(sphere_cylinder_mass, capsule_mass); mj_deleteModel(model); } TEST_F(MjCGeomTest, CapsuleInertiaZ) { const std::string xml_path = GetTestDataFilePath(kCapsuleInertiaPath); mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, 0, 0); // z-inertia of capsule should equal sphere + cylinder z-inertia. mjtNum sphere_cylinder_z_inertia = model->body_inertia[3*kSphereBodyId + 2] + model->body_inertia[3*kCylinderBodyId + 2]; mjtNum capsule_z_inertia = model->body_inertia[3*kCapsuleBodyId + 2]; EXPECT_DOUBLE_EQ(sphere_cylinder_z_inertia, capsule_z_inertia); mj_deleteModel(model); } TEST_F(MjCGeomTest, CapsuleInertiaX) { const std::string xml_path = GetTestDataFilePath(kCapsuleInertiaPath); mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, 0, 0); // The CoM of a solid hemisphere is 3/8*radius away from from the disk. mjtNum hs_com = model->geom_size[3*kCapsuleGeomId] * 3 / 8; // The mass of the two hemispherical end-caps is just the mass of the sphere. mjtNum sphere_mass = model->body_mass[1]; // x-inertia of capsule should equal sphere + cylinder x-inertias, with // corrections from shifting the hemispheres using parallel axis theorem. mjtNum sphere_cylinder_x_inertia = model->body_inertia[3*kSphereBodyId] + model->body_inertia[3*kCylinderBodyId]; // Parallel axis-theorem #1: translate the hemispheres in to the origin. mjtNum translate_in = hs_com; sphere_cylinder_x_inertia -= sphere_mass * translate_in*translate_in; // Parallel axis-theorem #2: translate the hemispheres out to the end caps. mjtNum cylinder_half_length = model->geom_size[3*kCapsuleGeomId + 1]; mjtNum translate_out = cylinder_half_length + hs_com; sphere_cylinder_x_inertia += sphere_mass * translate_out*translate_out; // Compare native capsule inertia and computed inertia. mjtNum capsule_x_inertia = model->body_inertia[3*kCapsuleBodyId]; EXPECT_DOUBLE_EQ(sphere_cylinder_x_inertia, capsule_x_inertia); mj_deleteModel(model); } // ------------- test inertiagrouprange ---------------------------------------- TEST_F(MjCGeomTest, IgnoreGeomOutsideInertiagrouprange) { static constexpr char xml[] = R"( )"; mjModel* m = LoadModelFromString(xml, nullptr, 0); EXPECT_THAT(m->body_mass[1], 0); mj_deleteModel(m); } TEST_F(MjCGeomTest, IgnoreBadGeomOutsideInertiagrouprange) { static constexpr char xml[] = R"( )"; mjModel* m = LoadModelFromString(xml, nullptr, 0); EXPECT_THAT(m->body_mass[1], 0); mj_deleteModel(m); } // ------------- test invalid size values -------------------------------------- TEST_F(MjCGeomTest, NanSize) { // even if the caller ignores warnings, models shouldn't compile with NaN // geom sizes mju_user_warning = nullptr; static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); ASSERT_THAT(model, testing::IsNull()); ASSERT_THAT(error.data(), HasSubstr("nan")); } // ------------- test height fields -------------------------------------------- using MjCHFieldTest = MujocoTest; TEST_F(MjCHFieldTest, PngMap) { const std::string xml_path = GetTestDataFilePath("user/testdata/png_hfield.xml"); std::array error; mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, error.data(), error.size()); ASSERT_THAT(model, NotNull()) << error.data(); EXPECT_EQ(model->nhfield, 1); EXPECT_EQ(model->geom_type[0], mjGEOM_HFIELD); EXPECT_GT(model->nhfielddata, 0); float min_hfield = 1e7; float max_hfield = -1e7; for (int i = 0; i < model->nhfielddata; i++) { float v = model->hfield_data[i]; min_hfield = std::min(min_hfield, v); max_hfield = std::max(max_hfield, v); } EXPECT_EQ(min_hfield, 0) << "hfield should be normalised to [0, 1]"; EXPECT_EQ(max_hfield, 1) << "hfield should be normalised to [0, 1]"; mj_deleteModel(model); } // ------------- test quaternion normalization---------------------------------- using QuatNorm = MujocoTest; TEST_F(QuatNorm, QuatNotNormalized) { static constexpr char xml[] = R"( )"; std::array error; mjModel* m = LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(AsVector(m->body_quat+4, 4), ElementsAre(1./5, 2./5, 2./5, 4./5)); EXPECT_THAT(AsVector(m->geom_quat, 4), ElementsAre(1./5, 2./5, 2./5, 4./5)); EXPECT_THAT(AsVector(m->site_quat, 4), ElementsAre(1./5, 2./5, 2./5, 4./5)); EXPECT_THAT(AsVector(m->cam_quat, 4), ElementsAre(1./5, 2./5, 2./5, 4./5)); mj_deleteModel(m); } // ------------- test camera specifications ------------------------------------ using CameraSpecTest = MujocoTest; TEST_F(CameraSpecTest, FovyLimits) { static constexpr char xml[] = R"( )"; std::array error; mjModel* m = LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(m, IsNull()) << error.data(); EXPECT_THAT(error.data(), HasSubstr("fovy too large")); mj_deleteModel(m); } TEST_F(CameraSpecTest, DuplicatedFocalIgnorePixel) { static constexpr char xml[] = R"( )"; std::array error; mjModel* m = LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(m, NotNull()) << error.data(); EXPECT_NEAR(m->cam_intrinsic[0], 5e-4, 1e-6); // focal length in meters (x) EXPECT_NEAR(m->cam_intrinsic[1], 5e-4, 1e-6); // focal length in meters (y) mj_deleteModel(m); } TEST_F(CameraSpecTest, FovyFromResolution) { static constexpr char xml[] = R"( )"; std::array error; mjModel* m = LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(m, NotNull()) << error.data(); EXPECT_NEAR(m->cam_fovy[0], 41.112, 1e-3); EXPECT_NEAR(m->cam_intrinsic[0], 8e-3, 1e-6); // focal length in meters (x) EXPECT_NEAR(m->cam_intrinsic[1], 8e-3, 1e-6); // focal length in meters (y) EXPECT_EQ(m->cam_intrinsic[2], 0); // principal point in meters (x) EXPECT_EQ(m->cam_intrinsic[3], 0); // principal point in meters (y) mj_deleteModel(m); } TEST_F(CameraSpecTest, FovyFromResolutionPixel) { static constexpr char xml[] = R"( )"; std::array error; mjModel* m = LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(m, NotNull()) << error.data(); EXPECT_NEAR(m->cam_fovy[0], 41.112, 1e-3); EXPECT_NEAR(m->cam_intrinsic[0], 8e-3, 1e-6); // focal length in meters (x) EXPECT_NEAR(m->cam_intrinsic[1], 8e-3, 1e-6); // focal length in meters (y) EXPECT_EQ(m->cam_intrinsic[2], 0); // principal point in meters (x) EXPECT_EQ(m->cam_intrinsic[3], 0); // principal point in meters (y) mj_deleteModel(m); } // ------------- test actuator order ------------------------------------------- using ActuatorTest = MujocoTest; TEST_F(ActuatorTest, ActuatorOrderDoesntMatter) { static constexpr char xml1[] = R"( )"; static constexpr char xml2[] = R"( )"; mjModel* model1 = LoadModelFromString(xml1, nullptr, 0); mjData* data1 = mj_makeData(model1); mjModel* model2 = LoadModelFromString(xml2, nullptr, 0); mjData* data2 = mj_makeData(model2); // check activation indexing EXPECT_EQ(model1->actuator_actadr[0], 0); EXPECT_EQ(model1->actuator_actadr[1], -1); EXPECT_EQ(model2->actuator_actadr[0], -1); EXPECT_EQ(model2->actuator_actadr[1], 0); // integrate both models, flipping the controls while (data1->time < 1) { data1->ctrl[0] = data1->time; data1->ctrl[1] = -data1->time; mj_step(model1, data1); } while (data2->time < 1) { data2->ctrl[0] = -data2->time; data2->ctrl[1] = data2->time; mj_step(model2, data2); } // expect states to match exactly EXPECT_EQ(data1->qpos[0], data2->qpos[0]); EXPECT_EQ(data1->qvel[0], data2->qvel[0]); EXPECT_EQ(data1->act[0], data2->act[0]); mj_deleteData(data2); mj_deleteModel(model2); mj_deleteData(data1); mj_deleteModel(model1); } // ------------- test actlimited and actrange fields --------------------------- using ActRangeTest = MujocoTest; TEST_F(ActRangeTest, ActRangeParsed) { static constexpr char xml[] = R"( )"; mjModel* m = LoadModelFromString(xml, nullptr, 0); EXPECT_EQ(m->actuator_actlimited[0], 1); EXPECT_EQ(m->actuator_actrange[0], -1); EXPECT_EQ(m->actuator_actrange[1], 1.5); mj_deleteModel(m); } TEST_F(ActRangeTest, ActRangeBad) { 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 actrange")); } TEST_F(ActRangeTest, ActRangeUndefined) { 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 actrange")); } TEST_F(ActRangeTest, ActRangeNoDyntype) { 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("actrange specified but dyntype is 'none'")); } TEST_F(ActRangeTest, ActRangeDefaultsPropagate) { static constexpr char xml[] = R"( )"; std::array error; mjModel* model = LoadModelFromString(xml, error.data(), error.size()); // first actuator EXPECT_THAT(model->actuator_actlimited[0], 1); EXPECT_THAT(model->actuator_actrange[0], -1); EXPECT_THAT(model->actuator_actrange[1], 1); // // second actuator EXPECT_THAT(model->actuator_actlimited[1], 0); EXPECT_THAT(model->actuator_actrange[2], 2); EXPECT_THAT(model->actuator_actrange[3], 3); mj_deleteModel(model); } // ---------------------------- test actdim ------------------------------------ using ActDimTest = MujocoTest; TEST_F(ActDimTest, BiggerThanOneOnlyForUser) { 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("actdim > 1 is only allowed for dyntype 'user'")); } TEST_F(ActDimTest, NonzeroNotAllowedInStateless) { 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 actdim 1 in stateless")); } TEST_F(ActDimTest, ZeroNotAllowedInStateful) { 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 actdim 0 in stateful")); } // ------------- test nuser_xxx fields ----------------------------------------- using UserDataTest = MujocoTest; TEST_F(UserDataTest, NBodyTooSmall) { 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(UserDataTest, NJointTooSmall) { 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(UserDataTest, NGeomTooSmall) { 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(UserDataTest, NSiteTooSmall) { 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(UserDataTest, NCameraTooSmall) { 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(UserDataTest, NTendonTooSmall) { 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(UserDataTest, NActuatorTooSmall) { 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(UserDataTest, NSensorTooSmall) { 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 for auto parsing of *limited fields ---------------------- using LimitedTest = MujocoTest; constexpr char kKeyAutoLimits[] = "user/testdata/auto_limits.xml"; // check joint limit values when automatically inferred based on range TEST_F(LimitedTest, JointLimited) { const std::string xml_path = GetTestDataFilePath(kKeyAutoLimits); mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); ASSERT_THAT(model, NotNull()); // see `user/testdata/auto_limits.xml` for expected values for (int i=0; i < model->njnt; i++) { EXPECT_EQ(model->jnt_limited[i], (mjtByte)model->jnt_user[i]); } mj_deleteModel(model); } TEST_F(LimitedTest, ErrorIfLimitedMissingOnJoint) { 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("limited")); } TEST_F(LimitedTest, ExplicitLimitedFalseIsOk) { 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->jnt_limited[0], 0); mj_deleteModel(model); } TEST_F(LimitedTest, ErrorIfLimitedMissingOnTendon) { 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("limited")); EXPECT_THAT(error.data(), HasSubstr("tendon")); } TEST_F(LimitedTest, ErrorIfForceLimitedMissingOnActuator) { 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("forcelimited")); EXPECT_THAT(error.data(), HasSubstr("forcerange")); EXPECT_THAT(error.data(), HasSubstr("actuator")); } // ------------- tests for tendon springrange ---------------------------------- using SpringrangeTest = MujocoTest; TEST_F(SpringrangeTest, DefaultsPropagate) { 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->tendon_lengthspring[0], .2); EXPECT_EQ(model->tendon_lengthspring[1], .5); mj_deleteModel(model); } TEST_F(SpringrangeTest, InvalidRange) { 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 springlength in tendon")); } // ------------- test frame ---------------------------------------------------- TEST_F(MujocoTest, Frame) { static constexpr char xml[] = R"( )"; constexpr mjtNum eps = 1e-14; std::array error; mjModel* m = LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(m, testing::NotNull()) << error.data(); EXPECT_EQ(m->nbody, 5); // geom quat transformed to euler = 0 0 50 EXPECT_NEAR(m->geom_quat[0], mju_cos(25. * mjPI / 180.), 1e-3); EXPECT_NEAR(m->geom_quat[1], 0, 0); EXPECT_NEAR(m->geom_quat[2], 0, 0); EXPECT_NEAR(m->geom_quat[3], mju_sin(25. * mjPI / 180.), 1e-3); // geom transformed to frame 0 1 0, 0 0 1, 1 0 0 EXPECT_NEAR(m->geom_quat[4], .5, eps); EXPECT_NEAR(m->geom_quat[5], .5, eps); EXPECT_NEAR(m->geom_quat[6], .5, eps); EXPECT_NEAR(m->geom_quat[7], .5, eps); // geom pos transformed from 0 1 0 to 0 2 0 EXPECT_EQ(m->geom_pos[ 9], 0); EXPECT_EQ(m->geom_pos[10], 2); EXPECT_EQ(m->geom_pos[11], 0); // body pos transformed from 1 0 0 to 1 1 0 EXPECT_EQ(m->body_pos[6], 1); EXPECT_EQ(m->body_pos[7], 1); EXPECT_EQ(m->body_pos[8], 0); // nested geom pos not transformed EXPECT_EQ(m->geom_pos[12], 0); EXPECT_EQ(m->geom_pos[13], 0); EXPECT_EQ(m->geom_pos[14], 1); // joint axis transformed to 0 -1 0 EXPECT_NEAR(m->jnt_axis[0], 0, eps); EXPECT_NEAR(m->jnt_axis[1], -1, eps); EXPECT_NEAR(m->jnt_axis[2], 0, eps); mjData* d = mj_makeData(m); mj_kinematics(m, d); // body and frame equivalence geom 2 vs 6 EXPECT_NEAR(d->geom_xpos[6], d->geom_xpos[18], eps); EXPECT_NEAR(d->geom_xpos[7], d->geom_xpos[19], eps); EXPECT_NEAR(d->geom_xpos[8], d->geom_xpos[20], eps); EXPECT_NEAR(d->geom_xmat[18], d->geom_xmat[54], eps); EXPECT_NEAR(d->geom_xmat[19], d->geom_xmat[55], eps); EXPECT_NEAR(d->geom_xmat[20], d->geom_xmat[56], eps); EXPECT_NEAR(d->geom_xmat[21], d->geom_xmat[57], eps); mj_deleteModel(m); mj_deleteData(d); } } // namespace } // namespace mujoco