Refactor mjVFS and resource management.

Resource operations (e.g. mju_openResource, mju_readResource, and
mju_closeResource, etc.) are now all handled by a VFS instance. It
is now up to the VFS to determine which provider to use in order to
handle those operations.

This allows us to dynamically add/remove (aka "mount") providers to
a VFS to handle special requests. mj_addFileVFS and mj_addBufferVFS
have been reimplemented as two such use-cases. Moreover, we expose
the mounting behaviour with two new functions: mj_mountVFS and
mj_unmountVFS.

PiperOrigin-RevId: 861550939
Change-Id: I070eb4bcc2982466c8f368f7918005538baa5185
This commit is contained in:
Haroon Qureshi
2026-01-26 23:52:35 -08:00
committed by Copybara-Service
parent e977b0d660
commit 0ddbb46fa1
12 changed files with 829 additions and 433 deletions
+1 -1
View File
@@ -301,7 +301,7 @@ TEST_F(ResourceTest, GeneralFailureTest) {
error.data(), error.size());
ASSERT_THAT(resource, IsNull());
EXPECT_THAT(error.data(), HasSubstr("could not open"));
EXPECT_THAT(error.data(), HasSubstr("Error opening file"));
}
TEST_F(ResourceTest, NameWithValidPrefix) {
+162
View File
@@ -26,6 +26,7 @@
namespace mujoco {
namespace {
using ::testing::IsNull;
using ::testing::NotNull;
using UserVfsTest = MujocoTest;
@@ -36,6 +37,46 @@ static bool HasFile(const mjVFS* vfs, const std::string& filename) {
return result;
}
struct TestProvider : public mjpResourceProvider {
// The TestProvider will increment the value at `addr` by these amounts when
// each callback is invoked. This can be used to verity that the correct
// callbacks are being invoked when expected.
enum CallbackValues {
Mounted = 100,
Unmounted = 200,
Opened = 300,
Read = 400,
Closed = 500,
};
explicit TestProvider(int* addr) {
mjp_defaultResourceProvider(this);
prefix = "test";
data = addr;
mount = [](mjResource* res) {
*(int*)res->provider->data += Mounted;
return 1;
};
unmount = [](mjResource* res) {
*(int*)res->provider->data += Unmounted;
return 1;
};
open = [](mjResource* res) {
*(int*)res->provider->data += Opened;
return 1;
};
read = [](mjResource* res, const void** out) {
*(int*)res->provider->data += Read;
*out = res->provider->data;
return (int)sizeof(int);
};
close = [](mjResource* res) {
*(int*)res->provider->data += Closed;
};
}
};
TEST_F(UserVfsTest, AddFile) {
constexpr char path[] = "engine/testdata/actuation/";
const std::string dir = GetTestDataFilePath(path);
@@ -245,5 +286,126 @@ TEST_F(UserVfsTest, Timestamps) {
mj_deleteVFS(&vfs);
}
TEST_F(UserVfsTest, MountUnmount) {
int test = 0;
int expect = 0;
TestProvider provider(&test);
mjVFS vfs;
mj_defaultVFS(&vfs);
mjResource* res = mju_openResource("", "/some/path/foo", &vfs, nullptr, 0);
EXPECT_THAT(res, IsNull());
EXPECT_EQ(test, expect);
mj_mountVFS(&vfs, "/some/path", &provider);
expect += TestProvider::Mounted;
EXPECT_EQ(test, expect);
res = mju_openResource("", "/some/path/foo", &vfs, nullptr, 0);
expect += TestProvider::Opened;
EXPECT_THAT(res, NotNull());
EXPECT_EQ(test, expect);
const void* buffer = nullptr;
const int size = mju_readResource(res, &buffer);
expect += TestProvider::Read;
EXPECT_THAT(buffer, NotNull());
EXPECT_GT(size, 0);
EXPECT_EQ(test, expect);
mju_closeResource(res);
expect += TestProvider::Closed;
EXPECT_EQ(test, expect);
mj_unmountVFS(&vfs, "/some/path");
expect += TestProvider::Unmounted;
EXPECT_EQ(test, expect);
mj_deleteVFS(&vfs);
}
TEST_F(UserVfsTest, AutoMountProviders) {
int test = 0;
int expect = 0;
TestProvider provider(&test);
mjp_registerResourceProvider(&provider);
mjVFS vfs;
mj_defaultVFS(&vfs);
mjResource* res = mju_openResource("", "test:foo", &vfs, nullptr, 0);
expect += TestProvider::Mounted + TestProvider::Opened;
EXPECT_THAT(res, NotNull());
mju_closeResource(res);
expect += TestProvider::Closed;
EXPECT_EQ(test, expect);
mj_deleteVFS(&vfs);
expect += TestProvider::Unmounted;
EXPECT_EQ(test, expect);
}
TEST_F(UserVfsTest, StackedMounts) {
int test1 = 0;
int test2 = 1000;
int test3 = 1000000;
int expect1 = test1;
int expect2 = test2;
int expect3 = test3;
TestProvider provider1(&test1);
TestProvider provider2(&test2);
TestProvider provider3(&test3);
mjVFS vfs;
mj_defaultVFS(&vfs);
mj_mountVFS(&vfs, "/some/path", &provider1);
mj_mountVFS(&vfs, "/some/path/further/down/very/deep", &provider2);
mj_mountVFS(&vfs, "/some/path/further/down", &provider3);
expect1 += TestProvider::Mounted;
expect2 += TestProvider::Mounted;
expect3 += TestProvider::Mounted;
EXPECT_EQ(test1, expect1);
EXPECT_EQ(test2, expect2);
EXPECT_EQ(test3, expect3);
mjResource* res1 = mju_openResource("", "/some/path/foo", &vfs, nullptr, 0);
expect1 += TestProvider::Opened;
EXPECT_EQ(test1, expect1);
EXPECT_EQ(test2, expect2);
EXPECT_EQ(test3, expect3);
mjResource* res2 =
mju_openResource("", "/some/path/further/down/foo", &vfs, nullptr, 0);
expect3 += TestProvider::Opened;
EXPECT_EQ(test1, expect1);
EXPECT_EQ(test2, expect2);
EXPECT_EQ(test3, expect3);
mjResource* res3 = mju_openResource(
"", "/some/path/further/down/very/deep/foo", &vfs, nullptr, 0);
expect2 += TestProvider::Opened;
EXPECT_EQ(test1, expect1);
EXPECT_EQ(test2, expect2);
EXPECT_EQ(test3, expect3);
mju_closeResource(res1);
mju_closeResource(res2);
mju_closeResource(res3);
expect1 += TestProvider::Closed;
expect2 += TestProvider::Closed;
expect3 += TestProvider::Closed;
mj_deleteVFS(&vfs);
expect1 += TestProvider::Unmounted;
expect2 += TestProvider::Unmounted;
expect3 += TestProvider::Unmounted;
EXPECT_EQ(test1, expect1);
EXPECT_EQ(test2, expect2);
EXPECT_EQ(test3, expect3);
}
} // namespace
} // namespace mujoco
+1 -1
View File
@@ -126,7 +126,7 @@ TEST_F(LoadXmlTest, InvalidFileFails) {
std::array<char, 1000> error;
mjSpec* spec = mj_parseXML("invalid", nullptr, error.data(), error.size());
EXPECT_THAT(spec, IsNull()) << "Expected model loading to fail.";
EXPECT_THAT(error.data(), HasSubstr("No such file or directory"));
EXPECT_THAT(error.data(), HasSubstr("Error opening file"));
}
TEST_F(MujocoTest, SaveXmlShortString) {