sausage-vfs: tests per topic under WASI, nested differential tests, contract through Mounts

This commit is contained in:
2026-10-23 00:12:54 +01:00
parent 4882edf243
commit 218a718a3d
7 changed files with 570 additions and 123 deletions
+96 -38
View File
@@ -1,9 +1,9 @@
//! Compiles a real WASIp2 guest and runs std, SQLite, and tempfile through the adapter.
//! Builds one WASIp2 guest and runs each filesystem topic in an isolated instance.
use sausage_vfs::{
adapter::{self, Adapter, View},
*,
};
use std::sync::Arc;
use std::sync::{Arc, OnceLock};
use wasmtime::{
Engine, Store,
component::{Component, Linker, ResourceTable},
@@ -22,26 +22,37 @@ impl WasiView for State {
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn rust_guest() {
let manifest =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/guests/files/Cargo.toml");
let target = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../target/vfs/guests");
let status = std::process::Command::new("cargo")
.args([
"build",
"--release",
"--locked",
"--offline",
"--target",
"wasm32-wasip2",
"--manifest-path",
])
.arg(manifest)
.env("CARGO_TARGET_DIR", &target)
.status()
fn guest() -> &'static (Engine, Component) {
static GUEST: OnceLock<(Engine, Component)> = OnceLock::new();
GUEST.get_or_init(|| {
let manifest =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/guests/files/Cargo.toml");
let target = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../target/vfs/guests");
let status = std::process::Command::new("cargo")
.args([
"build",
"--release",
"--offline",
"--target",
"wasm32-wasip2",
"--manifest-path",
])
.arg(manifest)
.env("CARGO_TARGET_DIR", &target)
.status()
.unwrap();
assert!(status.success(), "guest build failed");
let engine = Engine::default();
let component = Component::from_file(
&engine,
target.join("wasm32-wasip2/release/vfs-files-guest.wasm"),
)
.unwrap();
assert!(status.success());
(engine, component)
})
}
async fn run(topic: &str, shared: Arc<Memory>) {
let (engine, component) = guest();
let world = tempfile::tempdir().unwrap();
std::fs::create_dir(world.path().join("tmp")).unwrap();
let mut mounts = Mounts::default();
@@ -50,45 +61,92 @@ async fn rust_guest() {
Arc::new(Host::new(world.path(), Process::default()).unwrap()),
false,
);
mounts.mount("/run", Arc::new(Memory::default()), false);
mounts.mount("/run", shared, false);
mounts.mount("/self", Arc::new(Memory::default()), false);
let mut generated = Generated::default();
generated.insert("/value", || Ok(b"snapshot".to_vec()));
mounts.mount("/proc", Arc::new(generated), true);
let engine = Engine::default();
let mut linker = Linker::new(&engine);
let mut linker = Linker::new(engine);
wasmtime_wasi::p2::add_to_linker_async(&mut linker).unwrap();
adapter::add_to_linker(&mut linker, |s: &mut State| View {
adapter: &mut s.fs,
table: &mut s.table,
})
.unwrap();
let component = Component::from_file(
&engine,
target.join("wasm32-wasip2/release/vfs-files-guest.wasm"),
)
.unwrap();
let mut store = Store::new(
&engine,
engine,
State {
wasi: WasiCtxBuilder::new()
.inherit_stdio()
.env("TMPDIR", "/tmp")
.env("VFS_TOPIC", topic)
.build(),
table: ResourceTable::new(),
fs: Adapter::new(mounts),
},
);
let command =
wasmtime_wasi::p2::bindings::Command::instantiate_async(&mut store, &component, &linker)
wasmtime_wasi::p2::bindings::Command::instantiate_async(&mut store, component, &linker)
.await
.unwrap();
let result = command.wasi_cli_run().call_run(&mut store).await;
assert!(
command
.wasi_cli_run()
.call_run(&mut store)
.await
.unwrap()
.is_ok()
matches!(result, Ok(Ok(()))),
"guest topic {topic} failed: {result:?}"
);
}
async fn isolated(topic: &str) {
run(topic, Arc::new(Memory::default())).await;
}
/// Basic filesystem operations support relative paths and metadata.
#[tokio::test(flavor = "multi_thread")]
async fn basic_files() {
isolated("basic").await;
}
/// Recursive operations and directory entries preserve nested trees.
#[tokio::test(flavor = "multi_thread")]
async fn nested_trees() {
isolated("trees").await;
}
/// Open flags and error kinds match Rust's filesystem contract.
#[tokio::test(flavor = "multi_thread")]
async fn options_and_errors() {
isolated("options").await;
}
/// Renames replace existing files and move complete folders.
#[tokio::test(flavor = "multi_thread")]
async fn renames() {
isolated("rename").await;
}
/// Sparse writes return zeros for the gap beyond EOF.
#[tokio::test(flavor = "multi_thread")]
async fn sparse_writes() {
isolated("sparse").await;
}
/// Streams transfer several MiB with a verified checksum.
#[tokio::test(flavor = "multi_thread")]
async fn large_streams() {
isolated("streams").await;
}
/// Mount listings, root writes, and read-only errors follow the namespace.
#[tokio::test(flavor = "multi_thread")]
async fn mount_boundaries() {
isolated("mounts").await;
}
/// Separate guest stores share the same memory layer.
#[tokio::test(flavor = "multi_thread")]
async fn shared_memory_between_guests() {
let memory = Arc::new(Memory::default());
run("shared_write", memory.clone()).await;
run("shared_read", memory).await;
}
/// SQLite commits many rows and reads them after reopen.
#[tokio::test(flavor = "multi_thread")]
async fn sqlite_transactions() {
isolated("sqlite").await;
}
/// Named temporary files persist through the filesystem adapter.
#[tokio::test(flavor = "multi_thread")]
async fn named_temporary_files() {
isolated("tempfile").await;
}