080668df42
When reporting conflicts between parent and child elements during attachment, the warning message now indicates if a value is derived from the default rather than being explicitly authored in the XML. This improves clarity in conflict resolution warnings. PiperOrigin-RevId: 936724265 Change-Id: I68c677b19020c19bf0260f5b3b77fe6ca3661bb9
966 lines
31 KiB
C++
966 lines
31 KiB
C++
// Copyright 2026 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.
|
|
|
|
#include <array>
|
|
#include <string>
|
|
|
|
#include <gmock/gmock.h>
|
|
#include <gtest/gtest.h>
|
|
#include <mujoco/mjspec.h>
|
|
#include <mujoco/mujoco.h>
|
|
#include "src/xml/xml_api.h"
|
|
#include "test/fixture.h"
|
|
|
|
namespace mujoco {
|
|
namespace {
|
|
|
|
using ::testing::HasSubstr;
|
|
using ::testing::IsNull;
|
|
using ::testing::NotNull;
|
|
|
|
TEST_F(MujocoTest, AttachWarningsSurviveRecompile) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
// attach warnings should persist through recompilation
|
|
static constexpr char xml_parent[] = R"(
|
|
<mujoco>
|
|
<option timestep="0.005"/>
|
|
<worldbody>
|
|
<frame name="attachment"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char xml_child[] = R"(
|
|
<mujoco>
|
|
<option timestep="0.001"/>
|
|
<worldbody>
|
|
<body name="child">
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
std::array<char, 1024> er;
|
|
mjSpec* parent = mj_parseXMLString(xml_parent, 0, er.data(), er.size());
|
|
ASSERT_THAT(parent, NotNull()) << er.data();
|
|
mjSpec* child = mj_parseXMLString(xml_child, 0, er.data(), er.size());
|
|
ASSERT_THAT(child, NotNull()) << er.data();
|
|
|
|
mjsFrame* frame = mjs_findFrame(parent, "attachment");
|
|
ASSERT_THAT(frame, NotNull());
|
|
mjsElement* attached = mjs_attach(
|
|
frame->element, mjs_findBody(child, "child")->element, "child-", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
// attach warning should be present
|
|
int attach_warnings = mjs_numWarnings(parent);
|
|
EXPECT_GE(attach_warnings, 1);
|
|
EXPECT_THAT(mjs_getWarning(parent, 0), HasSubstr("timestep"));
|
|
|
|
// compile
|
|
mjModel* m = mj_compile(parent, nullptr);
|
|
ASSERT_THAT(m, NotNull()) << mjs_getError(parent);
|
|
|
|
// attach warnings should survive compilation
|
|
EXPECT_GE(mjs_numWarnings(parent), attach_warnings);
|
|
EXPECT_THAT(mjs_getWarning(parent, 0), HasSubstr("timestep"));
|
|
|
|
// recompile
|
|
mj_deleteModel(m);
|
|
m = mj_compile(parent, nullptr);
|
|
ASSERT_THAT(m, NotNull()) << mjs_getError(parent);
|
|
|
|
// attach warnings should still be there after recompile
|
|
EXPECT_GE(mjs_numWarnings(parent), attach_warnings);
|
|
EXPECT_THAT(mjs_getWarning(parent, 0), HasSubstr("timestep"));
|
|
|
|
mj_deleteModel(m);
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictWarningDefault) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
// default mode (warning): conflicting values should keep parent, set warning
|
|
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->option.timestep = 0.005;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.timestep = 0.001;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
// parent value should be unchanged
|
|
EXPECT_EQ(parent->option.timestep, mjtNum(0.005));
|
|
|
|
// one grouped warning per attach
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
std::string w = mjs_getWarning(parent, 0);
|
|
EXPECT_THAT(w, HasSubstr("policy is 'warning'"));
|
|
EXPECT_THAT(w, HasSubstr("timestep: parent has 0.005, child has 0.001,"
|
|
" keeping parent value"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictWarningNoConflict) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
// warning mode: child has non-default, parent is default → warn + keep parent
|
|
mjSpec* parent = mj_makeSpec();
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.timestep = 0.001;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
// warning produced when child is non-default
|
|
EXPECT_TRUE(mjs_isWarning(parent));
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
EXPECT_THAT(mjs_getWarning(parent, 0), HasSubstr("timestep"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictMergeMin) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->compiler.conflict = mjCONFLICT_MERGE;
|
|
parent->option.timestep = 0.005;
|
|
parent->option.tolerance = 1e-10;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.timestep = 0.001;
|
|
child->option.tolerance = 1e-12;
|
|
child->option.sleep_tolerance = 0.005;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
// min-fields should take the smaller value
|
|
EXPECT_EQ(parent->option.timestep, mjtNum(0.001));
|
|
EXPECT_EQ(parent->option.tolerance, mjtNum(1e-12));
|
|
// one side is default -> copy and warn
|
|
EXPECT_EQ(parent->option.sleep_tolerance, mjtNum(0.005));
|
|
|
|
// one grouped warning containing all three fields
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
std::string w = mjs_getWarning(parent, 0);
|
|
EXPECT_THAT(w, HasSubstr("policy is 'merge'"));
|
|
EXPECT_THAT(w, HasSubstr("timestep: parent has 0.005, child has 0.001,"
|
|
" taking the minimum"));
|
|
EXPECT_THAT(w, HasSubstr("tolerance"));
|
|
EXPECT_THAT(w, HasSubstr("sleep_tolerance"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictMergeMax) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->compiler.conflict = mjCONFLICT_MERGE;
|
|
parent->option.iterations = 150;
|
|
parent->nkey = 5;
|
|
parent->nuser_body = 2;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.iterations = 200;
|
|
child->nkey = 8;
|
|
child->nuser_body = 4;
|
|
|
|
mjsBody* child_body = mjs_addBody(mjs_findBody(child, "world"), 0);
|
|
mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), nullptr);
|
|
|
|
mjsElement* attached =
|
|
mjs_attach(frame->element, child_body->element, "child_", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
// max-fields should take the larger value
|
|
EXPECT_EQ(parent->option.iterations, 200);
|
|
EXPECT_EQ(parent->nkey, 8);
|
|
EXPECT_EQ(parent->nuser_body, 4);
|
|
|
|
// one grouped warning containing all three fields
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
std::string w = mjs_getWarning(parent, 0);
|
|
EXPECT_THAT(w, HasSubstr("iterations: parent has 150, child has 200,"
|
|
" taking the maximum"));
|
|
EXPECT_THAT(w, HasSubstr("nkey"));
|
|
EXPECT_THAT(w, HasSubstr("nuser_body"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictMergeError) {
|
|
// merge mode: conflicting error-fields should produce an error
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->compiler.conflict = mjCONFLICT_MERGE;
|
|
parent->option.integrator = mjINT_RK4;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.integrator = mjINT_IMPLICIT;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
|
|
// error-fields should fail on conflict
|
|
EXPECT_THAT(attached, IsNull());
|
|
EXPECT_THAT(mjs_getError(parent), HasSubstr("integrator: parent has"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictErrorMode) {
|
|
// error mode: any conflict -> error
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->compiler.conflict = mjCONFLICT_ERROR;
|
|
parent->option.timestep = 0.005;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.timestep = 0.001;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
|
|
EXPECT_THAT(attached, IsNull());
|
|
std::string error = mjs_getError(parent);
|
|
EXPECT_THAT(error, HasSubstr("policy is 'error'"));
|
|
EXPECT_THAT(error, HasSubstr("timestep: parent has 0.005, child has 0.001"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictErrorNoConflict) {
|
|
// error mode: no conflict (one side default) -> succeeds
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->compiler.conflict = mjCONFLICT_ERROR;
|
|
parent->option.timestep = 0.005;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
// child timestep is default -> no conflict
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
// parent unchanged
|
|
EXPECT_EQ(parent->option.timestep, mjtNum(0.005));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictMergeFromBody) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
// merge mode: attach from body (not spec) still merges
|
|
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->compiler.conflict = mjCONFLICT_MERGE;
|
|
parent->option.timestep = 0.005;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.timestep = 0.001;
|
|
mjsBody* child_body = mjs_addBody(mjs_findBody(child, "world"), 0);
|
|
|
|
mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), nullptr);
|
|
|
|
mjsElement* attached =
|
|
mjs_attach(frame->element, child_body->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
EXPECT_EQ(parent->option.timestep, mjtNum(0.001));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictMergeGravityError) {
|
|
// merge mode: conflicting gravity -> error
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->compiler.conflict = mjCONFLICT_MERGE;
|
|
parent->option.gravity[2] = -10.0;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.gravity[2] = -1.62;
|
|
|
|
mjsBody* child_body = mjs_addBody(mjs_findBody(child, "world"), 0);
|
|
mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), nullptr);
|
|
|
|
mjs_attach(frame->element, child_body->element, "child_", "");
|
|
|
|
EXPECT_THAT(mjs_getError(parent),
|
|
HasSubstr("gravity: parent has 0 0 -10, child has 0 0 -1.62"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictMergeRealtimeMin) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
// merge mode: conflicting realtime -> take min
|
|
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->compiler.conflict = mjCONFLICT_MERGE;
|
|
parent->visual.global.realtime = 2.0f;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->visual.global.realtime = 0.5f;
|
|
|
|
mjsBody* child_body = mjs_addBody(mjs_findBody(child, "world"), 0);
|
|
mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), nullptr);
|
|
|
|
mjsElement* attached =
|
|
mjs_attach(frame->element, child_body->element, "child_", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
EXPECT_EQ(parent->visual.global.realtime, 0.5f);
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictWarningBitfields) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
// warning mode: conflicting disable flags report individual flag names
|
|
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->option.disableflags = mjDSBL_GRAVITY | mjDSBL_CONTACT;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.disableflags = mjDSBL_GRAVITY | mjDSBL_ACTUATION;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
// Contact and Actuation differ, Gravity agrees -> 1 grouped warning
|
|
EXPECT_TRUE(mjs_isWarning(parent));
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
std::string w = mjs_getWarning(parent, 0);
|
|
EXPECT_THAT(w, HasSubstr("flag 'Contact': parent set, child unset,"
|
|
" keeping parent"));
|
|
EXPECT_THAT(w, HasSubstr("flag 'Actuation': parent unset, child set,"
|
|
" keeping parent"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictMergeMultipleWarnings) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
// merge mode: multiple min/max conflicts should be in one grouped warning
|
|
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->compiler.conflict = mjCONFLICT_MERGE;
|
|
parent->option.timestep = 0.005;
|
|
parent->option.iterations = 150;
|
|
parent->visual.global.realtime = 2.0f;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.timestep = 0.001; // min conflict
|
|
child->option.iterations = 200; // max conflict
|
|
child->visual.global.realtime = 0.5f; // min conflict
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
// values should be merged
|
|
EXPECT_EQ(parent->option.timestep, mjtNum(0.001));
|
|
EXPECT_EQ(parent->option.iterations, 200);
|
|
EXPECT_EQ(parent->visual.global.realtime, 0.5f);
|
|
|
|
// one grouped warning containing all three fields
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
std::string w = mjs_getWarning(parent, 0);
|
|
EXPECT_THAT(w, HasSubstr("policy is 'merge'"));
|
|
EXPECT_THAT(w, HasSubstr("timestep: parent has 0.005, child has 0.001,"
|
|
" taking the minimum"));
|
|
EXPECT_THAT(w, HasSubstr("iterations: parent has 150, child has 200,"
|
|
" taking the maximum"));
|
|
EXPECT_THAT(w, HasSubstr("realtime"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictWarningBitfieldsNoConflict) {
|
|
// warning mode: one side has zero disableflags -> no conflict
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->option.disableflags = mjDSBL_GRAVITY;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
// child disableflags is 0 (default) -> no conflict
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
EXPECT_FALSE(mjs_isWarning(parent));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictMergeBitfields) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
// merge mode: flags are ORed together
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->compiler.conflict = mjCONFLICT_MERGE;
|
|
parent->option.disableflags = mjDSBL_GRAVITY | mjDSBL_CONTACT;
|
|
parent->option.enableflags = mjENBL_OVERRIDE;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.disableflags = mjDSBL_GRAVITY | mjDSBL_ACTUATION;
|
|
child->option.enableflags = mjENBL_ENERGY;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
// union of flags
|
|
EXPECT_EQ(parent->option.disableflags,
|
|
mjDSBL_GRAVITY | mjDSBL_CONTACT | mjDSBL_ACTUATION);
|
|
EXPECT_EQ(parent->option.enableflags, mjENBL_OVERRIDE | mjENBL_ENERGY);
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictErrorBitfields) {
|
|
// error mode: differing flags -> per-flag error
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->compiler.conflict = mjCONFLICT_ERROR;
|
|
parent->option.disableflags = mjDSBL_GRAVITY;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.disableflags = mjDSBL_CONTACT;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
|
|
EXPECT_THAT(attached, IsNull());
|
|
// should mention the specific flag name, not "disableflags"
|
|
std::string error = mjs_getError(parent);
|
|
EXPECT_THAT(error, HasSubstr("flag 'Contact'"));
|
|
EXPECT_THAT(error, HasSubstr("flag 'Gravity'"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictErrorNoMutation) {
|
|
// failed attach should not mutate the parent spec
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->compiler.conflict = mjCONFLICT_MERGE;
|
|
parent->option.timestep = 0.005;
|
|
parent->option.iterations = 150;
|
|
parent->option.integrator = mjINT_RK4;
|
|
parent->option.gravity[2] = -10.0;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.timestep = 0.001; // min-mergeable
|
|
child->option.iterations = 200; // max-mergeable
|
|
child->option.integrator = mjINT_IMPLICIT; // unmergeable -> error
|
|
child->option.gravity[2] = -1.62; // unmergeable array -> error
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
|
|
// attach should fail
|
|
EXPECT_THAT(attached, IsNull());
|
|
|
|
// parent should be completely unchanged
|
|
EXPECT_EQ(parent->option.timestep, mjtNum(0.005));
|
|
EXPECT_EQ(parent->option.iterations, 150);
|
|
EXPECT_EQ(parent->option.integrator, mjINT_RK4);
|
|
EXPECT_EQ(parent->option.gravity[2], mjtNum(-10.0));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictMultipleErrors) {
|
|
// error mode: multiple conflicts should all be reported in the error message
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->compiler.conflict = mjCONFLICT_ERROR;
|
|
parent->option.timestep = 0.005;
|
|
parent->option.iterations = 150;
|
|
parent->option.gravity[2] = -10.0;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.timestep = 0.001;
|
|
child->option.iterations = 200;
|
|
child->option.gravity[2] = -1.62;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
|
|
EXPECT_THAT(attached, IsNull());
|
|
// all three conflicting fields should appear in the error message
|
|
std::string error = mjs_getError(parent);
|
|
EXPECT_THAT(error, HasSubstr("timestep"));
|
|
EXPECT_THAT(error, HasSubstr("iterations"));
|
|
EXPECT_THAT(error, HasSubstr("gravity"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictWarningArray) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
// warning mode: conflicting array -> keep parent, warn
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->option.gravity[2] = -10.0;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.gravity[2] = -1.62;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
// parent gravity should be unchanged
|
|
EXPECT_EQ(parent->option.gravity[2], mjtNum(-10.0));
|
|
|
|
// one grouped warning containing gravity
|
|
EXPECT_TRUE(mjs_isWarning(parent));
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
EXPECT_THAT(mjs_getWarning(parent, 0),
|
|
HasSubstr("gravity: parent has 0 0 -10, child has 0 0 -1.62,"
|
|
" keeping parent value"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictSubjectNames) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
|
|
// Test case 1: both custom names
|
|
{
|
|
mjSpec* parent = mj_makeSpec();
|
|
mjs_setString(parent->modelname, "parent_model");
|
|
parent->option.timestep = 0.005;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
mjs_setString(child->modelname, "child_model");
|
|
child->option.timestep = 0.001;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
std::string w = mjs_getWarning(parent, 0);
|
|
EXPECT_THAT(w, HasSubstr("Attach conflict when attaching 'child_model' to "
|
|
"'parent_model', policy is 'warning'"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
// Test case 2: only child custom name
|
|
{
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->option.timestep = 0.005;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
mjs_setString(child->modelname, "child_model");
|
|
child->option.timestep = 0.001;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
std::string w = mjs_getWarning(parent, 0);
|
|
EXPECT_THAT(w, HasSubstr("Attach conflict when attaching 'child_model', "
|
|
"policy is 'warning'"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
// Test case 3: only parent custom name
|
|
{
|
|
mjSpec* parent = mj_makeSpec();
|
|
mjs_setString(parent->modelname, "parent_model");
|
|
parent->option.timestep = 0.005;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.timestep = 0.001;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
std::string w = mjs_getWarning(parent, 0);
|
|
EXPECT_THAT(w, HasSubstr("Attach conflict when attaching to 'parent_model',"
|
|
" policy is 'warning'"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
// Test case 4: no custom name (both "MuJoCo Model")
|
|
{
|
|
mjSpec* parent = mj_makeSpec();
|
|
parent->option.timestep = 0.005;
|
|
|
|
mjSpec* child = mj_makeSpec();
|
|
child->option.timestep = 0.001;
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached = mjs_attach(world->element, child->element, "", "");
|
|
ASSERT_THAT(attached, NotNull());
|
|
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
std::string w = mjs_getWarning(parent, 0);
|
|
EXPECT_THAT(w, HasSubstr("Attach conflict on attach, "
|
|
"policy is 'warning'"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictAuthoredDefault) {
|
|
// child explicitly sets timestep to its default value (0.002) in XML,
|
|
// which should still conflict with a non-default parent timestep
|
|
static constexpr char parent_xml[] = R"(
|
|
<mujoco>
|
|
<compiler conflict="error"/>
|
|
<option timestep="0.005"/>
|
|
<worldbody>
|
|
<body name="parent_body"><geom size="1"/></body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char child_xml[] = R"(
|
|
<mujoco>
|
|
<option timestep="0.002"/>
|
|
<worldbody>
|
|
<body name="child_body"><geom size="1"/></body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
std::array<char, 1024> err;
|
|
mjSpec* parent =
|
|
mj_parseXMLString(parent_xml, nullptr, err.data(), err.size());
|
|
ASSERT_THAT(parent, NotNull()) << err.data();
|
|
mjSpec* child = mj_parseXMLString(child_xml, nullptr, err.data(), err.size());
|
|
ASSERT_THAT(child, NotNull()) << err.data();
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached =
|
|
mjs_attach(world->element, child->element, "child_", "");
|
|
|
|
// attach should fail: child authored timestep=0.002 (the default),
|
|
// but parent has timestep=0.005 — authored tracking catches this
|
|
EXPECT_THAT(attached, IsNull());
|
|
EXPECT_THAT(mjs_getError(parent), HasSubstr("timestep"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictNonAuthoredDefault) {
|
|
// child does NOT set timestep in XML, parent has non-default timestep.
|
|
// no conflict should be detected (child didn't author the field)
|
|
static constexpr char parent_xml[] = R"(
|
|
<mujoco>
|
|
<compiler conflict="error"/>
|
|
<option timestep="0.005"/>
|
|
<worldbody>
|
|
<body name="parent_body"><geom size="1"/></body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char child_xml[] = R"(
|
|
<mujoco>
|
|
<worldbody>
|
|
<body name="child_body"><geom size="1"/></body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
std::array<char, 1024> err;
|
|
mjSpec* parent =
|
|
mj_parseXMLString(parent_xml, nullptr, err.data(), err.size());
|
|
ASSERT_THAT(parent, NotNull()) << err.data();
|
|
mjSpec* child = mj_parseXMLString(child_xml, nullptr, err.data(), err.size());
|
|
ASSERT_THAT(child, NotNull()) << err.data();
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached =
|
|
mjs_attach(world->element, child->element, "child_", "");
|
|
|
|
// attach should succeed: child didn't author timestep
|
|
EXPECT_THAT(attached, NotNull()) << mjs_getError(parent);
|
|
// parent timestep should be unchanged
|
|
EXPECT_EQ(parent->option.timestep, mjtNum(0.005));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictXMLMergeAuthoredDefault) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
|
|
static constexpr char parent_xml[] = R"(
|
|
<mujoco>
|
|
<compiler conflict="merge"/>
|
|
<option timestep="0.005"/>
|
|
<worldbody/>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char child_xml[] = R"(
|
|
<mujoco>
|
|
<option timestep="0.002"/>
|
|
<worldbody/>
|
|
</mujoco>
|
|
)";
|
|
|
|
std::array<char, 1024> error;
|
|
mjSpec* parent =
|
|
mj_parseXMLString(parent_xml, nullptr, error.data(), error.size());
|
|
ASSERT_THAT(parent, NotNull()) << error.data();
|
|
|
|
mjSpec* child =
|
|
mj_parseXMLString(child_xml, nullptr, error.data(), error.size());
|
|
ASSERT_THAT(child, NotNull()) << error.data();
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached =
|
|
mjs_attach(world->element, child->element, "child_", "");
|
|
ASSERT_THAT(attached, NotNull()) << "Error details: " << mjs_getError(parent);
|
|
|
|
// Child explicitly authored timestep=0.002. Parent has timestep=0.005. So
|
|
// they conflict on both-authored. Under merge mode, min-merge applies ->
|
|
// child wins.
|
|
EXPECT_EQ(parent->option.timestep, mjtNum(0.002));
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
EXPECT_THAT(mjs_getWarning(parent, 0),
|
|
HasSubstr("timestep: parent has 0.005, child has 0.002, "
|
|
"taking the minimum"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictXMLBitfieldSilentAdoption) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
|
|
static constexpr char parent_xml[] = R"(
|
|
<mujoco>
|
|
<compiler conflict="merge"/>
|
|
<worldbody/>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char child_xml[] = R"(
|
|
<mujoco>
|
|
<option>
|
|
<flag constraint="disable"/>
|
|
</option>
|
|
<worldbody/>
|
|
</mujoco>
|
|
)";
|
|
|
|
std::array<char, 1024> error;
|
|
mjSpec* parent =
|
|
mj_parseXMLString(parent_xml, nullptr, error.data(), error.size());
|
|
ASSERT_THAT(parent, NotNull()) << error.data();
|
|
|
|
mjSpec* child =
|
|
mj_parseXMLString(child_xml, nullptr, error.data(), error.size());
|
|
ASSERT_THAT(child, NotNull()) << error.data();
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached =
|
|
mjs_attach(world->element, child->element, "child_", "");
|
|
ASSERT_THAT(attached, NotNull()) << "Error details: " << mjs_getError(parent);
|
|
|
|
// Parent should adopt child's constraint disable flag since parent didn't
|
|
// restrict flags. In merge mode, this logging counts as 1 warning.
|
|
EXPECT_EQ(parent->option.disableflags, mjDSBL_CONSTRAINT);
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
EXPECT_THAT(mjs_getWarning(parent, 0),
|
|
HasSubstr("flag 'Constraint': added from child"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachWarningBoundaryAndPreservation) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
|
|
// 1. Build a parent XML that generates a compile warning
|
|
static constexpr char parent_xml[] = R"(
|
|
<mujoco>
|
|
<compiler conflict="warning"/>
|
|
<option timestep="0.005"/>
|
|
<worldbody>
|
|
<body name="parent">
|
|
<flexcomp name="grid" type="grid" count="3 3 1" spacing="0.1 0.1 0.1"
|
|
dim="2" radius="0.01">
|
|
<contact internal="false"/>
|
|
</flexcomp>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
)";
|
|
|
|
// 2. Child 1 has conflicting timestep (0.001)
|
|
static constexpr char child1_xml[] = R"(
|
|
<mujoco>
|
|
<option timestep="0.001"/>
|
|
<worldbody/>
|
|
</mujoco>
|
|
)";
|
|
|
|
// 3. Child 2 has conflicting sleep_tolerance (0.005)
|
|
static constexpr char child2_xml[] = R"(
|
|
<mujoco>
|
|
<option sleep_tolerance="0.005"/>
|
|
<worldbody/>
|
|
</mujoco>
|
|
)";
|
|
|
|
std::array<char, 1024> error;
|
|
mjSpec* parent =
|
|
mj_parseXMLString(parent_xml, nullptr, error.data(), error.size());
|
|
ASSERT_THAT(parent, NotNull()) << error.data();
|
|
|
|
mjSpec* child1 =
|
|
mj_parseXMLString(child1_xml, nullptr, error.data(), error.size());
|
|
ASSERT_THAT(child1, NotNull()) << error.data();
|
|
|
|
mjSpec* child2 =
|
|
mj_parseXMLString(child2_xml, nullptr, error.data(), error.size());
|
|
ASSERT_THAT(child2, NotNull()) << error.data();
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
|
|
// Perform Attach 1
|
|
mjsElement* attached1 =
|
|
mjs_attach(world->element, child1->element, "c1_", "");
|
|
ASSERT_THAT(attached1, NotNull())
|
|
<< "Error details: " << mjs_getError(parent);
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
EXPECT_THAT(mjs_getWarning(parent, 0), HasSubstr("timestep"));
|
|
|
|
// Perform Attach 2
|
|
mjsElement* attached2 =
|
|
mjs_attach(world->element, child2->element, "c2_", "");
|
|
ASSERT_THAT(attached2, NotNull())
|
|
<< "Error details: " << mjs_getError(parent);
|
|
EXPECT_EQ(mjs_numWarnings(parent), 2);
|
|
EXPECT_THAT(mjs_getWarning(parent, 1), HasSubstr("sleep_tolerance"));
|
|
|
|
// Compile 1: compiles and generates a compile warning (flex is not rigid)
|
|
mjModel* model1 = mj_compile(parent, nullptr);
|
|
ASSERT_THAT(model1, NotNull());
|
|
|
|
// Warnings should include both attach warnings (indices 0 and 1) AND the
|
|
// compile warning (at index 2)
|
|
int num_warnings_after_compile = mjs_numWarnings(parent);
|
|
EXPECT_GE(num_warnings_after_compile, 3);
|
|
EXPECT_THAT(mjs_getWarning(parent, 2), HasSubstr("not rigid"));
|
|
|
|
// Compile 2 (Recompile): Recompiling should CLEAR compile warnings but
|
|
// PRESERVE all attach warnings!
|
|
mjModel* model2 = mj_compile(parent, nullptr);
|
|
ASSERT_THAT(model2, NotNull());
|
|
|
|
// Attach warnings must still be preserved and compile warnings refreshed
|
|
EXPECT_EQ(mjs_numWarnings(parent), num_warnings_after_compile);
|
|
EXPECT_THAT(mjs_getWarning(parent, 0), HasSubstr("timestep"));
|
|
EXPECT_THAT(mjs_getWarning(parent, 1), HasSubstr("sleep_tolerance"));
|
|
EXPECT_THAT(mjs_getWarning(parent, 2), HasSubstr("not rigid"));
|
|
|
|
mj_deleteModel(model1);
|
|
mj_deleteModel(model2);
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child1);
|
|
mj_deleteSpec(child2);
|
|
}
|
|
|
|
TEST_F(MujocoTest, AttachConflictWarningZFarDefaultMessage) {
|
|
mock_warning_handler.ExpectWarnings();
|
|
|
|
static constexpr char parent_xml[] = R"(
|
|
<mujoco>
|
|
<compiler conflict="warning"/>
|
|
<worldbody/>
|
|
</mujoco>
|
|
)";
|
|
|
|
static constexpr char child_xml[] = R"(
|
|
<mujoco>
|
|
<visual>
|
|
<map zfar="30"/>
|
|
</visual>
|
|
<worldbody/>
|
|
</mujoco>
|
|
)";
|
|
|
|
std::array<char, 1024> error;
|
|
mjSpec* parent =
|
|
mj_parseXMLString(parent_xml, nullptr, error.data(), error.size());
|
|
ASSERT_THAT(parent, NotNull()) << error.data();
|
|
|
|
mjSpec* child =
|
|
mj_parseXMLString(child_xml, nullptr, error.data(), error.size());
|
|
ASSERT_THAT(child, NotNull()) << error.data();
|
|
|
|
mjsBody* world = mjs_findBody(parent, "world");
|
|
mjsElement* attached =
|
|
mjs_attach(world->element, child->element, "child_", "");
|
|
ASSERT_THAT(attached, NotNull()) << "Error details: " << mjs_getError(parent);
|
|
|
|
// Since only the child authored 'zfar' and the parent relied on defaults,
|
|
// we warning-log and state that the parent has the default value.
|
|
EXPECT_TRUE(mjs_isWarning(parent));
|
|
EXPECT_EQ(mjs_numWarnings(parent), 1);
|
|
EXPECT_THAT(
|
|
mjs_getWarning(parent, 0),
|
|
HasSubstr("zfar: parent has 50 (default), child has "
|
|
"30, keeping parent value"));
|
|
|
|
mj_deleteSpec(parent);
|
|
mj_deleteSpec(child);
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace mujoco
|
|
|