Add policies for global attribute conflict resolution upon attach

For example when loading parent_merge.xml:

```
WARNING: Attach conflict when attaching 'child' to 'parent_merge', policy is 'merge'
timestep: parent has 0.005, child has 0.002, taking the minimum
iterations: parent has 50, child has 100, taking the maximum
flag 'Damper': added from child
```

When loading parent_error.xml:

```
XML Error: Attach conflict when attaching 'child' to 'parent_error', policy is 'error'
timestep: parent has 0.005, child has 0.002
iterations: parent has 50, child has 100
Element 'attach', line 10
```
PiperOrigin-RevId: 933620810
Change-Id: Ib477863b5ef763474d27fb4be5a4148be1d5d500
This commit is contained in:
Yuval Tassa
2026-06-17 03:34:10 -07:00
committed by Copybara-Service
parent 30357c38a3
commit 410c73168c
37 changed files with 2705 additions and 449 deletions
+75
View File
@@ -3897,5 +3897,80 @@ TEST_F(ActuatorParseTest, DampingArmatureDefaultsPropagate) {
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, AttachConflictXMLWarning) {
mock_warning_handler.ExpectWarnings();
std::array<char, 1024> error;
std::string path = GetTestDataFilePath("xml/testdata/parent_warn.xml");
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
ASSERT_THAT(spec, NotNull()) << error.data();
// one grouped warning per attach, containing gravity and damper flag
// (constraint is only authored by child, not a conflict)
EXPECT_EQ(mjs_numWarnings(spec), 1);
std::string w = mjs_getWarning(spec, 0);
EXPECT_THAT(w, HasSubstr("gravity: parent has 0 0 -10, child has 0 0 0,"
" keeping parent value"));
EXPECT_THAT(w, HasSubstr("flag 'Damper'"));
mjModel* m = mj_compile(spec, nullptr);
ASSERT_THAT(m, NotNull());
// Gravity should be parent's value (0 0 -10)
EXPECT_MJTNUM_EQ(m->opt.gravity[0], 0);
EXPECT_MJTNUM_EQ(m->opt.gravity[1], 0);
EXPECT_MJTNUM_EQ(m->opt.gravity[2], -10);
mj_deleteModel(m);
mj_deleteSpec(spec);
}
TEST_F(XMLReaderTest, AttachConflictXMLMerge) {
mock_warning_handler.ExpectWarnings();
std::array<char, 1024> error;
std::string path = GetTestDataFilePath("xml/testdata/parent_merge.xml");
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
ASSERT_THAT(spec, NotNull()) << error.data();
// one grouped warning per attach, containing timestep, iterations, and flag
EXPECT_GE(mjs_numWarnings(spec), 1);
std::string w = mjs_getWarning(spec, 0);
EXPECT_THAT(w, HasSubstr("timestep: parent has 0.005, child has 0.002,"
" taking the minimum"));
EXPECT_THAT(w, HasSubstr("iterations: parent has 50, child has 100,"
" taking the maximum"));
mjModel* m = mj_compile(spec, nullptr);
ASSERT_THAT(m, NotNull());
// Timestep should be min (0.002)
EXPECT_MJTNUM_EQ(m->opt.timestep, 0.002);
// Iterations should be max (100)
EXPECT_EQ(m->opt.iterations, 100);
mj_deleteModel(m);
mj_deleteSpec(spec);
}
TEST_F(XMLReaderTest, AttachConflictXMLError) {
std::array<char, 1024> error;
std::string path = GetTestDataFilePath("xml/testdata/parent_error.xml");
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
// Should fail to parse because of conflict in timestep in error mode
EXPECT_THAT(spec, IsNull());
EXPECT_THAT(error.data(),
HasSubstr("timestep: parent has 0.005, child has 0.002"));
}
TEST_F(XMLReaderTest, AttachConflictXMLMergeUnmergableError) {
std::array<char, 1024> error;
std::string path =
GetTestDataFilePath("xml/testdata/parent_merge_unmergable.xml");
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
// Should fail to parse because gravity is unmergeable in merge mode
EXPECT_THAT(spec, IsNull());
EXPECT_THAT(error.data(),
HasSubstr("gravity: parent has 0 0 -10, child has 0 0 0"));
}
} // namespace
} // namespace mujoco