diff --git a/test/user/CMakeLists.txt b/test/user/CMakeLists.txt index c2d3b69d..bc07f262 100644 --- a/test/user/CMakeLists.txt +++ b/test/user/CMakeLists.txt @@ -26,6 +26,13 @@ mujoco_test( "MUJOCO_PLUGIN_DIR=$" ) +mujoco_test( + user_recompile_test + PROPERTIES + ENVIRONMENT + "MUJOCO_PLUGIN_DIR=$" +) + mujoco_test( user_flex_test ) diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index 707f698b..b65b273f 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -14,13 +14,10 @@ // Tests for user/user_api.cc. -#include #include -#include #include #include #include -#include // NOLINT #include #include #include @@ -29,13 +26,11 @@ #include #include -#include #include "src/cc/array_safety.h" #include #include #include #include "src/xml/xml_api.h" -#include "src/xml/xml_numeric_format.h" #include "test/fixture.h" namespace mujoco { @@ -768,116 +763,6 @@ TEST_F(MujocoTest, ModifyShellInertiaFails) { mj_deleteModel(model); } -// ------------------- test recompilation multiple files ----------------------- - -std::vector GetRecompileTestModels() { - std::vector models; - std::string ext(".xml"); - for (const auto& path : {GetTestDataFilePath("."), GetModelPath(".")}) { - for (const auto& p : std::filesystem::recursive_directory_iterator(path)) { - if (p.path().extension() == ext) { - std::string xml = p.path().string(); - if (absl::StrContains(xml, "malformed_") || - absl::StrContains(xml, "_fail") || - absl::StrContains(xml, "touch_grid") || - absl::StrContains(xml, "perf") || - absl::StrContains(xml, "cow")) { - continue; - } - models.push_back(xml); - } - } - } - return models; -} - -class RecompileCompareTest : public MujocoTest, - public ::testing::WithParamInterface { - public: -}; -TEST_P(RecompileCompareTest, RecompileCompare) { - std::string xml = GetParam(); - std::string field = ""; - - FullFloatPrecision increase_precision; - - // load spec - std::array err; - mjSpec* s = mj_parseXML(xml.c_str(), 0, err.data(), err.size()); - - if (!s) { - GTEST_SKIP() << "Failed to load " << xml << ": " << err.data(); - } - - // copy spec - mjSpec* s_copy = mj_copySpec(s); - - // compare signature - EXPECT_EQ(s->element->signature, s_copy->element->signature) << xml; - - // compile twice and compare - mjModel* m_old = mj_compile(s, nullptr); - - if (!m_old) { - mj_deleteSpec(s); - GTEST_SKIP() << "Failed to compile " << xml << ": " << mjs_getError(s); - } - - mjModel* m_new = mj_compile(s, nullptr); - mjModel* m_copy = mj_compile(s_copy, nullptr); - - // compare signature - EXPECT_EQ(m_old->signature, m_new->signature) << xml; - EXPECT_EQ(m_old->signature, m_copy->signature) << xml; - - ASSERT_THAT(m_new, NotNull()) - << "Failed to recompile " << xml << ": " << mjs_getError(s); - ASSERT_THAT(m_copy, NotNull()) - << "Failed to compile " << xml << ": " << mjs_getError(s_copy); - - mjtNum tol = 0; - - EXPECT_LE(CompareModel(m_old, m_new, field), tol) - << "Compiled and recompiled models are different!\n" - << "Affected file " << xml << '\n' - << "Different field: " << field << '\n'; - - EXPECT_LE(CompareModel(m_old, m_copy, field), tol) - << "Original and copied models are different!\n" - << "Affected file " << xml << '\n' - << "Different field: " << field << '\n'; - - // copy to a new spec, compile and compare - mjSpec* s_copy2 = mj_copySpec(s); - mjModel* m_copy2 = mj_compile(s_copy2, nullptr); - - ASSERT_THAT(m_copy2, NotNull()) - << "Failed to compile " << xml << ": " << mjs_getError(s_copy2); - - EXPECT_LE(CompareModel(m_old, m_copy2, field), tol) - << "Original and re-copied models are different!\n" - << "Affected file " << xml << '\n' - << "Different field: " << field << '\n'; - - mj_deleteModel(m_new); - mj_deleteModel(m_copy); - mj_deleteModel(m_copy2); - mj_deleteSpec(s_copy); - mj_deleteSpec(s_copy2); - mj_deleteSpec(s); - mj_deleteModel(m_old); -} - -INSTANTIATE_TEST_SUITE_P( - AllModels, RecompileCompareTest, - ::testing::ValuesIn(GetRecompileTestModels()), - [](const ::testing::TestParamInfo& info) { - std::string name = std::filesystem::path(info.param).filename().string(); - std::replace_if( - name.begin(), name.end(), - [](char c) { return !std::isalnum(c); }, '_'); - return name + "_" + std::to_string(info.index); - }); TEST_F(MujocoTest, RecompileEdit) { static constexpr char xml[] = R"( diff --git a/test/user/user_recompile_test.cc b/test/user/user_recompile_test.cc new file mode 100644 index 00000000..2945c170 --- /dev/null +++ b/test/user/user_recompile_test.cc @@ -0,0 +1,149 @@ +// 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. + +// Tests for recompiling multiple files. + +#include +#include +#include +#include +#include // NOLINT +#include +#include + +#include +#include +#include +#include +#include +#include "src/xml/xml_api.h" +#include "src/xml/xml_numeric_format.h" +#include "test/fixture.h" + +namespace mujoco { +namespace { + +using ::testing::NotNull; + +std::vector GetRecompileTestModels() { + std::vector models; + std::string ext(".xml"); + for (const auto& path : {GetTestDataFilePath("."), GetModelPath(".")}) { + for (const auto& p : std::filesystem::recursive_directory_iterator(path)) { + if (p.path().extension() == ext) { + std::string xml = p.path().string(); + if (absl::StrContains(xml, "malformed_") || + absl::StrContains(xml, "_fail") || + absl::StrContains(xml, "touch_grid") || + absl::StrContains(xml, "perf") || + absl::StrContains(xml, "cow")) { + continue; + } + models.push_back(xml); + } + } + } + return models; +} + +class RecompileCompareTest : public MujocoTest, + public ::testing::WithParamInterface { + public: +}; +TEST_P(RecompileCompareTest, RecompileCompare) { + std::string xml = GetParam(); + std::string field = ""; + + FullFloatPrecision increase_precision; + + // load spec + std::array err; + mjSpec* s = mj_parseXML(xml.c_str(), 0, err.data(), err.size()); + + if (!s) { + GTEST_SKIP() << "Failed to load " << xml << ": " << err.data(); + } + + // copy spec + mjSpec* s_copy = mj_copySpec(s); + + // compare signature + EXPECT_EQ(s->element->signature, s_copy->element->signature) << xml; + + // compile twice and compare + mjModel* m_old = mj_compile(s, nullptr); + + if (!m_old) { + mj_deleteSpec(s); + GTEST_SKIP() << "Failed to compile " << xml << ": " << mjs_getError(s); + } + + mjModel* m_new = mj_compile(s, nullptr); + mjModel* m_copy = mj_compile(s_copy, nullptr); + + // compare signature + EXPECT_EQ(m_old->signature, m_new->signature) << xml; + EXPECT_EQ(m_old->signature, m_copy->signature) << xml; + + ASSERT_THAT(m_new, NotNull()) + << "Failed to recompile " << xml << ": " << mjs_getError(s); + ASSERT_THAT(m_copy, NotNull()) + << "Failed to compile " << xml << ": " << mjs_getError(s_copy); + + mjtNum tol = 0; + + EXPECT_LE(CompareModel(m_old, m_new, field), tol) + << "Compiled and recompiled models are different!\n" + << "Affected file " << xml << '\n' + << "Different field: " << field << '\n'; + + EXPECT_LE(CompareModel(m_old, m_copy, field), tol) + << "Original and copied models are different!\n" + << "Affected file " << xml << '\n' + << "Different field: " << field << '\n'; + + // copy to a new spec, compile and compare + mjSpec* s_copy2 = mj_copySpec(s); + mjModel* m_copy2 = mj_compile(s_copy2, nullptr); + + ASSERT_THAT(m_copy2, NotNull()) + << "Failed to compile " << xml << ": " << mjs_getError(s_copy2); + + EXPECT_LE(CompareModel(m_old, m_copy2, field), tol) + << "Original and re-copied models are different!\n" + << "Affected file " << xml << '\n' + << "Different field: " << field << '\n'; + + mj_deleteModel(m_new); + mj_deleteModel(m_copy); + mj_deleteModel(m_copy2); + mj_deleteSpec(s_copy); + mj_deleteSpec(s_copy2); + mj_deleteSpec(s); + mj_deleteModel(m_old); +} + +INSTANTIATE_TEST_SUITE_P( + AllModels, RecompileCompareTest, + ::testing::ValuesIn(GetRecompileTestModels()), + [](const ::testing::TestParamInfo& info) { + std::string name = std::filesystem::path(info.param).filename().string(); + std::replace_if( + name.begin(), name.end(), + [](char c) { return !std::isalnum(c); }, '_'); + return name + "_" + std::to_string(info.index); + }); + +} // namespace +} // namespace mujoco diff --git a/test/xml/CMakeLists.txt b/test/xml/CMakeLists.txt index d4cefdb1..920e6933 100644 --- a/test/xml/CMakeLists.txt +++ b/test/xml/CMakeLists.txt @@ -28,3 +28,12 @@ mujoco_test( ENVIRONMENT "MUJOCO_PLUGIN_DIR=$" ) + +mujoco_test( + xml_write_read_test + ADDITIONAL_LINK_LIBRARIES + absl::flat_hash_set + PROPERTIES + ENVIRONMENT + "MUJOCO_PLUGIN_DIR=$" +) diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index afa8215b..c8e2386e 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -14,7 +14,6 @@ // Tests for xml/xml_native_writer.cc. -#include #include #if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) #include @@ -23,13 +22,10 @@ #include #include #include -#include // NOLINT(build/c++17) #include -#include #include #include -#include #include #include #include @@ -1376,153 +1372,6 @@ TEST_F(XMLWriterTest, NonRGBTextures) { mj_deleteModel(model); } -// ------------------- test loading and saving multiple files ------------------ -std::vector GetWriteReadTestModels() { - std::vector models; - std::string ext(".xml"); - for (const auto& path : {GetTestDataFilePath("."), GetModelPath(".")}) { - for (const auto& p : std::filesystem::recursive_directory_iterator(path)) { - if (p.path().extension() == ext) { - std::string xml = p.path().string(); - if ( // if file is meant to fail, skip it - absl::StrContains(xml, "malformed_") || - absl::StrContains(xml, "_fail") || - // exclude files that are too slow to load - absl::StrContains(xml, "cow") || - absl::StrContains(xml, "gmsh_") || - absl::StrContains(xml, "shark_") || - absl::StrContains(xml, "perf") || - // exclude files that fail the comparison test - absl::StrContains(xml, "rfcamera") || - absl::StrContains(xml, "tactile") || - absl::StrContains(xml, "makemesh") || - absl::StrContains(xml, "many_dependencies") || - absl::StrContains(xml, "usd") || - absl::StrContains(xml, "torus_maxhull") || - absl::StrContains(xml, "fitmesh_") || - absl::StrContains(xml, "lengthrange") || - absl::StrContains(xml, "hfield_xml") || - absl::StrContains(xml, "fromto_convex") || - absl::StrContains(xml, "cube_skin") || - absl::StrContains(xml, "cube_3x3x3") || - // exclude files that fail since we do not save pinned flex nodes - absl::StrContains(xml, "gripper_trilinear") || - absl::StrContains(xml, "strain")) { - continue; - } - models.push_back(xml); - } - } - } - return models; -} - -class WriteReadCompareTest : public XMLWriterTest, - public ::testing::WithParamInterface { - public: -}; -TEST_P(WriteReadCompareTest, WriteReadCompare) { - std::string xml = GetParam(); - - // full precision float printing - FullFloatPrecision increase_precision; - - // load model - std::array error; - mjSpec* s = - mj_parseXML(xml.c_str(), nullptr, error.data(), error.size()); - if (!s) { - GTEST_SKIP() << "Failed to load " << xml.c_str() << ": " << error.data(); - } - - mjModel* m = mj_compile(s, nullptr); - if (!m) { - mj_deleteSpec(s); - GTEST_SKIP() << "Failed to compile " << xml.c_str() << ": " << error.data(); - } - - // make data - mjData* d = mj_makeData(m); - ASSERT_THAT(d, testing::NotNull()) << "Failed to create data\n"; - - // save and load back - auto abs_path = std::filesystem::path(xml); - mjSpec* stemp = mj_parseXMLString(SaveAndReadXml(s).c_str(), 0, error.data(), - error.size()); - ASSERT_THAT(stemp, NotNull()) - << "Failed to load " << xml.c_str() << ": " << error.data(); - mjs_setString(stemp->modelfiledir, - abs_path.remove_filename().string().c_str()); - mjModel* mtemp = mj_compile(stemp, nullptr); - - ASSERT_THAT(mtemp, NotNull()) << error.data() << " from " << xml.c_str(); - - mjtNum tol = 0; - - // for particularly sensitive models, relax the tolerance - if (absl::StrContains(xml, "belt.xml") || - absl::StrContains(xml, "cable.xml")) { - tol = 1e-13; - } - - // compare and delete - std::string field = ""; - mjtNum result = CompareModel(m, mtemp, field); - EXPECT_LE(result, tol) << "Loaded and saved models are different!\n" - << "Affected file " << xml << '\n' - << "Different field: " << field << '\n'; - mj_deleteModel(mtemp); - - // check for stack memory leak - mj_step(m, d); - EXPECT_EQ(d->pstack, 0) << "mjData stack memory leak detected in " << - xml << '\n'; - - // delete data - mj_deleteData(d); - - // allocate buffer, save m into it - size_t sz = mj_sizeModel(m); - void* buffer = mju_malloc(sz); - mj_saveModel(m, nullptr, buffer, sz); - - // make new VFS add buffer to it - mjVFS* vfs = (mjVFS*)mju_malloc(sizeof(mjVFS)); - mj_defaultVFS(vfs); - int failed = mj_addBufferVFS(vfs, "model.mjb", buffer, sz); - EXPECT_EQ(failed, 0) << "Failed to add buffer to VFS"; - - // load model from VFS - mtemp = mj_loadModel("model.mjb", vfs); - ASSERT_THAT(mtemp, NotNull()); - - // compare with 0 tolerance - field = ""; - result = CompareModel(m, mtemp, field); - EXPECT_EQ(result, 0) << "Loaded and saved binary models are different!\n" - << "Affected file " << xml << '\n' - << "Different field: " << field << '\n'; - - // clean up - mj_deleteSpec(s); - mj_deleteSpec(stemp); - mj_deleteModel(m); - mj_deleteModel(mtemp); - mj_deleteVFS(vfs); - mju_free(vfs); - mju_free(buffer); -} - -INSTANTIATE_TEST_SUITE_P( - AllModels, WriteReadCompareTest, - ::testing::ValuesIn(GetWriteReadTestModels()), - [](const ::testing::TestParamInfo& info) { - std::string name = std::filesystem::path(info.param).filename().string(); - std::replace_if( - name.begin(), name.end(), - [](char c) { return !std::isalnum(c); }, '_'); - return name + "_" + std::to_string(info.index); - }); // ---------------- test CopyBack functionality (decompiler) ------------------ using DecompilerTest = MujocoTest; diff --git a/test/xml/xml_write_read_test.cc b/test/xml/xml_write_read_test.cc new file mode 100644 index 00000000..1a1e1255 --- /dev/null +++ b/test/xml/xml_write_read_test.cc @@ -0,0 +1,187 @@ +// 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. + +// Tests for loading and saving multiple files. + +#include +#include +#include +#include +#include // NOLINT(build/c++17) +#include +#include + +#include +#include +#include +#include +#include +#include "src/xml/xml_numeric_format.h" +#include "test/fixture.h" + +namespace mujoco { +namespace { + +using ::testing::NotNull; +using XMLWriterTest = MujocoTest; + +std::vector GetWriteReadTestModels() { + std::vector models; + std::string ext(".xml"); + for (const auto& path : {GetTestDataFilePath("."), GetModelPath(".")}) { + for (const auto& p : std::filesystem::recursive_directory_iterator(path)) { + if (p.path().extension() == ext) { + std::string xml = p.path().string(); + if ( // if file is meant to fail, skip it + absl::StrContains(xml, "malformed_") || + absl::StrContains(xml, "_fail") || + // exclude files that are too slow to load + absl::StrContains(xml, "cow") || + absl::StrContains(xml, "gmsh_") || + absl::StrContains(xml, "shark_") || + absl::StrContains(xml, "perf") || + // exclude files that fail the comparison test + absl::StrContains(xml, "rfcamera") || + absl::StrContains(xml, "tactile") || + absl::StrContains(xml, "makemesh") || + absl::StrContains(xml, "many_dependencies") || + absl::StrContains(xml, "usd") || + absl::StrContains(xml, "torus_maxhull") || + absl::StrContains(xml, "fitmesh_") || + absl::StrContains(xml, "lengthrange") || + absl::StrContains(xml, "hfield_xml") || + absl::StrContains(xml, "fromto_convex") || + absl::StrContains(xml, "cube_skin") || + absl::StrContains(xml, "cube_3x3x3") || + // exclude files that fail since we do not save pinned flex nodes + absl::StrContains(xml, "gripper_trilinear") || + absl::StrContains(xml, "strain")) { + continue; + } + models.push_back(xml); + } + } + } + return models; +} + +class WriteReadCompareTest : public XMLWriterTest, + public ::testing::WithParamInterface { + public: +}; +TEST_P(WriteReadCompareTest, WriteReadCompare) { + std::string xml = GetParam(); + + // full precision float printing + FullFloatPrecision increase_precision; + + // load model + std::array error; + mjSpec* s = + mj_parseXML(xml.c_str(), nullptr, error.data(), error.size()); + if (!s) { + GTEST_SKIP() << "Failed to load " << xml.c_str() << ": " << error.data(); + } + + mjModel* m = mj_compile(s, nullptr); + if (!m) { + mj_deleteSpec(s); + GTEST_SKIP() << "Failed to compile " << xml.c_str() << ": " << error.data(); + } + + // make data + mjData* d = mj_makeData(m); + ASSERT_THAT(d, testing::NotNull()) << "Failed to create data\n"; + + // save and load back + auto abs_path = std::filesystem::path(xml); + mjSpec* stemp = mj_parseXMLString(SaveAndReadXml(s).c_str(), 0, error.data(), + error.size()); + ASSERT_THAT(stemp, NotNull()) + << "Failed to load " << xml.c_str() << ": " << error.data(); + mjs_setString(stemp->modelfiledir, + abs_path.remove_filename().string().c_str()); + mjModel* mtemp = mj_compile(stemp, nullptr); + + ASSERT_THAT(mtemp, NotNull()) << error.data() << " from " << xml.c_str(); + + mjtNum tol = 0; + + // for particularly sensitive models, relax the tolerance + if (absl::StrContains(xml, "belt.xml") || + absl::StrContains(xml, "cable.xml")) { + tol = 1e-13; + } + + // compare and delete + std::string field = ""; + mjtNum result = CompareModel(m, mtemp, field); + EXPECT_LE(result, tol) << "Loaded and saved models are different!\n" + << "Affected file " << xml << '\n' + << "Different field: " << field << '\n'; + mj_deleteModel(mtemp); + + // check for stack memory leak + mj_step(m, d); + EXPECT_EQ(d->pstack, 0) << "mjData stack memory leak detected in " << + xml << '\n'; + + // delete data + mj_deleteData(d); + + // allocate buffer, save m into it + size_t sz = mj_sizeModel(m); + void* buffer = mju_malloc(sz); + mj_saveModel(m, nullptr, buffer, sz); + + // make new VFS add buffer to it + mjVFS* vfs = (mjVFS*)mju_malloc(sizeof(mjVFS)); + mj_defaultVFS(vfs); + int failed = mj_addBufferVFS(vfs, "model.mjb", buffer, sz); + EXPECT_EQ(failed, 0) << "Failed to add buffer to VFS"; + + // load model from VFS + mtemp = mj_loadModel("model.mjb", vfs); + ASSERT_THAT(mtemp, NotNull()); + + // compare with 0 tolerance + field = ""; + result = CompareModel(m, mtemp, field); + EXPECT_EQ(result, 0) << "Loaded and saved binary models are different!\n" + << "Affected file " << xml << '\n' + << "Different field: " << field << '\n'; + + // clean up + mj_deleteSpec(s); + mj_deleteSpec(stemp); + mj_deleteModel(m); + mj_deleteModel(mtemp); + mj_deleteVFS(vfs); + mju_free(vfs); + mju_free(buffer); +} + +INSTANTIATE_TEST_SUITE_P( + AllModels, WriteReadCompareTest, + ::testing::ValuesIn(GetWriteReadTestModels()), + [](const ::testing::TestParamInfo& info) { + std::string name = std::filesystem::path(info.param).filename().string(); + std::replace_if( + name.begin(), name.end(), + [](char c) { return !std::isalnum(c); }, '_'); + return name + "_" + std::to_string(info.index); + }); + +} // namespace +} // namespace mujoco