Skip to content

Commit dc57dd8

Browse files
committed
Add tests to spin-core crate
Signed-off-by: Lann Martin <[email protected]>
1 parent 1e442b8 commit dc57dd8

File tree

10 files changed

+286
-4
lines changed

10 files changed

+286
-4
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build.rs

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ error: the `wasm32-wasi` target is not installed
3939

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

42+
build_wasm_test_program("core-wasi-test.wasm", "crates/core/tests/core-wasi-test");
4243
build_wasm_test_program("rust-http-test.wasm", "crates/http/tests/rust-http-test");
4344
build_wasm_test_program("redis-rust.wasm", "crates/redis/tests/rust");
4445
build_wasm_test_program("wagi-test.wasm", "crates/http/tests/wagi-test");

crates/core/Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ async-trait = "0.1"
1010
wasi-cap-std-sync = "0.39"
1111
wasi-common = "0.39"
1212
wasmtime = "0.39"
13-
wasmtime-wasi = { version = "0.39", features = ["tokio"] }
13+
wasmtime-wasi = { version = "0.39", features = ["tokio"] }
14+
15+
[dev-dependencies]
16+
tempfile = "3"
17+
tokio = { version = "1", features = ["macros", "rt"] }

crates/core/src/host_component.rs

+41
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,44 @@ impl HostComponentsData {
174174
self.data[idx].get_or_insert_with(|| self.data_builders[idx]())
175175
}
176176
}
177+
178+
#[cfg(test)]
179+
mod tests {
180+
use super::*;
181+
182+
struct TestHC;
183+
184+
impl HostComponent for TestHC {
185+
type Data = u8;
186+
187+
fn add_to_linker<T: Send>(
188+
_linker: &mut Linker<T>,
189+
_get: impl Fn(&mut Data<T>) -> &mut Self::Data + Send + Sync + Copy + 'static,
190+
) -> Result<()> {
191+
Ok(())
192+
}
193+
194+
fn build_data(&self) -> Self::Data {
195+
0
196+
}
197+
}
198+
199+
#[test]
200+
fn host_components_data() {
201+
let engine = wasmtime::Engine::default();
202+
let mut linker: crate::Linker<()> = crate::Linker::new(&engine);
203+
204+
let mut builder = HostComponents::builder();
205+
let handle1 = builder
206+
.add_host_component(&mut linker, Arc::new(TestHC))
207+
.unwrap();
208+
let handle2 = builder.add_host_component(&mut linker, TestHC).unwrap();
209+
let host_components = builder.build();
210+
let mut hc_data = host_components.new_data();
211+
212+
assert_eq!(hc_data.get_or_insert(handle1), &0);
213+
214+
hc_data.set(handle2, 1);
215+
assert_eq!(hc_data.get_or_insert(handle2), &1);
216+
}
217+
}

crates/core/src/io.rs

+19
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,22 @@ impl OutputBuffer {
1616
WritePipe::from_shared(self.0.clone())
1717
}
1818
}
19+
20+
#[cfg(test)]
21+
mod tests {
22+
use std::io::IoSlice;
23+
24+
use wasi_common::WasiFile;
25+
26+
use super::*;
27+
28+
#[tokio::test]
29+
async fn take_what_you_write() {
30+
let mut buf = OutputBuffer::default();
31+
buf.writer()
32+
.write_vectored(&[IoSlice::new(b"foo")])
33+
.await
34+
.unwrap();
35+
assert_eq!(buf.take(), b"foo");
36+
}
37+
}

crates/core/src/store.rs

+13-3
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ impl StoreBuilder {
108108
self.store_limits = StoreLimitsAsync::new(Some(max_memory_size), None);
109109
}
110110

