diff --git a/Cargo.lock b/Cargo.lock index aa517b5f0..bc4e8ec7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,7 @@ dependencies = [ "tempdir", "vdso", "walkdir", + "wat", "x86_64", ] @@ -325,6 +326,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "leb128" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3576a87f2ba00f6f106fdfcd16db1d698d648a26ad8e0573cad8537c3c362d2a" + [[package]] name = "libc" version = "0.2.103" @@ -870,6 +877,24 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wast" +version = "38.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0d7b256bef26c898fa7344a2d627e8499f5a749432ce0a05eae1a64ff0c271" +dependencies = [ + "leb128", +] + +[[package]] +name = "wat" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcfaeb27e2578d2c6271a45609f4a055e6d7ba3a12eff35b1fd5ba147bdf046" +dependencies = [ + "wast", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index ed4842971..318e73598 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ vdso = "0.1" [build-dependencies] cc = "1.0" +wat = "1.0" walkdir = "2" protobuf-codegen-pure = "2.25" sallyport = { git = "https://github.com/enarx/sallyport", rev = "a567a22665c7e5ba88a8c4acd64ab43ee32b4681", features = [ "asm" ] } diff --git a/build.rs b/build.rs index a6adff48d..1e9b98722 100644 --- a/build.rs +++ b/build.rs @@ -107,6 +107,17 @@ fn build_cc_tests(in_path: &Path, out_path: &Path) { } } +fn build_wasm_tests(in_path: &Path, out_path: &Path) { + for wat in find_files_with_extensions(&["wat"], &in_path) { + let wasm = out_path + .join(wat.file_stem().unwrap()) + .with_extension("wasm"); + let bin = wat::parse_file(&wat).unwrap_or_else(|_| panic!("failed to compile {:?}", &wat)); + std::fs::write(&wasm, &bin).unwrap_or_else(|_| panic!("failed to write {:?}", &wasm)); + println!("cargo:rerun-if-changed={}", &wat.display()); + } +} + // Build a binary named `bin_name` from the crate located at `in_dir`, // targeting `target_name`, then strip the resulting binary and place it // at `out_dir`/bin/`bin_name`. @@ -235,6 +246,7 @@ fn main() -> Result<(), Box> { build_cc_tests(&Path::new(CRATE).join(TEST_BINS_IN), &out_dir_bin); build_rs_tests(&Path::new(CRATE).join(TEST_BINS_IN), &out_dir_bin); + build_wasm_tests(&Path::new(CRATE).join("tests/wasm"), &out_dir_bin); let target = "x86_64-unknown-linux-musl"; diff --git a/internal/wasmldr/build.rs b/internal/wasmldr/build.rs deleted file mode 100644 index a70310fc4..000000000 --- a/internal/wasmldr/build.rs +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -use std::path::Path; - -fn main() { - let in_dir = Path::new("fixtures"); - let out_dir = - std::env::var_os("OUT_DIR").expect("The OUT_DIR environment variable must be set"); - let out_dir = Path::new(&out_dir).join("fixtures"); - std::fs::create_dir_all(&out_dir).expect("Can't create output directory"); - - for entry in in_dir.read_dir().unwrap().flatten() { - let wat = entry.path(); - match wat.extension() { - Some(ext) if ext == "wat" => { - let wasm = out_dir - .join(wat.file_name().unwrap()) - .with_extension("wasm"); - let binary = wat::parse_file(&wat).expect("Can't parse wat file"); - std::fs::write(&wasm, &binary).expect("Can't write wasm file"); - println!("cargo:rerun-if-changed={}", &wat.display()); - } - _ => {} - } - } -} diff --git a/internal/wasmldr/fixtures/bundle/config.yaml b/internal/wasmldr/fixtures/bundle/config.yaml deleted file mode 100644 index 6b4695fb7..000000000 --- a/internal/wasmldr/fixtures/bundle/config.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -stdio: - stdin: - bundle: stdin.txt - stdout: - file: stdout.txt - stderr: - file: stderr.txt diff --git a/internal/wasmldr/fixtures/bundle/stdin.txt b/internal/wasmldr/fixtures/bundle/stdin.txt deleted file mode 100644 index cd0875583..000000000 --- a/internal/wasmldr/fixtures/bundle/stdin.txt +++ /dev/null @@ -1 +0,0 @@ -Hello world! diff --git a/internal/wasmldr/fixtures/hello_wasi_snapshot1.wat b/tests/wasm/hello_wasi_snapshot1.wat similarity index 100% rename from internal/wasmldr/fixtures/hello_wasi_snapshot1.wat rename to tests/wasm/hello_wasi_snapshot1.wat diff --git a/internal/wasmldr/fixtures/no_export.wat b/tests/wasm/no_export.wat similarity index 100% rename from internal/wasmldr/fixtures/no_export.wat rename to tests/wasm/no_export.wat diff --git a/internal/wasmldr/fixtures/return_1.wat b/tests/wasm/return_1.wat similarity index 100% rename from internal/wasmldr/fixtures/return_1.wat rename to tests/wasm/return_1.wat diff --git a/internal/wasmldr/fixtures/wasi_snapshot1.wat b/tests/wasm/wasi_snapshot1.wat similarity index 100% rename from internal/wasmldr/fixtures/wasi_snapshot1.wat rename to tests/wasm/wasi_snapshot1.wat diff --git a/tests/wasmldr_tests.rs b/tests/wasmldr_tests.rs new file mode 100644 index 000000000..24463ca1e --- /dev/null +++ b/tests/wasmldr_tests.rs @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: Apache-2.0 + +use process_control::{ChildExt, Output, Timeout}; +use std::fs::File; +use std::os::unix::io::{IntoRawFd, RawFd}; +use std::os::unix::process::CommandExt; +use std::path::Path; +use std::process::{Command, Stdio}; + +extern crate libc; +use libc::c_int; + +use std::io; +use std::io::Write; +use std::time::Duration; + +mod common; +use common::{check_output, CRATE, KEEP_BIN, OUT_DIR, TEST_BINS_OUT, TIMEOUT_SECS}; + +use serial_test::serial; + +const MODULE_FD: RawFd = 3; + +// wrap a libc call to return io::Result +fn cvt(rv: c_int) -> io::Result { + if rv == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(rv) + } +} + +// wrap a libc call to return io::Result<()> +fn cv(rv: c_int) -> io::Result<()> { + cvt(rv).and(Ok(())) +} + +trait CommandFdExt { + fn inherit_with_fd(&mut self, file: impl IntoRawFd, child_fd: RawFd) -> &mut Self; +} + +impl CommandFdExt for Command { + fn inherit_with_fd(&mut self, file: impl IntoRawFd, child_fd: RawFd) -> &mut Self { + let fd = file.into_raw_fd(); + if fd == child_fd { + unsafe { + self.pre_exec(move || cv(libc::fcntl(fd, libc::F_SETFD, 0))); + } + } else { + unsafe { + self.pre_exec(move || cv(libc::dup2(fd, child_fd))); + } + } + self + } +} + +pub fn wasmldr_exec<'a>(wasm: &str, input: impl Into>) -> Output { + let wasm_path = Path::new(CRATE) + .join(OUT_DIR) + .join(TEST_BINS_OUT) + .join(wasm); + let wasm_file = + File::open(wasm_path).unwrap_or_else(|e| panic!("failed to open `{}`: {:#?}", wasm, e)); + + let mut child = Command::new(&String::from(KEEP_BIN)) + .current_dir(CRATE) + .arg("exec") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .inherit_with_fd(wasm_file, MODULE_FD) + .spawn() + .unwrap_or_else(|e| panic!("failed to run `{}`: {:#?}", wasm, e)); + + if let Some(input) = input.into() { + child + .stdin + .as_mut() + .unwrap() + .write_all(input) + .expect("failed to write stdin to child"); + + drop(child.stdin.take()); + } + + let output = child + .with_output_timeout(Duration::from_secs(TIMEOUT_SECS)) + .terminating() + .wait() + .unwrap_or_else(|e| panic!("failed to run `{}`: {:#?}", wasm, e)) + .unwrap_or_else(|| panic!("process `{}` timed out", wasm)); + + assert!( + output.status.code().is_some(), + "process `{}` terminated by signal {:?}", + wasm, + output.status.signal() + ); + + output +} + +fn run_wasm_test<'a>( + wasm: &str, + status: i32, + input: impl Into>, + expected_stdout: impl Into>, + expected_stderr: impl Into>, +) -> Output { + let output = wasmldr_exec(wasm, input); + check_output(&output, status, expected_stdout, expected_stderr); + output +} + +#[test] +#[serial] +fn return_1() { + // This module does, in fact, return 1. But function return values + // are separate from setting the process exit status code, so + // we still expect a return code of '0' here. + run_wasm_test("return_1.wasm", 0, None, None, None); +} + +#[test] +#[serial] +fn wasi_snapshot1() { + // This module uses WASI to return the number of commandline args. + // Since we don't currently do anything with the function return value, + // we don't get any output here, and we expect '0', as above. + run_wasm_test("wasi_snapshot1.wasm", 0, None, None, None); +} + +#[test] +#[serial] +fn hello_wasi_snapshot1() { + // This module just prints "Hello, world!" to stdout. Hooray! + run_wasm_test( + "hello_wasi_snapshot1.wasm", + 0, + None, + &b"Hello, world!\n"[..], + None, + ); +} + +#[test] +#[serial] +fn no_export() { + // This module has no exported functions, so we get Error::ExportNotFound, + // which wasmldr maps to EX_DATAERR (65) at process exit. + run_wasm_test("no_export.wasm", 65, None, None, None); +}