From 7992f7e88824191651c5ecd61af5c81932b9bcaa Mon Sep 17 00:00:00 2001 From: James Sturtevant Date: Tue, 1 Aug 2023 15:44:36 +0000 Subject: [PATCH] Centralize running wasi test Signed-off-by: James Sturtevant --- Cargo.lock | 1 + crates/containerd-shim-wasm/Cargo.toml | 2 +- .../src/sandbox/testutil.rs | 107 +++++++++++- .../containerd-shim-wasmedge/src/instance.rs | 90 +--------- crates/containerd-shim-wasmtime/Cargo.toml | 1 + .../containerd-shim-wasmtime/src/instance.rs | 154 ++++++------------ 6 files changed, 159 insertions(+), 196 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e607ed2cc..e9619ca4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -558,6 +558,7 @@ dependencies = [ "pretty_assertions", "serde", "serde_json", + "serial_test", "tempfile", "thiserror", "ttrpc", diff --git a/crates/containerd-shim-wasm/Cargo.toml b/crates/containerd-shim-wasm/Cargo.toml index 64b5cb183..092f00aed 100644 --- a/crates/containerd-shim-wasm/Cargo.toml +++ b/crates/containerd-shim-wasm/Cargo.toml @@ -28,12 +28,12 @@ clone3 = "0.2" libc = { workspace = true } caps = "0.5" proc-mounts = "0.3" +tempfile = "3" [build-dependencies] ttrpc-codegen = { version = "0.4.2", optional = true } [dev-dependencies] -tempfile = "3" pretty_assertions = "1" signal-hook = "0.3" env_logger = { workspace = true } diff --git a/crates/containerd-shim-wasm/src/sandbox/testutil.rs b/crates/containerd-shim-wasm/src/sandbox/testutil.rs index fd03167e3..df532a90a 100644 --- a/crates/containerd-shim-wasm/src/sandbox/testutil.rs +++ b/crates/containerd-shim-wasm/src/sandbox/testutil.rs @@ -1,7 +1,22 @@ //! Testing utilities used across different modules -use super::{Error, Result}; -use std::process::{Command, Stdio}; +use super::{instance::Wait, EngineGetter, Error, Instance, InstanceConfig, Result}; +use anyhow::Result as AnyHowResult; +use chrono::{DateTime, Utc}; +use libc::SIGKILL; +use oci_spec::runtime::{ProcessBuilder, RootBuilder, SpecBuilder}; +use serde::{Deserialize, Serialize}; +use std::io::Write; +use std::os::unix::fs::OpenOptionsExt; +use std::{ + borrow::Cow, + fs::{create_dir, File, OpenOptions}, + path::PathBuf, + process::{Command, Stdio}, + sync::mpsc::channel, + time::Duration, +}; +use tempfile::TempDir; fn normalize_test_name(test: &str) -> Result<&str> { let closure_removed = test.trim_end_matches("::{{closure}}"); @@ -64,5 +79,91 @@ macro_rules! function { name }}; } - pub use function; + +#[derive(Serialize, Deserialize)] +struct Options { + root: Option, +} + +pub fn run_wasi_test( + dir: &TempDir, + wasmbytes: Cow<[u8]>, + start_fn: Option<&str>, +) -> AnyHowResult<(u32, DateTime), Error> +where + I: Instance + EngineGetter, + E: Sync + Send + Clone, +{ + create_dir(dir.path().join("rootfs"))?; + let rootdir = dir.path().join("runwasi"); + create_dir(&rootdir)?; + let opts = Options { + root: Some(rootdir), + }; + let opts_file = OpenOptions::new() + .read(true) + .create(true) + .truncate(true) + .write(true) + .open(dir.path().join("options.json"))?; + write!(&opts_file, "{}", serde_json::to_string(&opts)?)?; + + let wasm_path = dir.path().join("rootfs/hello.wasm"); + let mut f = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o755) + .open(wasm_path)?; + f.write_all(&wasmbytes)?; + + let stdout = File::create(dir.path().join("stdout"))?; + drop(stdout); + + let entrypoint = match start_fn { + Some(s) => "./hello.wasm#".to_string() + s, + None => "./hello.wasm".to_string(), + }; + let spec = SpecBuilder::default() + .root(RootBuilder::default().path("rootfs").build()?) + .process( + ProcessBuilder::default() + .cwd("/") + .args(vec![entrypoint]) + .build()?, + ) + .build()?; + + spec.save(dir.path().join("config.json"))?; + + let mut cfg = InstanceConfig::new( + I::new_engine()?, + "test_namespace".into(), + "/containerd/address".into(), + ); + let cfg = cfg + .set_bundle(dir.path().to_str().unwrap().to_string()) + .set_stdout(dir.path().join("stdout").to_str().unwrap().to_string()); + + let wasi = I::new("test".to_string(), Some(cfg)); + + wasi.start()?; + + let (tx, rx) = channel(); + let waiter = Wait::new(tx); + wasi.wait(&waiter).unwrap(); + + let res = match rx.recv_timeout(Duration::from_secs(10)) { + Ok(res) => Ok(res), + Err(e) => { + wasi.kill(SIGKILL as u32).unwrap(); + return Err(Error::Others(format!( + "error waiting for module to finish: {0}", + e + ))); + } + }; + wasi.delete()?; + res +} diff --git a/crates/containerd-shim-wasmedge/src/instance.rs b/crates/containerd-shim-wasmedge/src/instance.rs index 0906d15f0..070e36035 100644 --- a/crates/containerd-shim-wasmedge/src/instance.rs +++ b/crates/containerd-shim-wasmedge/src/instance.rs @@ -273,21 +273,13 @@ impl EngineGetter for Wasi { #[cfg(test)] mod wasitest { - use std::borrow::Cow; - use std::fs::{create_dir, read_to_string, File, OpenOptions}; - use std::os::unix::prelude::OpenOptionsExt; - use std::sync::mpsc::channel; - use std::time::Duration; - + use super::*; use containerd_shim_wasm::function; use containerd_shim_wasm::sandbox::exec::has_cap_sys_admin; - use containerd_shim_wasm::sandbox::testutil::run_test_with_sudo; - use oci_spec::runtime::{ProcessBuilder, RootBuilder, SpecBuilder}; - use tempfile::{tempdir, TempDir}; - + use containerd_shim_wasm::sandbox::testutil::{run_test_with_sudo, run_wasi_test}; use serial_test::serial; - - use super::*; + use std::fs::read_to_string; + use tempfile::tempdir; use wasmedge_sdk::{ config::{CommonConfigOptions, ConfigBuilder}, @@ -332,76 +324,6 @@ mod wasitest { "# .as_bytes(); - fn run_wasi_test(dir: &TempDir, wasmbytes: Cow<[u8]>) -> Result<(u32, DateTime), Error> { - create_dir(dir.path().join("rootfs"))?; - let rootdir = dir.path().join("runwasi"); - create_dir(&rootdir)?; - let opts = Options { - root: Some(rootdir), - }; - let opts_file = OpenOptions::new() - .read(true) - .create(true) - .truncate(true) - .write(true) - .open(dir.path().join("options.json"))?; - write!(&opts_file, "{}", serde_json::to_string(&opts)?)?; - - let wasm_path = dir.path().join("rootfs/hello.wasm"); - let mut f = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .mode(0o755) - .open(wasm_path)?; - f.write_all(&wasmbytes)?; - - let stdout = File::create(dir.path().join("stdout"))?; - drop(stdout); - - let spec = SpecBuilder::default() - .root(RootBuilder::default().path("rootfs").build()?) - .process( - ProcessBuilder::default() - .cwd("/") - .args(vec!["./hello.wasm".to_string()]) - .build()?, - ) - .build()?; - - spec.save(dir.path().join("config.json"))?; - - let mut cfg = InstanceConfig::new( - Wasi::new_engine()?, - "test_namespace".into(), - "/containerd/address".into(), - ); - let cfg = cfg - .set_bundle(dir.path().to_str().unwrap().to_string()) - .set_stdout(dir.path().join("stdout").to_str().unwrap().to_string()); - - let wasi = Wasi::new("test".to_string(), Some(cfg)); - - wasi.start()?; - - let (tx, rx) = channel(); - let waiter = Wait::new(tx); - wasi.wait(&waiter).unwrap(); - - let res = match rx.recv_timeout(Duration::from_secs(10)) { - Ok(res) => Ok(res), - Err(e) => { - wasi.kill(SIGKILL as u32).unwrap(); - return Err(Error::Others(format!( - "error waiting for module to finish: {0}", - e - ))); - } - }; - wasi.delete()?; - res - } - #[test] #[serial] fn test_delete_after_create() { @@ -432,7 +354,7 @@ mod wasitest { let path = dir.path(); let wasmbytes = wat2wasm(WASI_HELLO_WAT).unwrap(); - let res = run_wasi_test(&dir, wasmbytes)?; + let res = run_wasi_test::(&dir, wasmbytes, None)?; assert_eq!(res.0, 0); @@ -454,7 +376,7 @@ mod wasitest { let dir = tempdir()?; let wasmbytes = wat2wasm(WASI_RETURN_ERROR).unwrap(); - let res = run_wasi_test(&dir, wasmbytes)?; + let res = run_wasi_test::(&dir, wasmbytes, None)?; // Expect error code from the run. assert_eq!(res.0, 137); diff --git a/crates/containerd-shim-wasmtime/Cargo.toml b/crates/containerd-shim-wasmtime/Cargo.toml index 24fa3a7b7..54455fbb9 100644 --- a/crates/containerd-shim-wasmtime/Cargo.toml +++ b/crates/containerd-shim-wasmtime/Cargo.toml @@ -42,6 +42,7 @@ tempfile = "3.7" libc = { workspace = true } pretty_assertions = "1" env_logger = "0.10" +serial_test = "*" [[bin]] name = "containerd-shim-wasmtime-v1" diff --git a/crates/containerd-shim-wasmtime/src/instance.rs b/crates/containerd-shim-wasmtime/src/instance.rs index 0385cb5bf..f5e6d7b2e 100644 --- a/crates/containerd-shim-wasmtime/src/instance.rs +++ b/crates/containerd-shim-wasmtime/src/instance.rs @@ -264,55 +264,55 @@ impl EngineGetter for Wasi { #[cfg(test)] mod wasitest { - use std::fs::{create_dir, read_to_string, File, OpenOptions}; - use std::io::prelude::*; - use std::os::unix::prelude::OpenOptionsExt; - use std::sync::mpsc::channel; - use std::time::Duration; - use containerd_shim_wasm::function; use containerd_shim_wasm::sandbox::exec::has_cap_sys_admin; - use containerd_shim_wasm::sandbox::instance::Wait; - use containerd_shim_wasm::sandbox::testutil::run_test_with_sudo; - use oci_spec::runtime::{ProcessBuilder, RootBuilder, SpecBuilder}; - use tempfile::{tempdir, TempDir}; + use containerd_shim_wasm::sandbox::testutil::{run_test_with_sudo, run_wasi_test}; + use serial_test::serial; + use std::fs::read_to_string; + use tempfile::tempdir; use super::*; // This is taken from https://github.com/bytecodealliance/wasmtime/blob/6a60e8363f50b936e4c4fc958cb9742314ff09f3/docs/WASI-tutorial.md?plain=1#L270-L298 - const WASI_HELLO_WAT: &[u8]= r#"(module - ;; Import the required fd_write WASI function which will write the given io vectors to stdout - ;; The function signature for fd_write is: - ;; (File Descriptor, *iovs, iovs_len, nwritten) -> Returns number of bytes written - (import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32))) - - (memory 1) - (export "memory" (memory 0)) - - ;; Write 'hello world\n' to memory at an offset of 8 bytes - ;; Note the trailing newline which is required for the text to appear - (data (i32.const 8) "hello world\n") - - (func $main (export "_start") - ;; Creating a new io vector within linear memory - (i32.store (i32.const 0) (i32.const 8)) ;; iov.iov_base - This is a pointer to the start of the 'hello world\n' string - (i32.store (i32.const 4) (i32.const 12)) ;; iov.iov_len - The length of the 'hello world\n' string - - (call $fd_write - (i32.const 1) ;; file_descriptor - 1 for stdout - (i32.const 0) ;; *iovs - The pointer to the iov array, which is stored at memory location 0 - (i32.const 1) ;; iovs_len - We're printing 1 string stored in an iov - so one. - (i32.const 20) ;; nwritten - A place in memory to store the number of bytes written + fn hello_world_module(start_fn: Option<&str>) -> Vec { + let start_fn = start_fn.unwrap_or("_start"); + format!(r#"(module + ;; Import the required fd_write WASI function which will write the given io vectors to stdout + ;; The function signature for fd_write is: + ;; (File Descriptor, *iovs, iovs_len, nwritten) -> Returns number of bytes written + (import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32))) + + (memory 1) + (export "memory" (memory 0)) + + ;; Write 'hello world\n' to memory at an offset of 8 bytes + ;; Note the trailing newline which is required for the text to appear + (data (i32.const 8) "hello world\n") + + (func $main (export "{start_fn}") + ;; Creating a new io vector within linear memory + (i32.store (i32.const 0) (i32.const 8)) ;; iov.iov_base - This is a pointer to the start of the 'hello world\n' string + (i32.store (i32.const 4) (i32.const 12)) ;; iov.iov_len - The length of the 'hello world\n' string + + (call $fd_write + (i32.const 1) ;; file_descriptor - 1 for stdout + (i32.const 0) ;; *iovs - The pointer to the iov array, which is stored at memory location 0 + (i32.const 1) ;; iovs_len - We're printing 1 string stored in an iov - so one. + (i32.const 20) ;; nwritten - A place in memory to store the number of bytes written + ) + drop ;; Discard the number of bytes written from the top of the stack ) - drop ;; Discard the number of bytes written from the top of the stack ) - ) - "#.as_bytes(); + "#).as_bytes().to_vec() + } #[test] fn test_delete_after_create() -> Result<()> { - let dir = tempdir()?; - let cfg = prepare_cfg(&dir)?; + let cfg = InstanceConfig::new( + Wasi::new_engine()?, + "test_namespace".into(), + "/containerd/address".into(), + ); let i = Wasi::new("".to_string(), Some(&cfg)); i.delete()?; @@ -321,92 +321,30 @@ mod wasitest { } #[test] - fn test_wasi() -> Result<(), Error> { + #[serial] + fn test_wasi_entrypoint() -> Result<(), Error> { if !has_cap_sys_admin() { println!("running test with sudo: {}", function!()); return run_test_with_sudo(function!()); } // start logging + // to enable logging run `export RUST_LOG=trace` and append cargo command with --show-output + // before running test let _ = env_logger::try_init(); let dir = tempdir()?; - let cfg = prepare_cfg(&dir)?; - - let wasi = Wasi::new("test".to_string(), Some(&cfg)); + let path = dir.path(); - wasi.start()?; + let module = hello_world_module(None); - let (tx, rx) = channel(); - let waiter = Wait::new(tx); - wasi.wait(&waiter).unwrap(); + let res = run_wasi_test::(&dir, module.into(), None)?; - let res = match rx.recv_timeout(Duration::from_secs(10)) { - Ok(res) => res, - Err(e) => { - wasi.kill(SIGKILL as u32).unwrap(); - return Err(Error::Others(format!( - "error waiting for module to finish: {0}", - e - ))); - } - }; assert_eq!(res.0, 0); - let output = read_to_string(dir.path().join("stdout"))?; + let output = read_to_string(path.join("stdout"))?; assert_eq!(output, "hello world\n"); - wasi.delete()?; - reset_stdio(); Ok(()) } - - fn prepare_cfg(dir: &TempDir) -> Result> { - create_dir(dir.path().join("rootfs"))?; - - let opts = Options { - root: Some(dir.path().join("runwasi")), - }; - let opts_file = OpenOptions::new() - .read(true) - .create(true) - .truncate(true) - .write(true) - .open(dir.path().join("options.json"))?; - write!(&opts_file, "{}", serde_json::to_string(&opts)?)?; - - let wasm_path = dir.path().join("rootfs/hello.wat"); - let mut f = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .mode(0o755) - .open(wasm_path)?; - f.write_all(WASI_HELLO_WAT)?; - - let stdout = File::create(dir.path().join("stdout"))?; - let stderr = File::create(dir.path().join("stderr"))?; - drop(stdout); - drop(stderr); - let spec = SpecBuilder::default() - .root(RootBuilder::default().path("rootfs").build()?) - .process( - ProcessBuilder::default() - .cwd("/") - .args(vec!["./hello.wat".to_string()]) - .build()?, - ) - .build()?; - spec.save(dir.path().join("config.json"))?; - let mut cfg = InstanceConfig::new( - Engine::default(), - "test_namespace".into(), - "/containerd/address".into(), - ); - let cfg = cfg - .set_bundle(dir.path().to_str().unwrap().to_string()) - .set_stdout(dir.path().join("stdout").to_str().unwrap().to_string()) - .set_stderr(dir.path().join("stderr").to_str().unwrap().to_string()); - Ok(cfg.to_owned()) - } }