111-
/// Inherit stdin, stdout, and stderr from the host process.
112-
pub fn inherit_stdio(&mut self) {
113-
self.with_wasi(|wasi| wasi.inherit_stdio());
111+
/// Inherit stdin from the host process.
112+
pub fn inherit_stdin(&mut self) {
113+
self.with_wasi(|wasi| wasi.inherit_stdin());
114114
}
115115

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

126+
/// Inherit stdin from the host process.
127+
pub fn inherit_stdout(&mut self) {
128+
self.with_wasi(|wasi| wasi.inherit_stdout());
129+
}
130+
126131
/// Sets the WASI `stdout` descriptor.
127132
pub fn stdout(&mut self, file: impl WasiFile + 'static) {
128133
self.with_wasi(|wasi| wasi.stdout(Box::new(file)))
@@ -140,6 +145,11 @@ impl StoreBuilder {
140145
buffer
141146
}
142147

148+
/// Inherit stdin from the host process.
149+
pub fn inherit_stderr(&mut self) {
150+
self.with_wasi(|wasi| wasi.inherit_stderr());
151+
}
152+
143153
/// Sets the WASI `stderr` descriptor.
144154
pub fn stderr(&mut self, file: impl WasiFile + 'static) {
145155
self.with_wasi(|wasi| wasi.stderr(Box::new(file)))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[build]
2+
target = "wasm32-wasi"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "core-wasi-test"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[profile.release]
7+
debug = true
8+
9+
[workspace]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//! This test program takes argument(s) that determine which WASI feature to
2+
//! exercise and returns an exit code of 0 for success, 1 for WASI interface
3+
//! failure (which is sometimes expected in a test), and some other code on
4+
//! invalid argument(s).
5+
6+
type Result = std::result::Result<(), Box<dyn std::error::Error>>;
7+
8+
fn main() -> Result {
9+
let mut args = std::env::args();
10+
let cmd = args.next().expect("cmd");
11+
match cmd.as_str() {
12+
"noop" => (),
13+
"echo" => {
14+
eprintln!("echo");
15+
std::io::copy(&mut std::io::stdin(), &mut std::io::stdout())?;
16+
}
17+
"alloc" => {
18+
let size: usize = args.next().expect("size").parse().expect("size");
19+
eprintln!("alloc {size}");
20+
let layout = std::alloc::Layout::from_size_align(size, 8).expect("layout");
21+
unsafe {
22+
let p = std::alloc::alloc(layout);
23+
if p.is_null() {
24+
return Err("allocation failed".into());
25+
}
26+
// Force allocation to actually happen
27+
p.read_volatile();
28+
}
29+
}
30+
"read" => {
31+
let path = args.next().expect("path");
32+
eprintln!("read {path}");
33+
std::fs::read(path)?;
34+
}
35+
"write" => {
36+
let path = args.next().expect("path");
37+
eprintln!("write {path}");
38+
std::fs::write(path, "content")?;
39+
}
40+
"panic" => {
41+
eprintln!("panic");
42+
panic!("intentional panic");
43+
}
44+
cmd => panic!("unknown cmd {cmd}"),
45+
};
46+
Ok(())
47+
}

crates/core/tests/integration_test.rs

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
use std::{io::Cursor, path::PathBuf};
2+
3+
use spin_core::{Config, Engine, Module, StoreBuilder, Trap};
4+
use tempfile::TempDir;
5+
use wasi_common::pipe::WritePipe;
6+
use wasmtime::TrapCode;
7+
8+
#[tokio::test(flavor = "multi_thread")]
9+
async fn test_stdio() {
10+
let stdout_pipe = WritePipe::new_in_memory();
11+
12+
run_core_wasi_test(["echo"], |store_builder| {
13+
store_builder.stdin_pipe(Cursor::new(b"DATA"));
14+
store_builder.stdout(stdout_pipe.clone());
15+
})
16+
.await
17+
.unwrap();
18+
19+
assert_eq!(stdout_pipe.try_into_inner().unwrap().into_inner(), b"DATA");
20+
}
21+
22+
#[tokio::test(flavor = "multi_thread")]
23+
async fn test_read_only_preopened_dir() {
24+
let filename = "test_file";
25+
let tempdir = TempDir::new().unwrap();
26+
std::fs::write(tempdir.path().join(filename), "x").unwrap();
27+
28+
run_core_wasi_test(["read", filename], |store_builder| {
29+
store_builder
30+
.read_only_preopened_dir(&tempdir, "/".into())
31+
.unwrap();
32+
})
33+
.await
34+
.unwrap();
35+
}
36+
37+
#[tokio::test(flavor = "multi_thread")]
38+
async fn test_read_only_preopened_dir_write_fails() {
39+
let filename = "test_file";
40+
let tempdir = TempDir::new().unwrap();
41+
std::fs::write(tempdir.path().join(filename), "x").unwrap();
42+
43+
let err = run_core_wasi_test(["write", filename], |store_builder| {
44+
store_builder
45+
.read_only_preopened_dir(&tempdir, "/".into())
46+
.unwrap();
47+
})
48+
.await
49+
.unwrap_err();
50+
let trap = err.downcast::<Trap>().expect("trap");
51+
assert_eq!(trap.i32_exit_status(), Some(1));
52+
}
53+
54+
#[tokio::test(flavor = "multi_thread")]
55+
async fn test_read_write_preopened_dir() {
56+
let filename = "test_file";
57+
let tempdir = TempDir::new().unwrap();
58+
59+
run_core_wasi_test(["write", filename], |store_builder| {
60+
store_builder
61+
.read_write_preopened_dir(&tempdir, "/".into())
62+
.unwrap();
63+
})
64+
.await
65+
.unwrap();
66+
67+
let content = std::fs::read(tempdir.path().join(filename)).unwrap();
68+
assert_eq!(content, b"content");
69+
}
70+
71+
#[tokio::test(flavor = "multi_thread")]
72+
async fn test_max_memory_size_obeyed() {
73+
let max = 10_000_000;
74+
let alloc = max / 10;
75+
run_core_wasi_test(["alloc", &format!("{alloc}")], |store_builder| {
76+
store_builder.max_memory_size(max);
77+
})
78+
.await
79+
.unwrap();
80+
}
81+
82+
#[tokio::test(flavor = "multi_thread")]
83+
async fn test_max_memory_size_violated() {
84+
let max = 10_000_000;
85+
let alloc = max * 2;
86+
let err = run_core_wasi_test(["alloc", &format!("{alloc}")], |store_builder| {
87+
store_builder.max_memory_size(max);
88+
})
89+
.await
90+
.unwrap_err();
91+
let trap = err.downcast::<Trap>().expect("trap");
92+
assert_eq!(trap.i32_exit_status(), Some(1));
93+
}
94+
95+
#[tokio::test(flavor = "multi_thread")]
96+
#[cfg(not(tarpaulin))]
97+
async fn test_panic() {
98+
let err = run_core_wasi_test(["panic"], |_| {}).await.unwrap_err();
99+
let trap = err.downcast::<Trap>().expect("trap");
100+
assert_eq!(trap.trap_code(), Some(TrapCode::UnreachableCodeReached));
101+
}
102+
103+
async fn run_core_wasi_test<'a>(
104+
args: impl IntoIterator<Item = &'a str>,
105+
f: impl FnOnce(&mut StoreBuilder),
106+
) -> anyhow::Result<()> {
107+
let mut config = Config::default();
108+
config
109+
.wasmtime_config()
110+
.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);
111+
112+
let engine: Engine<()> = Engine::builder(&config).unwrap().build();
113+
114+
let mut store_builder: StoreBuilder = engine.store_builder();
115+
116+
f(&mut store_builder);
117+
store_builder.stderr_pipe(TestWriter);
118+
store_builder.args(args).unwrap();
119+
120+
let mut store = store_builder.build().unwrap();
121+
122+
let module_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
123+
.join("../../target/test-programs/core-wasi-test.wasm");
124+
let module = Module::from_file(engine.as_ref(), module_path).unwrap();
125+
126+
let instance_pre = engine.instantiate_pre(&module).unwrap();
127+
128+
let instance = instance_pre.instantiate_async(&mut store).await.unwrap();
129+
130+
let func = instance.get_func(&mut store, "_start").unwrap();
131+
132+
func.call_async(&mut store, &[], &mut []).await
133+
}
134+
135+
// Write with `print!`, required for test output capture
136+
struct TestWriter;
137+
138+
impl std::io::Write for TestWriter {
139+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
140+
print!("{}", String::from_utf8_lossy(buf));
141+
Ok(buf.len())
142+
}
143+
144+
fn flush(&mut self) -> std::io::Result<()> {
145+
Ok(())
146+
}
147+
}

0 commit comments

Comments
 (0)