//! Builds one WASIp2 guest and runs each filesystem topic in an isolated instance. use sausage_vfs::{ adapter::{self, Adapter, View}, *, }; use std::sync::{Arc, OnceLock}; use wasmtime::{ Engine, Store, component::{Component, Linker, ResourceTable}, }; use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; struct State { wasi: WasiCtx, table: ResourceTable, fs: Adapter, } impl WasiView for State { fn ctx(&mut self) -> WasiCtxView<'_> { WasiCtxView { ctx: &mut self.wasi, table: &mut self.table, } } } 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(); (engine, component) }) } async fn run(topic: &str, shared: Arc) { let (engine, component) = guest(); let world = tempfile::tempdir().unwrap(); std::fs::create_dir(world.path().join("tmp")).unwrap(); let mut mounts = Mounts::default(); mounts.mount( "/", Arc::new(Host::new(world.path(), Process::default()).unwrap()), 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 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 mut store = Store::new( engine, State { wasi: WasiCtxBuilder::new() .inherit_stdio() .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) .await .unwrap(); let result = command.wasi_cli_run().call_run(&mut store).await; assert!( 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; }