153 lines
4.9 KiB
Rust
153 lines
4.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 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;
|
|
}
|