//! Core-module tests for preview1 filesystem routing and real userland programs. use sausage_vfs::{ preview1::{self, State}, *, }; use std::{ hash::{Hash, Hasher}, path::Path, sync::{Arc, OnceLock}, }; use wasmtime::{Engine, Linker, Module, Store}; fn engine() -> &'static Engine { static E: OnceLock = OnceLock::new(); E.get_or_init(Engine::default) } fn module(path: &str) -> Module { let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("required guest {path}: {e}")); let mut hash = std::collections::hash_map::DefaultHasher::new(); bytes.hash(&mut hash); engine().precompile_compatibility_hash().hash(&mut hash); let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("preview1-guests"); std::fs::create_dir_all(&dir).unwrap(); let cache = dir.join(format!("{:x}.cwasm", hash.finish())); if !cache.exists() { std::fs::write(&cache, engine().precompile_module(&bytes).unwrap()).unwrap(); } // This cache contains only modules compiled locally by this test with this engine. unsafe { Module::deserialize_file(engine(), cache) }.unwrap() } async fn run(module: &Module, root: Arc, cwd: &str, args: &[&str]) -> (String, String) { let (stdout, mut out) = tokio::io::duplex(1024 * 1024); let (stderr, mut err) = tokio::io::duplex(1024 * 1024); let mut linker = Linker::new(engine()); preview1::add_to_linker(&mut linker, |s: &mut State| s).unwrap(); let mut store = Store::new( engine(), State::new( root, cwd, args.iter().map(|s| (*s).into()).collect(), vec![], Box::new(tokio::io::empty()), Box::new(stdout), Box::new(stderr), ), ); let instance = linker.instantiate_async(&mut store, module).await.unwrap(); let start = instance .get_typed_func::<(), ()>(&mut store, "_start") .unwrap(); let result = start.call_async(&mut store, ()).await; drop(store); use tokio::io::AsyncReadExt; let mut stdout = String::new(); out.read_to_string(&mut stdout).await.unwrap(); let mut stderr = String::new(); err.read_to_string(&mut stderr).await.unwrap(); if let Err(e) = result { assert_eq!( e.downcast_ref::().map(|e| e.0), Some(0), "guest failed: {e:?}\n{stderr}" ); } (stdout, stderr) } fn wat(path: &str, body: &str) -> Module { Module::new(engine(), format!(r#"(module (import "wasi_snapshot_preview1" "path_filestat_get" (func $stat (param i32 i32 i32 i32 i32) (result i32))) (memory (export "memory") 1) (data (i32.const 0) "{path}") (func (export "_start") {body}))"#)).unwrap() } #[tokio::test] async fn cwd_and_parent_across_mounts() { let base = Arc::new(Memory::default()); base.mkdir("/work").await.unwrap(); let shared = Arc::new(Memory::default()); shared.mkdir("/inner").await.unwrap(); base.open( "/value", Open { create: true, write: true, ..Open::default() }, ) .await .unwrap(); let file = shared .open( "/value", Open { create: true, write: true, ..Open::default() }, ) .await .unwrap(); file.write(0, b"hello".to_vec()).await.unwrap(); let mut mounts = Mounts::default(); mounts.mount("/", base, false); mounts.mount("/shared", shared, false); let mounts = Arc::new(mounts); let guest = wat( "../shared/value", "i32.const 3 i32.const 1 i32.const 0 i32.const 15 i32.const 64 call $stat if unreachable end i32.const 96 i64.load i64.const 5 i64.ne if unreachable end", ); run(&guest, mounts.clone(), "/work", &["probe"]).await; let parent = wat( "../../value", "i32.const 3 i32.const 1 i32.const 0 i32.const 11 i32.const 64 call $stat if unreachable end i32.const 96 i64.load i64.const 0 i64.ne if unreachable end", ); run(&parent, mounts, "/shared/inner", &["probe"]).await; println!("PASS cwd and .. across mounts"); } #[tokio::test] async fn missing_file_errno() { let guest = wat( "../shared/value", "i32.const 3 i32.const 1 i32.const 0 i32.const 15 i32.const 64 call $stat i32.const 44 i32.ne if unreachable end", ); run(&guest, Arc::new(Memory::default()), "/work", &["probe"]).await; println!("PASS missing path returns NOENT=44"); } #[tokio::test] async fn poll_preserves_input_and_returns_clock() { use preview1::{types::*, wasi_snapshot_preview1::WasiSnapshotPreview1}; use wiggle::{GuestMemory, GuestPtr}; let mut state = State::new( Arc::new(Memory::default()), "/", vec![], vec![], Box::new(&b"x"[..]), Box::new(tokio::io::sink()), Box::new(tokio::io::sink()), ); let mut bytes = [0; 256]; let mut memory = GuestMemory::Unshared(&mut bytes); let sub = GuestPtr::new(0); let event = GuestPtr::new(64); memory .write( sub, Subscription { userdata: 7, u: SubscriptionU::FdRead(SubscriptionFdReadwrite { file_descriptor: 0.into(), }), }, ) .unwrap(); assert_eq!( state.poll_oneoff(&mut memory, sub, event, 1).await.unwrap(), 1 ); assert_eq!(memory.read(event).unwrap().userdata, 7); let iov = GuestPtr::new(128); memory .write( iov, Iovec { buf: GuestPtr::new(192), buf_len: 100, }, ) .unwrap(); assert!( state .fd_read(&mut memory, 0.into(), iov.as_array(1)) .await .is_err() ); memory .write( iov, Iovec { buf: GuestPtr::new(192), buf_len: 1, }, ) .unwrap(); assert_eq!( state .fd_read(&mut memory, 0.into(), iov.as_array(1)) .await .unwrap(), 1 ); assert_eq!(memory.read(GuestPtr::::new(192)).unwrap(), b'x'); memory .write( sub, Subscription { userdata: 8, u: SubscriptionU::Clock(SubscriptionClock { id: Clockid::Monotonic, timeout: 0, precision: 0, flags: Subclockflags::SUBSCRIPTION_CLOCK_ABSTIME, }), }, ) .unwrap(); assert_eq!( state.poll_oneoff(&mut memory, sub, event, 1).await.unwrap(), 1 ); assert_eq!(memory.read(event).unwrap().type_, Eventtype::Clock); memory .write( sub, Subscription { userdata: 9, u: SubscriptionU::FdWrite(SubscriptionFdReadwrite { file_descriptor: 1.into(), }), }, ) .unwrap(); assert_eq!( state.poll_oneoff(&mut memory, sub, event, 1).await.unwrap(), 1 ); assert_eq!(memory.read(event).unwrap().type_, Eventtype::FdWrite); } #[tokio::test(flavor = "multi_thread")] async fn userland_files() { let guest = module("/tmp/wasi-userland-lab/uu/coreutils-0.12.0-wasm32-wasip1/coreutils.wasm"); let root = Arc::new(Memory::default()); run(&guest, root.clone(), "/", &["coreutils", "mkdir", "/work"]).await; run( &guest, root.clone(), "/work", &["coreutils", "touch", "input"], ) .await; let input = root .open( "/work/input", Open { write: true, ..Open::default() }, ) .await .unwrap(); input.write(0, b"hello preview1\n".to_vec()).await.unwrap(); run( &guest, root.clone(), "/work", &["coreutils", "cp", "input", "copy"], ) .await; let (cat, _) = run(&guest, root.clone(), "/work", &["coreutils", "cat", "copy"]).await; assert_eq!(cat, "hello preview1\n"); let (ls, _) = run( &guest, root.clone(), "/work", &["coreutils", "ls", "-la", "."], ) .await; assert!(ls.contains("copy") && ls.contains("input"), "{ls}"); run(&guest, root.clone(), "/work", &["coreutils", "rm", "copy"]).await; assert_eq!( root.stat("/work/copy", true).await.unwrap_err().kind(), std::io::ErrorKind::NotFound ); println!("PASS uutils mkdir touch cp cat ls -la rm"); let qjs = module("/tmp/wasi-userland-lab/qjs-wasi.wasm"); let script = b"let f = std.open('input', 'r'); let text = f.readAsString(); f.close(); let out = std.open('js-output', 'w'); out.puts(text.toUpperCase()); out.close(); console.log('QuickJS files ok');"; let file = root .open( "/work/check.mjs", Open { create: true, write: true, ..Open::default() }, ) .await .unwrap(); file.write(0, script.to_vec()).await.unwrap(); let (out, _) = run( &qjs, root.clone(), "/work", &["qjs", "--std", "--module", "check.mjs"], ) .await; assert!(out.contains("QuickJS files ok")); let output = root.open("/work/js-output", Open::default()).await.unwrap(); assert_eq!(output.read(0, 100).await.unwrap(), b"HELLO PREVIEW1\n"); println!("PASS QuickJS reads and writes files"); } #[tokio::test(flavor = "multi_thread")] async fn esbuild_bundle() { let dir = tempfile::tempdir().unwrap(); let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../spikes/s6-esbuild"); for name in ["app.tsx", "check.mjs", "package.json", "package-lock.json"] { std::fs::copy(fixture.join(name), dir.path().join(name)).unwrap(); } assert!( std::process::Command::new("cp") .args(["-r", "/tmp/s6-esbuild/app/node_modules"]) .arg(dir.path()) .status() .unwrap() .success() ); let flags = [ "app.tsx", "--bundle", "--minify", "--format=iife", "--platform=browser", "--jsx=automatic", "--jsx-import-source=preact", "--legal-comments=none", "--log-level=error", ]; assert!( std::process::Command::new("/tmp/s6-esbuild/esbuild-native") .current_dir(dir.path()) .args(flags) .arg("--outfile=native.js") .status() .unwrap() .success() ); let root = Arc::new(Host::new(dir.path(), Process::default()).unwrap()); let guest = module("/tmp/s6-esbuild/esbuild.wasm"); let mut args = vec!["esbuild"]; args.extend(flags); args.push("--outfile=host.js"); run(&guest, root, "/", &args).await; assert_eq!( std::fs::read(dir.path().join("native.js")).unwrap(), std::fs::read(dir.path().join("host.js")).unwrap() ); let render = std::process::Command::new("node") .current_dir(dir.path()) .args(["check.mjs", "host.js"]) .output() .unwrap(); assert!( render.status.success(), "{}", String::from_utf8_lossy(&render.stderr) ); println!("{}", String::from_utf8_lossy(&render.stdout)); println!("PASS esbuild through sausage-vfs: byte-identical to native"); }