Files
surma 151550242b sausage-vfs: review fixes (fallible growth, symlink flags, stream cancel, sized reads, timestamps, error kinds, linker shadowing)
API: Layer::stat(path, follow), Layer::readlink, Open::follow,
Stat::symlink and Entry::symlink. Process::idle documents the shutdown
order: abort, await the task, await idle, release locks.
2026-10-23 00:49:46 +01:00

212 lines
6.9 KiB
Rust

//! 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<Memory>) {
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::<State>::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());
}