Mujoco KDL Wrapper  0.5.1
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
test_regressions.cpp
Go to the documentation of this file.
1// Regressions from the 0.4.0 audit; headless, self-skips without the bundled Gen3.
2
5#include "example_paths.hpp"
6
7#include <gtest/gtest.h>
8
9#include <algorithm>
10#include <cstdlib>
11#include <filesystem>
12#include <fstream>
13#include <string>
14#include <thread>
15#include <vector>
16
17namespace fs = std::filesystem;
18
19static mj_kdl::SceneObject make_box(const std::string &name, double x)
20{
22 box.name = name;
24 box.fixed = true;
25 for (int k = 0; k < 3; ++k) box.size[k] = 0.02;
26 box.pos[0] = x;
27 box.pos[1] = 0.5;
28 box.pos[2] = 0.02;
29 for (int k = 0; k < 4; ++k) box.rgba[k] = k == 0 || k == 3 ? 1.0f : 0.0f;
30 box.friction[0] = 1.0;
31 box.friction[1] = 0.005;
32 box.friction[2] = 0.0001;
33 return box;
34}
35
36class RebuildTest : public testing::Test
37{
38 protected:
42
43 void SetUp() override
44 {
45 const std::string gen3 = mj_kdl_examples::find_asset("kinova_gen3/gen3.xml");
46 if (gen3.empty()) GTEST_SKIP() << "kinova_gen3/gen3.xml not found";
47 spec_.timestep = 0.002;
48 spec_.add_floor = true;
49 spec_.add_skybox = false;
50 spec_.robots.emplace_back();
51 spec_.robots.back().path = gen3;
52 ASSERT_TRUE(mj_kdl::init_env(&env_, &spec_));
53 ASSERT_TRUE(mj_kdl::init_robot_from_mjcf(&robot_, &env_, "base_link", "bracelet_link"));
54 }
55
56 double qpos(const char *joint) const
57 {
58 return env_.data->qpos[env_.model->jnt_qposadr[mj_name2id(env_.model, mjOBJ_JOINT, joint)]];
59 }
60 double ctrl(const char *actuator) const
61 {
62 return env_.data->ctrl[mj_name2id(env_.model, mjOBJ_ACTUATOR, actuator)];
63 }
64};
65
66TEST_F(RebuildTest, AddObjectKeepsThePhysicsStateAndTheCommands)
67{
69 for (int k = 0; k < 50; ++k) {
70 mj_kdl::update(&env_);
71 robot_.jnt_trq_cmd[1] = 5.0;
72 mj_kdl::step(&env_);
73 }
74 mj_kdl::update(&env_);
75 const double time = env_.data->time;
76 const double q2 = qpos("joint_2");
77 const double u2 = ctrl("joint_2_torque");
78 const auto q_msr = robot_.jnt_pos_msr;
79 const auto trq_in = robot_.jnt_trq_cmd;
80 ASSERT_NE(q2, 0.0);
81 ASSERT_NE(u2, 0.0);
82
83 ASSERT_TRUE(mj_kdl::scene_add_object(&env_, make_box("box", 0.5)));
84 EXPECT_EQ(env_.data->time, time);
85 EXPECT_EQ(qpos("joint_2"), q2);
86 EXPECT_EQ(ctrl("joint_2_torque"), u2) << "the command survives the mode switch";
87 EXPECT_EQ(robot_.ctrl_mode, mj_kdl::CtrlMode::TORQUE);
88 EXPECT_EQ(robot_.jnt_trq_cmd, trq_in);
89 mj_kdl::update(&env_);
90 EXPECT_EQ(robot_.jnt_pos_msr, q_msr);
91}
92
93TEST_F(RebuildTest, FailedRemoveKeepsTheObjectOrder)
94{
95 spec_.objects.push_back(make_box("base", 0.5));
96 auto child = make_box("child", 0.0);
97 child.attach_to = { mj_kdl::AttachKind::Body, "base" };
98 spec_.objects.push_back(child);
99 ASSERT_TRUE(mj_kdl::init_env(&env_, &spec_));
100
101 EXPECT_FALSE(mj_kdl::scene_remove_object(&env_, "base")) << "child needs its parent";
102 ASSERT_EQ(env_.spec.objects.size(), 2u);
103 EXPECT_EQ(env_.spec.objects[0].name, "base");
104 EXPECT_EQ(env_.spec.objects[1].name, "child");
105 EXPECT_TRUE(mj_kdl::scene_add_object(&env_, make_box("more", -0.5)));
106}
107
108TEST_F(RebuildTest, RemovingARobotsJointFailsAndLeavesTheEnv)
109{
110 const fs::path xml = fs::temp_directory_path() / "mj_kdl_regressions_door.xml";
111 std::ofstream(xml) << R"(<mujoco><worldbody><body name="door" pos="0 1 0.5">
112 <joint name="door_hinge" type="hinge" axis="0 0 1"/>
113 <geom type="box" size="0.2 0.02 0.3" mass="1"/></body></worldbody></mujoco>)";
114 mj_kdl::SceneObject door_obj;
115 door_obj.name = "door";
116 door_obj.mjcf_path = xml.string();
117 door_obj.fixed = true;
118 ASSERT_TRUE(mj_kdl::scene_add_object(&env_, door_obj));
119 KDL::Chain chain;
120 chain.addSegment(KDL::Segment("door", KDL::Joint("door_hinge", KDL::Joint::RotZ)));
121 mj_kdl::Robot door;
122 ASSERT_TRUE(mj_kdl::init_robot_from_chain(&door, &env_, chain, { "door_hinge" }));
123
124 mjModel *const model = env_.model;
125 EXPECT_FALSE(mj_kdl::scene_remove_object(&env_, "door"));
126 fs::remove(xml);
127 EXPECT_EQ(env_.model, model);
128 EXPECT_EQ(env_.spec.objects.size(), 1u);
129 mj_kdl::update(&env_);
130 mj_kdl::step(&env_);
131 EXPECT_EQ(door.jnt_pos_msr.size(), 1u);
133
134TEST_F(RebuildTest, FailedReinitLeavesTheRobotAsItWas)
135{
137 tool.tcp_site = "no_such_site";
138 EXPECT_FALSE(
139 mj_kdl::init_robot_from_mjcf(&robot_, &env_, "base_link", "spherical_wrist_2_link", "", &tool)
140 );
141 EXPECT_EQ(robot_.n_joints, 7);
142 EXPECT_EQ(robot_.chain.getNrOfJoints(), 7u);
143 EXPECT_EQ(robot_.jnt_pos_msr.size(), 7u);
144 EXPECT_EQ(env_.robots.size(), 1u);
145 mj_kdl::update(&env_);
146 mj_kdl::step(&env_);
148
149TEST_F(RebuildTest, AnOnResetSetBeforeInitEnvIsKept)
150{
151 mj_kdl::Env env;
152 int calls = 0;
153 env.on_reset = [&calls](mj_kdl::ResetContext *) { ++calls; };
154 ASSERT_TRUE(mj_kdl::init_env(&env, &spec_));
155 mj_kdl::reset(&env);
156 EXPECT_EQ(calls, 1);
158
159TEST_F(RebuildTest, RecordersFollowARebuildAndOutliveEachOther)
160{
162 if (!mj_kdl::init_offscreen(&a, env_.model, 64, 48)) GTEST_SKIP() << "no EGL";
163 ASSERT_TRUE(mj_kdl::init_offscreen(&b, env_.model, 64, 48));
164 std::vector<std::uint8_t> rgb(64 * 48 * 3);
165 ASSERT_TRUE(mj_kdl::render_rgb(&a, &env_, rgb.data()));
166 ASSERT_TRUE(mj_kdl::render_rgb(&b, &env_, rgb.data()));
167
168 ASSERT_TRUE(mj_kdl::scene_add_object(&env_, make_box("box", 0.5)));
169 ASSERT_TRUE(mj_kdl::render_rgb(&a, &env_, rgb.data()));
170 const std::vector<std::uint8_t> before = rgb;
171 mj_kdl::cleanup(&b);
172 std::fill(rgb.begin(), rgb.end(), 0xAB);
173 EXPECT_TRUE(mj_kdl::render_rgb(&a, &env_, rgb.data()));
174 EXPECT_EQ(rgb, before) << "b's cleanup ended a's context";
175 ASSERT_TRUE(mj_kdl::init_offscreen(&a, env_.model, 32, 24)) << "an open recorder re-inits";
176 EXPECT_TRUE(mj_kdl::render_rgb(&a, &env_, rgb.data()));
177 mj_kdl::cleanup(&a);
179
180TEST_F(RebuildTest, AFrameShowsTheStateItWasTakenAt)
181{
183 if (!mj_kdl::init_offscreen(&vr, env_.model, 64, 48)) GTEST_SKIP() << "no EGL";
184 std::vector<std::uint8_t> at_a(64 * 48 * 3), at_b(64 * 48 * 3), called;
185 ASSERT_TRUE(mj_kdl::render_rgb(&vr, &env_, at_a.data()));
186
187 std::thread::id render_thread;
188 ASSERT_TRUE(mj_kdl::render_rgb(&vr, &env_, [&](const std::uint8_t *rgb) {
189 called.assign(rgb, rgb + at_a.size());
190 render_thread = std::this_thread::get_id();
191 }));
192 KDL::JntArray q(robot_.n_joints);
193 for (int i = 0; i < robot_.n_joints; ++i) q(i) = 1.0;
194 mj_kdl::set_joint_pos(&robot_, q);
195 ASSERT_TRUE(mj_kdl::render_rgb(&vr, &env_, at_b.data()));
196 mj_kdl::cleanup(&vr);
197
198 EXPECT_NE(at_a, at_b) << "the arm moved in view";
199 EXPECT_EQ(called, at_a) << "the callback shows the snapshot, not the state it rendered in";
200 EXPECT_NE(render_thread, std::this_thread::get_id());
202
203TEST_F(RebuildTest, OneRecorderKeepsEveryFrameItIsGiven)
204{
206 if (!mj_kdl::init_offscreen(&vr, env_.model, 64, 48)) GTEST_SKIP() << "no EGL";
207 int frames = 0;
208 for (int i = 0; i < 50; ++i) {
209 mj_kdl::step(&env_);
210 ASSERT_TRUE(mj_kdl::render_rgb(&vr, &env_, [&frames](const std::uint8_t *) { ++frames; }));
211 }
212 mj_kdl::cleanup(&vr);
213 EXPECT_EQ(frames, 50);
215
216TEST_F(RebuildTest, AnAttachmentLeftAtTheWorldIsRefused)
217{
218 mj_kdl::SceneSpec spec = spec_;
220 loose.mjcf_path = spec.robots[0].path;
221 loose.prefix = "loose_";
222 spec.robots[0].attachments.push_back(loose);
223 mj_kdl::Env env;
224 const mj_kdl::Status s = mj_kdl::init_env(&env, &spec);
225 EXPECT_FALSE(s);
226 EXPECT_NE(s.error.find("attach_to is the world"), std::string::npos) << s.error;
228
229TEST_F(RebuildTest, APortPointerSurvivesAReset)
230{
232 double *torque = &robot_.jnt_trq_cmd[1];
233 const double *position = &robot_.jnt_pos_msr[1];
234
235 mj_kdl::reset(&env_);
236 *torque = 5.0;
237 mj_kdl::update(&env_);
238 EXPECT_EQ(ctrl("joint_2_torque"), 5.0) << "a command written through a held pointer";
239 EXPECT_EQ(*position, qpos("joint_2")) << "a measurement read through a held pointer";
241
242TEST(LogLevel, IsASeverityThreshold)
243{
244 const auto level = mj_kdl::get_log_level();
245 const auto logs = [](mj_kdl::LogLevel threshold) {
246 mj_kdl::set_log_level(threshold);
247 testing::internal::CaptureStderr();
248 MJ_LOG_INFO("info");
249 MJ_LOG_WARN("warn");
250 MJ_LOG_ERROR("error");
251 return testing::internal::GetCapturedStderr();
252 };
253 const std::string info = logs(mj_kdl::LogLevel::INFO);
254 EXPECT_NE(info.find("info"), std::string::npos);
255 EXPECT_NE(info.find("error"), std::string::npos);
256 const std::string warn = logs(mj_kdl::LogLevel::WARN);
257 EXPECT_EQ(warn.find("info"), std::string::npos);
258 EXPECT_NE(warn.find("warn"), std::string::npos);
259 EXPECT_NE(warn.find("error"), std::string::npos);
260 const std::string error = logs(mj_kdl::LogLevel::ERROR);
261 EXPECT_EQ(error.find("warn"), std::string::npos);
262 EXPECT_NE(error.find("error"), std::string::npos);
263 EXPECT_TRUE(logs(mj_kdl::LogLevel::NONE).empty());
266
267TEST(Screenshot, PathReachesFfmpegVerbatimAndAMissingFfmpegIsAFailure)
268{
269 const fs::path dir = fs::temp_directory_path() / "mj_kdl_png_test";
270 fs::remove_all(dir);
271 fs::create_directories(dir);
272 const fs::path out = dir / "a \"quoted\" $(touch injected) shot.png";
273 const fs::path marker = dir / "injected";
274 const std::vector<std::uint8_t> rgb(8 * 4 * 3, 128);
275
276 if (!mj_kdl::write_png_rgb(out.string(), rgb.data(), 8, 4)) GTEST_SKIP() << "no ffmpeg";
277 EXPECT_GT(fs::file_size(out), 0u);
278 EXPECT_FALSE(fs::exists(marker));
279 EXPECT_FALSE(fs::exists(fs::current_path() / "injected"));
280
281 const std::string path = std::getenv("PATH") ? std::getenv("PATH") : "";
282 setenv("PATH", dir.c_str(), 1);
283 const std::vector<std::uint8_t> big(1024 * 1024 * 3, 0);
284 EXPECT_FALSE(mj_kdl::write_png_rgb((dir / "b.png").string(), big.data(), 1024, 1024));
285 setenv("PATH", path.c_str(), 1);
286 fs::remove_all(dir);
287}
289// The display is unreachable, so no window can open; the viewer must say so, not end the process.
290TEST_F(RebuildTest, AViewerThatCannotOpenReturnsAnError)
291{
292 const char *display = std::getenv("DISPLAY");
293 const char *wayland = std::getenv("WAYLAND_DISPLAY");
294 const std::string saved_display = display ? display : "";
295 const std::string saved_wayland = wayland ? wayland : "";
296 setenv("DISPLAY", ":987", 1);
297 unsetenv("WAYLAND_DISPLAY");
298
299 const mj_kdl::Status s = mj_kdl::open_viewer(&env_);
300
301 display ? setenv("DISPLAY", saved_display.c_str(), 1) : unsetenv("DISPLAY");
302 if (wayland) setenv("WAYLAND_DISPLAY", saved_wayland.c_str(), 1);
303 EXPECT_FALSE(s);
304 EXPECT_FALSE(mj_kdl::is_running(&env_.viewer));
306
307int main(int argc, char *argv[])
308{
309 testing::InitGoogleTest(&argc, argv);
310 return RUN_ALL_TESTS();
311}
void addSegment(const Segment &segment)
double qpos(const char *joint) const
void SetUp() override
mj_kdl::SceneSpec spec_
mj_kdl::Env env_
mj_kdl::Robot robot_
double ctrl(const char *actuator) const
ResetInfo reset(Env *env, const ResetOptions *options=nullptr)
Status init_env(Env *env, const SceneSpec *spec)
void update(Env *env)
bool step(Env *env)
void set_log_level(LogLevel level)
LogLevel get_log_level()
Status init_offscreen(VideoRecorder *vr, mjModel *model, int width, int height)
bool render_rgb(VideoRecorder *vr, Env *env, std::uint8_t *out)
Status set_control_mode(Robot *r, CtrlMode mode)
Status init_robot_from_mjcf(Robot *r, Env *env, const char *base_body, const char *tip_body, const char *prefix="", const ToolFrameSpec *tool=nullptr)
void set_joint_pos(Robot *r, const KDL::JntArray &q)
void cleanup(Robot *r)
Status init_robot_from_chain(Robot *r, Env *env, const KDL::Chain &chain, const std::vector< std::string > &joint_names, const char *prefix="", const ToolFrameSpec *tool=nullptr)
Status scene_remove_object(Env *env, const std::string &name)
Status scene_add_object(Env *env, const SceneObject &obj)
Status open_viewer(Env *env, const char *title="MuJoCo")
bool is_running(const Viewer *v)
#define MJ_LOG_ERROR(expr)
#define MJ_LOG_INFO(expr)
#define MJ_LOG_WARN(expr)
std::string find_asset(const fs::path &relative)
bool write_png_rgb(const std::string &path, const std::uint8_t *rgb, int width, int height)
ResetHook on_reset
std::vector< double > jnt_pos_msr
std::vector< RobotSpec > robots
std::string error
int main(int argc, char *argv[])
TEST_F(RebuildTest, AddObjectKeepsThePhysicsStateAndTheCommands)
TEST(LogLevel, IsASeverityThreshold)