Skip to content

Commit

Permalink
Add tests to spin-core crate
Browse files Browse the repository at this point in the history
Signed-off-by: Lann Martin <[email protected]>
  • Loading branch information
lann committed Sep 19, 2022
1 parent 1e442b8 commit dc57dd8
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 4 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ error: the `wasm32-wasi` target is not installed

std::fs::create_dir_all("target/test-programs").unwrap();

build_wasm_test_program("core-wasi-test.wasm", "crates/core/tests/core-wasi-test");
build_wasm_test_program("rust-http-test.wasm", "crates/http/tests/rust-http-test");
build_wasm_test_program("redis-rust.wasm", "crates/redis/tests/rust");
build_wasm_test_program("wagi-test.wasm", "crates/http/tests/wagi-test");
Expand Down
6 changes: 5 additions & 1 deletion crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ async-trait = "0.1"
wasi-cap-std-sync = "0.39"
wasi-common = "0.39"
wasmtime = "0.39"
wasmtime-wasi = { version = "0.39", features = ["tokio"] }
wasmtime-wasi = { version = "0.39", features = ["tokio"] }

[dev-dependencies]
tempfile = "3"
tokio = { version = "1", features = ["macros", "rt"] }
41 changes: 41 additions & 0 deletions crates/core/src/host_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,44 @@ impl HostComponentsData {
self.data[idx].get_or_insert_with(|| self.data_builders[idx]())
}
}

#[cfg(test)]
mod tests {
use super::*;

struct TestHC;

impl HostComponent for TestHC {
type Data = u8;

fn add_to_linker<T: Send>(
_linker: &mut Linker<T>,
_get: impl Fn(&mut Data<T>) -> &mut Self::Data + Send + Sync + Copy + 'static,
) -> Result<()> {
Ok(())
}

fn build_data(&self) -> Self::Data {
0
}
}

#[test]
fn host_components_data() {
let engine = wasmtime::Engine::default();
let mut linker: crate::Linker<()> = crate::Linker::new(&engine);

let mut builder = HostComponents::builder();
let handle1 = builder
.add_host_component(&mut linker, Arc::new(TestHC))
.unwrap();
let handle2 = builder.add_host_component(&mut linker, TestHC).unwrap();
let host_components = builder.build();
let mut hc_data = host_components.new_data();

assert_eq!(hc_data.get_or_insert(handle1), &0);

hc_data.set(handle2, 1);
assert_eq!(hc_data.get_or_insert(handle2), &1);
}
}
19 changes: 19 additions & 0 deletions crates/core/src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,22 @@ impl OutputBuffer {
WritePipe::from_shared(self.0.clone())
}
}

#[cfg(test)]
mod tests {
use std::io::IoSlice;

use wasi_common::WasiFile;

use super::*;

#[tokio::test]
async fn take_what_you_write() {
let mut buf = OutputBuffer::default();
buf.writer()
.write_vectored(&[IoSlice::new(b"foo")])
.await
.unwrap();
assert_eq!(buf.take(), b"foo");
}
}
16 changes: 13 additions & 3 deletions crates/core/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ impl StoreBuilder {
self.store_limits = StoreLimitsAsync::new(Some(max_memory_size), None);
}

