//! 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 outside = tempfile::tempdir().unwrap(); if topic == "review_links" { std::fs::create_dir(world.path().join("target")).unwrap(); std::fs::write(world.path().join("target/keep"), b"keep").unwrap(); std::fs::write(outside.path().join("secret"), b"secret").unwrap(); std::os::unix::fs::symlink("target", world.path().join("link")).unwrap(); std::os::unix::fs::symlink(outside.path(), world.path().join("outside-link")).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 wasi = WasiCtxBuilder::new(); wasi.inherit_stdio().env("VFS_TOPIC", topic); if topic == "review_preopen" { wasi.preopened_dir( world.path(), "/native", wasmtime_wasi::filesystem::FsPerms::ReadWrite, ) .unwrap(); } let mut store = Store::new( engine, State { wasi: wasi.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; } /// Stream drop preserves writes and small reads observe later mutations. #[tokio::test(flavor = "multi_thread")] async fn review_streams() { isolated("review_streams").await; } /// Timestamp conversion reports overflow and accepts directories. #[tokio::test(flavor = "multi_thread")] async fn review_times() { isolated("review_times").await; } /// Path errors distinguish trailing slashes, busy roots, and ordinary files. #[tokio::test(flavor = "multi_thread")] async fn review_paths() { isolated("review_paths").await; } /// Symlink traversal does not delete targets or escape the host root. #[tokio::test(flavor = "multi_thread")] async fn review_links() { isolated("review_links").await; } /// The guest sees only the virtual root preopen. #[tokio::test(flavor = "multi_thread")] async fn review_preopen() { isolated("review_preopen").await; } /// Adding the adapter restores duplicate-import errors on the caller's linker. #[test] fn linker_shadowing_is_reset() { let engine = Engine::default(); let mut linker = Linker::::new(&engine); wasmtime_wasi::p2::add_to_linker_async(&mut linker).unwrap(); adapter::add_to_linker(&mut linker, |s| View { adapter: &mut s.fs, table: &mut s.table, }) .unwrap(); assert!(wasmtime_wasi::p2::add_to_linker_async(&mut linker).is_err()); }