/// Inherit stdin, stdout, and stderr from the host process.
pub fn inherit_stdio(&mut self) {
self.with_wasi(|wasi| wasi.inherit_stdio());
/// Inherit stdin from the host process.
pub fn inherit_stdin(&mut self) {
self.with_wasi(|wasi| wasi.inherit_stdin());
}

/// Sets the WASI `stdin` descriptor.
Expand All @@ -123,6 +123,11 @@ impl StoreBuilder {
self.stdin(ReadPipe::new(r))
}

/// Inherit stdin from the host process.
pub fn inherit_stdout(&mut self) {
self.with_wasi(|wasi| wasi.inherit_stdout());
}

/// Sets the WASI `stdout` descriptor.
pub fn stdout(&mut self, file: impl WasiFile + 'static) {
self.with_wasi(|wasi| wasi.stdout(Box::new(file)))
Expand All @@ -140,6 +145,11 @@ impl StoreBuilder {
buffer
}

/// Inherit stdin from the host process.
pub fn inherit_stderr(&mut self) {
self.with_wasi(|wasi| wasi.inherit_stderr());
}

/// Sets the WASI `stderr` descriptor.
pub fn stderr(&mut self, file: impl WasiFile + 'static) {
self.with_wasi(|wasi| wasi.stderr(Box::new(file)))
Expand Down
2 changes: 2 additions & 0 deletions crates/core/tests/core-wasi-test/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
target = "wasm32-wasi"
9 changes: 9 additions & 0 deletions crates/core/tests/core-wasi-test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "core-wasi-test"
version = "0.1.0"
edition = "2021"

[profile.release]
debug = true

[workspace]
47 changes: 47 additions & 0 deletions crates/core/tests/core-wasi-test/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! This test program takes argument(s) that determine which WASI feature to
//! exercise and returns an exit code of 0 for success, 1 for WASI interface
//! failure (which is sometimes expected in a test), and some other code on
//! invalid argument(s).
type Result = std::result::Result<(), Box<dyn std::error::Error>>;

fn main() -> Result {
let mut args = std::env::args();
let cmd = args.next().expect("cmd");
match cmd.as_str() {
"noop" => (),
"echo" => {
eprintln!("echo");
std::io::copy(&mut std::io::stdin(), &mut std::io::stdout())?;
}
"alloc" => {
let size: usize = args.next().expect("size").parse().expect("size");
eprintln!("alloc {size}");
let layout = std::alloc::Layout::from_size_align(size, 8).expect("layout");
unsafe {
let p = std::alloc::alloc(layout);
if p.is_null() {
return Err("allocation failed".into());
}
// Force allocation to actually happen
p.read_volatile();
}
}
"read" => {
let path = args.next().expect("path");
eprintln!("read {path}");
std::fs::read(path)?;
}
"write" => {
let path = args.next().expect("path");
eprintln!("write {path}");
std::fs::write(path, "content")?;
}
"panic" => {
eprintln!("panic");
panic!("intentional panic");
}
cmd => panic!("unknown cmd {cmd}"),
};
Ok(())
}
147 changes: 147 additions & 0 deletions crates/core/tests/integration_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
use std::{io::Cursor, path::PathBuf};

use spin_core::{Config, Engine, Module, StoreBuilder, Trap};
use tempfile::TempDir;
use wasi_common::pipe::WritePipe;
use wasmtime::TrapCode;

#[tokio::test(flavor = "multi_thread")]
async fn test_stdio() {
let stdout_pipe = WritePipe::new_in_memory();

run_core_wasi_test(["echo"], |store_builder| {
store_builder.stdin_pipe(Cursor::new(b"DATA"));
store_builder.stdout(stdout_pipe.clone());
})
.await
.unwrap();

assert_eq!(stdout_pipe.try_into_inner().unwrap().into_inner(), b"DATA");
}

#[tokio::test(flavor = "multi_thread")]
async fn test_read_only_preopened_dir() {
let filename = "test_file";
let tempdir = TempDir::new().unwrap();
std::fs::write(tempdir.path().join(filename), "x").unwrap();

run_core_wasi_test(["read", filename], |store_builder| {
store_builder
.read_only_preopened_dir(&tempdir, "/".into())
.unwrap();
})
.await
.unwrap();
}

#[tokio::test(flavor = "multi_thread")]
async fn test_read_only_preopened_dir_write_fails() {
let filename = "test_file";
let tempdir = TempDir::new().unwrap();
std::fs::write(tempdir.path().join(filename), "x").unwrap();

let err = run_core_wasi_test(["write", filename], |store_builder| {
store_builder
.read_only_preopened_dir(&tempdir, "/".into())
.unwrap();
})
.await
.unwrap_err();
let trap = err.downcast::<Trap>().expect("trap");
assert_eq!(trap.i32_exit_status(), Some(1));
}

#[tokio::test(flavor = "multi_thread")]
async fn test_read_write_preopened_dir() {
let filename = "test_file";
let tempdir = TempDir::new().unwrap();

run_core_wasi_test(["write", filename], |store_builder| {
store_builder
.read_write_preopened_dir(&tempdir, "/".into())
.unwrap();
})
.await
.unwrap();

let content = std::fs::read(tempdir.path().join(filename)).unwrap();
assert_eq!(content, b"content");
}

#[tokio::test(flavor = "multi_thread")]
async fn test_max_memory_size_obeyed() {
let max = 10_000_000;
let alloc = max / 10;
run_core_wasi_test(["alloc", &format!("{alloc}")], |store_builder| {
store_builder.max_memory_size(max);
})
.await
.unwrap();
}

#[tokio::test(flavor = "multi_thread")]
async fn test_max_memory_size_violated() {
let max = 10_000_000;
let alloc = max * 2;
let err = run_core_wasi_test(["alloc", &format!("{alloc}")], |store_builder| {
store_builder.max_memory_size(max);
})
.await
.unwrap_err();
let trap = err.downcast::<Trap>().expect("trap");
assert_eq!(trap.i32_exit_status(), Some(1));
}

#[tokio::test(flavor = "multi_thread")]
#[cfg(not(tarpaulin))]
async fn test_panic() {
let err = run_core_wasi_test(["panic"], |_| {}).await.unwrap_err();
let trap = err.downcast::<Trap>().expect("trap");
assert_eq!(trap.trap_code(), Some(TrapCode::UnreachableCodeReached));
}

async fn run_core_wasi_test<'a>(
args: impl IntoIterator<Item = &'a str>,
f: impl FnOnce(&mut StoreBuilder),
) -> anyhow::Result<()> {
let mut config = Config::default();
config
.wasmtime_config()
.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);

let engine: Engine<()> = Engine::builder(&config).unwrap().build();

let mut store_builder: StoreBuilder = engine.store_builder();

f(&mut store_builder);
store_builder.stderr_pipe(TestWriter);
store_builder.args(args).unwrap();

let mut store = store_builder.build().unwrap();

let module_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../target/test-programs/core-wasi-test.wasm");
let module = Module::from_file(engine.as_ref(), module_path).unwrap();

let instance_pre = engine.instantiate_pre(&module).unwrap();

let instance = instance_pre.instantiate_async(&mut store).await.unwrap();

let func = instance.get_func(&mut store, "_start").unwrap();

func.call_async(&mut store, &[], &mut []).await
}

// Write with `print!`, required for test output capture
struct TestWriter;

impl std::io::Write for TestWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
print!("{}", String::from_utf8_lossy(buf));
Ok(buf.len())
}

fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}

0 comments on commit dc57dd8

Please sign in to comment.