#![cfg(not(miri))] use std::fs::File; use std::io::Write; use std::path::Path; use std::process::{Command, ExitStatus, Output, Stdio}; use tempfile::{NamedTempFile, TempDir}; use wasmtime::{Result, bail}; // Run the wasmtime CLI with the provided args and return the `Output`. // If the `stdin` is `Some`, opens the file and redirects to the child's stdin. pub fn run_wasmtime_for_output(args: &[&str], stdin: Option<&Path>) -> Result { let mut cmd = get_wasmtime_command()?; cmd.args(args); if let Some(file) = stdin { cmd.stdin(File::open(file)?); } cmd.output().map_err(Into::into) } /// Get the Wasmtime CLI as a [Command]. pub fn get_wasmtime_command() -> Result { let mut cmd = wasmtime_test_util::command(get_wasmtime_path()); // Ignore this if it's specified in the environment to allow tests to run in // "default mode" by default. cmd.env_remove("WASMTIME_NEW_CLI"); Ok(cmd) } fn get_wasmtime_path() -> &'static str { env!("CARGO_BIN_EXE_wasmtime") } // Run the wasmtime CLI with the provided args and, if it succeeds, return // the standard output in a `String`. pub fn run_wasmtime(args: &[&str]) -> Result { let output = run_wasmtime_for_output(args, None)?; if !output.status.success() { bail!( "Failed to execute wasmtime with: {:?}\nstatus: {}\n{}", args, output.status, String::from_utf8_lossy(&output.stderr) ); } Ok(String::from_utf8(output.stdout).unwrap()) } fn build_wasm(wat_path: impl AsRef) -> Result { let mut wasm_file = NamedTempFile::new()?; let wasm = wat::parse_file(wat_path)?; wasm_file.write(&wasm)?; Ok(wasm_file) } // Very basic use case: compile binary wasm file and run specific function with arguments. #[test] fn run_wasmtime_simple() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/simple.wat")?; run_wasmtime(&[ "run", "--invoke", "simple", "-Ccache=n", wasm.path().to_str().unwrap(), "4", ])?; Ok(()) } // Wasmtime shall fail when not enough arguments were provided. #[test] fn run_wasmtime_simple_fail_no_args() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/simple.wat")?; assert!( run_wasmtime(&[ "run", "-Ccache=n", "--invoke", "simple", wasm.path().to_str().unwrap(), ]) .is_err(), "shall fail" ); Ok(()) } #[test] fn run_coredump_smoketest() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/coredump_smoketest.wat")?; let coredump_file = NamedTempFile::new()?; let coredump_arg = format!("-Dcoredump={}", coredump_file.path().display()); let err = run_wasmtime(&[ "run", "--invoke", "a", "-Ccache=n", &coredump_arg, wasm.path().to_str().unwrap(), ]) .unwrap_err(); assert!(err.to_string().contains(&format!( "core dumped at {}", coredump_file.path().display() ))); Ok(()) } // Running simple wat #[test] fn run_wasmtime_simple_wat() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/simple.wat")?; run_wasmtime(&[ "run", "--invoke", "simple", "-Ccache=n", wasm.path().to_str().unwrap(), "4", ])?; assert_eq!( run_wasmtime(&[ "run", "--invoke", "get_f32", "-Ccache=n", wasm.path().to_str().unwrap(), ])?, "100\n" ); assert_eq!( run_wasmtime(&[ "run", "--invoke", "get_f64", "-Ccache=n", wasm.path().to_str().unwrap(), ])?, "100\n" ); Ok(()) } // Running a wat that traps. #[test] fn run_wasmtime_unreachable_wat() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/unreachable.wat")?; let output = run_wasmtime_for_output(&[wasm.path().to_str().unwrap(), "-Ccache=n"], None)?; assert_ne!(output.stderr, b""); assert_eq!(output.stdout, b""); assert_trap_code(&output.status); Ok(()) } fn assert_trap_code(status: &ExitStatus) { let code = status .code() .expect("wasmtime process should exit normally"); // Test for the specific error code Wasmtime uses to indicate a trap return. #[cfg(unix)] assert_eq!(code, 128 + libc::SIGABRT); #[cfg(windows)] assert_eq!(code, 3); } // Run a simple WASI hello world, snapshot0 edition. #[test] fn hello_wasi_snapshot0() -> Result<()> { for preview2 in ["-Spreview2=n", "-Spreview2=y"] { let stdout = run_wasmtime(&[ "-Ccache=n", preview2, "tests/all/cli_tests/hello_wasi_snapshot0.wat", ])?; assert_eq!(stdout, "Hello, world!\n"); } Ok(()) } // Run a simple WASI hello world, snapshot1 edition. #[test] fn hello_wasi_snapshot1() -> Result<()> { let stdout = run_wasmtime(&["-Ccache=n", "tests/all/cli_tests/hello_wasi_snapshot1.wat"])?; assert_eq!(stdout, "Hello, world!\n"); Ok(()) } #[test] fn timeout_in_start() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/iloop-start.wat")?; let output = run_wasmtime_for_output( &[ "run", "-Wtimeout=1ms", "-Ccache=n", wasm.path().to_str().unwrap(), ], None, )?; assert!(!output.status.success()); assert_eq!(output.stdout, b""); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("wasm trap: interrupt"), "bad stderr: {stderr}" ); Ok(()) } #[test] fn timeout_in_invoke() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/iloop-invoke.wat")?; let output = run_wasmtime_for_output( &[ "run", "-Wtimeout=1ms", "-Ccache=n", wasm.path().to_str().unwrap(), ], None, )?; assert!(!output.status.success()); assert_eq!(output.stdout, b""); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("wasm trap: interrupt"), "bad stderr: {stderr}" ); Ok(()) } // Exit with a valid non-zero exit code, snapshot0 edition. #[test] fn exit2_wasi_snapshot0() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/exit2_wasi_snapshot0.wat")?; for preview2 in ["-Spreview2=n", "-Spreview2=y"] { let output = run_wasmtime_for_output( &["-Ccache=n", preview2, wasm.path().to_str().unwrap()], None, )?; assert_eq!(output.status.code().unwrap(), 2); } Ok(()) } // Exit with a valid non-zero exit code, snapshot1 edition. #[test] fn exit2_wasi_snapshot1() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/exit2_wasi_snapshot1.wat")?; let output = run_wasmtime_for_output(&["-Ccache=n", wasm.path().to_str().unwrap()], None)?; assert_eq!(output.status.code().unwrap(), 2); Ok(()) } // Exit with a valid non-zero exit code, snapshot0 edition. #[test] fn exit125_wasi_snapshot0() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/exit125_wasi_snapshot0.wat")?; for preview2 in ["-Spreview2=n", "-Spreview2=y"] { let output = run_wasmtime_for_output( &["-Ccache=n", preview2, wasm.path().to_str().unwrap()], None, )?; dbg!(&output); assert_eq!(output.status.code().unwrap(), 125); } Ok(()) } // Exit with a valid non-zero exit code, snapshot1 edition. #[test] fn exit125_wasi_snapshot1() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/exit125_wasi_snapshot1.wat")?; let output = run_wasmtime_for_output(&["-Ccache=n", wasm.path().to_str().unwrap()], None)?; assert_eq!(output.status.code().unwrap(), 125); Ok(()) } // Exit with an invalid non-zero exit code, snapshot0 edition. #[test] fn exit126_wasi_snapshot0() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/exit126_wasi_snapshot0.wat")?; for preview2 in ["-Spreview2=n", "-Spreview2=y"] { let output = run_wasmtime_for_output( &["-Ccache=n", preview2, wasm.path().to_str().unwrap()], None, )?; assert_eq!(output.status.code().unwrap(), 1); assert!(output.stdout.is_empty()); assert!(String::from_utf8_lossy(&output.stderr).contains("invalid exit status")); } Ok(()) } // Exit with an invalid non-zero exit code, snapshot1 edition. #[test] fn exit126_wasi_snapshot1() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/exit126_wasi_snapshot1.wat")?; let output = run_wasmtime_for_output(&[wasm.path().to_str().unwrap(), "-Ccache=n"], None)?; assert_eq!(output.status.code().unwrap(), 1); assert!(output.stdout.is_empty()); assert!(String::from_utf8_lossy(&output.stderr).contains("invalid exit status")); Ok(()) } // Run a minimal command program. #[test] fn minimal_command() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/minimal-command.wat")?; let stdout = run_wasmtime(&["-Ccache=n", wasm.path().to_str().unwrap()])?; assert_eq!(stdout, ""); Ok(()) } // Run a minimal reactor program. #[test] fn minimal_reactor() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/minimal-reactor.wat")?; let stdout = run_wasmtime(&["-Ccache=n", wasm.path().to_str().unwrap()])?; assert_eq!(stdout, ""); Ok(()) } // Attempt to call invoke on a command. #[test] fn command_invoke() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/minimal-command.wat")?; run_wasmtime(&[ "run", "--invoke", "_start", "-Ccache=n", wasm.path().to_str().unwrap(), ])?; Ok(()) } // Attempt to call invoke on a command. #[test] fn reactor_invoke() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/minimal-reactor.wat")?; run_wasmtime(&[ "run", "--invoke", "_initialize", "-Ccache=n", wasm.path().to_str().unwrap(), ])?; Ok(()) } // Run the greeter test, which runs a preloaded reactor and a command. #[test] fn greeter() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/greeter_command.wat")?; let stdout = run_wasmtime(&[ "run", "-Ccache=n", "--preload", "reactor=tests/all/cli_tests/greeter_reactor.wat", wasm.path().to_str().unwrap(), ])?; assert_eq!( stdout, "Hello _initialize\nHello _start\nHello greet\nHello done\n" ); Ok(()) } // Run the greeter test, but this time preload a command. #[test] fn greeter_preload_command() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/greeter_reactor.wat")?; let stdout = run_wasmtime(&[ "run", "-Ccache=n", "--preload", "reactor=tests/all/cli_tests/hello_wasi_snapshot1.wat", wasm.path().to_str().unwrap(), ])?; assert_eq!(stdout, "Hello _initialize\n"); Ok(()) } // Run the greeter test, which runs a preloaded reactor and a command. #[test] fn greeter_preload_callable_command() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/greeter_command.wat")?; let stdout = run_wasmtime(&[ "run", "-Ccache=n", "--preload", "reactor=tests/all/cli_tests/greeter_callable_command.wat", wasm.path().to_str().unwrap(), ])?; assert_eq!(stdout, "Hello _start\nHello callable greet\nHello done\n"); Ok(()) } // Ensure successful WASI exit call with FPR saving frames on stack for Windows x64 // See https://github.com/bytecodealliance/wasmtime/issues/1967 #[test] fn exit_with_saved_fprs() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/exit_with_saved_fprs.wat")?; let output = run_wasmtime_for_output(&["-Ccache=n", wasm.path().to_str().unwrap()], None)?; assert_eq!(output.status.code().unwrap(), 0); assert!(output.stdout.is_empty()); Ok(()) } #[test] fn run_cwasm() -> Result<()> { let td = TempDir::new()?; let cwasm = td.path().join("foo.cwasm"); let stdout = run_wasmtime(&[ "compile", "tests/all/cli_tests/simple.wat", "-o", cwasm.to_str().unwrap(), ])?; assert_eq!(stdout, ""); let stdout = run_wasmtime(&["run", "--allow-precompiled", cwasm.to_str().unwrap()])?; assert_eq!(stdout, ""); Ok(()) } #[cfg(unix)] #[test] fn hello_wasi_snapshot0_from_stdin() -> Result<()> { // Run a simple WASI hello world, snapshot0 edition. // The module is piped from standard input. let wasm = build_wasm("tests/all/cli_tests/hello_wasi_snapshot0.wat")?; for preview2 in ["-Spreview2=n", "-Spreview2=y"] { let stdout = { let path = wasm.path(); let args: &[&str] = &["-Ccache=n", preview2, "-"]; let output = run_wasmtime_for_output(args, Some(path))?; if !output.status.success() { bail!( "Failed to execute wasmtime with: {:?}\n{}", args, String::from_utf8_lossy(&output.stderr) ); } Ok::<_, wasmtime::Error>(String::from_utf8(output.stdout).unwrap()) }?; assert_eq!(stdout, "Hello, world!\n"); } Ok(()) } #[test] fn specify_env() -> Result<()> { // By default no env is inherited let output = get_wasmtime_command()? .args(&["run", "tests/all/cli_tests/print_env.wat"]) .env("THIS_WILL_NOT", "show up in the output") .output()?; assert!(output.status.success()); assert_eq!(String::from_utf8_lossy(&output.stdout), ""); // Specify a single env var let output = get_wasmtime_command()? .args(&[ "run", "--env", "FOO=bar", "tests/all/cli_tests/print_env.wat", ]) .output()?; assert!(output.status.success()); assert_eq!(String::from_utf8_lossy(&output.stdout), "FOO=bar\n"); // Inherit a single env var let output = get_wasmtime_command()? .args(&["run", "--env", "FOO", "tests/all/cli_tests/print_env.wat"]) .env("FOO", "bar") .output()?; assert!(output.status.success()); assert_eq!(String::from_utf8_lossy(&output.stdout), "FOO=bar\n"); // Inherit a nonexistent env var let output = get_wasmtime_command()? .args(&[ "run", "--env", "SURELY_THIS_ENV_VAR_DOES_NOT_EXIST_ANYWHERE_RIGHT", "tests/all/cli_tests/print_env.wat", ]) .output()?; assert!(output.status.success()); // Inherit all env vars let output = get_wasmtime_command()? .args(&["run", "-Sinherit-env", "tests/all/cli_tests/print_env.wat"]) .env("FOO", "bar") .output()?; assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("FOO=bar"), "bad output: {stdout}"); Ok(()) } #[cfg(unix)] #[test] fn run_cwasm_from_stdin() -> Result<()> { use std::process::Stdio; let td = TempDir::new()?; let cwasm = td.path().join("foo.cwasm"); let stdout = run_wasmtime(&[ "compile", "tests/all/cli_tests/simple.wat", "-o", cwasm.to_str().unwrap(), ])?; assert_eq!(stdout, ""); // If stdin is literally the file itself then that should work let args: &[&str] = &["run", "--allow-precompiled", "-"]; let output = get_wasmtime_command()? .args(args) .stdin(File::open(&cwasm)?) .output()?; assert!(output.status.success(), "a file as stdin should work"); // If stdin is a pipe, that should also work let input = std::fs::read(&cwasm)?; let mut child = get_wasmtime_command()? .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; let mut stdin = child.stdin.take().unwrap(); let t = std::thread::spawn(move || { let _ = stdin.write_all(&input); }); let output = child.wait_with_output()?; assert!(output.status.success()); t.join().unwrap(); Ok(()) } #[cfg(feature = "wasi-threads")] #[test] fn run_threads() -> Result<()> { // Only run threaded tests on platforms that support threads. Also skip // these tests with ASAN as it, rightfully, complains about a memory leak. // The memory leak at this time is that child threads aren't joined with the // main thread, meaning that allocations done on child threads are indeed // leaked. if crate::threads::engine().is_none() || cfg!(asan) { return Ok(()); } let wasm = build_wasm("tests/all/cli_tests/threads.wat")?; let stdout = run_wasmtime(&[ "run", "-Wthreads,shared-memory", "-Sthreads", "-Ccache=n", wasm.path().to_str().unwrap(), ])?; assert!( stdout == "Called _start\n\ Running wasi_thread_start\n\ Running wasi_thread_start\n\ Running wasi_thread_start\n\ Done\n" ); Ok(()) } #[cfg(feature = "wasi-threads")] #[test] fn run_simple_with_wasi_threads() -> Result<()> { // Skip this test on platforms that don't support threads. if crate::threads::engine().is_none() { return Ok(()); } // We expect to be able to run Wasm modules that do not have correct // wasi-thread entry points or imported shared memory as long as no threads // are spawned. let wasm = build_wasm("tests/all/cli_tests/simple.wat")?; let stdout = run_wasmtime(&[ "run", "-Wthreads", "-Sthreads", "-Ccache=n", "--invoke", "simple", wasm.path().to_str().unwrap(), "4", ])?; assert_eq!(stdout, "4\n"); Ok(()) } #[test] fn wasm_flags() -> Result<()> { // Any argument after the wasm module should be interpreted as for the // command itself let stdout = run_wasmtime(&[ "run", "--", "tests/all/cli_tests/print-arguments.wat", "--argument", "-for", "the", "command", ])?; assert_eq!( stdout, "\ print-arguments.wat\n\ --argument\n\ -for\n\ the\n\ command\n\ " ); let stdout = run_wasmtime(&["run", "--", "tests/all/cli_tests/print-arguments.wat", "-"])?; assert_eq!( stdout, "\ print-arguments.wat\n\ -\n\ " ); let stdout = run_wasmtime(&["run", "--", "tests/all/cli_tests/print-arguments.wat", "--"])?; assert_eq!( stdout, "\ print-arguments.wat\n\ --\n\ " ); let stdout = run_wasmtime(&[ "run", "--", "tests/all/cli_tests/print-arguments.wat", "--", "--", "-a", "b", ])?; assert_eq!( stdout, "\ print-arguments.wat\n\ --\n\ --\n\ -a\n\ b\n\ " ); Ok(()) } #[test] fn name_same_as_builtin_command() -> Result<()> { // a bare subcommand shouldn't run successfully let output = get_wasmtime_command()? .current_dir("tests/all/cli_tests") .arg("run") .output()?; assert!(!output.status.success()); // a `--` prefix should let everything else get interpreted as a wasm // module and arguments, even if the module has a name like `run` let output = get_wasmtime_command()? .current_dir("tests/all/cli_tests") .arg("--") .arg("run") .output()?; assert!(output.status.success(), "expected success got {output:#?}"); // Passing options before the subcommand should work and doesn't require // `--` to disambiguate let output = get_wasmtime_command()? .current_dir("tests/all/cli_tests") .arg("-Ccache=n") .arg("run") .output()?; assert!(output.status.success(), "expected success got {output:#?}"); Ok(()) } #[test] #[cfg(unix)] fn run_just_stdin_argument() -> Result<()> { let output = get_wasmtime_command()? .arg("-") .stdin(File::open("tests/all/cli_tests/simple.wat")?) .output()?; assert!(output.status.success()); Ok(()) } #[test] fn wasm_flags_without_subcommand() -> Result<()> { let output = get_wasmtime_command()? .current_dir("tests/all/cli_tests/") .arg("print-arguments.wat") .arg("-foo") .arg("bar") .output()?; assert!(output.status.success()); assert_eq!( String::from_utf8_lossy(&output.stdout), "\ print-arguments.wat\n\ -foo\n\ bar\n\ " ); Ok(()) } #[test] fn wasi_misaligned_pointer() -> Result<()> { let output = get_wasmtime_command()? .arg("./tests/all/cli_tests/wasi_misaligned_pointer.wat") .output()?; assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("Pointer not aligned"), "bad stderr: {stderr}", ); Ok(()) } #[test] #[cfg_attr(not(feature = "component-model"), ignore)] fn hello_with_preview2() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/hello_wasi_snapshot1.wat")?; let stdout = run_wasmtime(&["-Ccache=n", "-Spreview2", wasm.path().to_str().unwrap()])?; assert_eq!(stdout, "Hello, world!\n"); Ok(()) } #[test] #[cfg_attr(not(feature = "component-model"), ignore)] fn component_missing_feature() -> Result<()> { let path = "tests/all/cli_tests/empty-component.wat"; let wasm = build_wasm(path)?; let output = get_wasmtime_command()? .arg("-Ccache=n") .arg("-Wcomponent-model=n") .arg(wasm.path()) .output()?; assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("cannot execute a component without `--wasm component-model`"), "bad stderr: {stderr}" ); // also tests with raw *.wat input let output = get_wasmtime_command()? .arg("-Ccache=n") .arg("-Wcomponent-model=n") .arg(path) .output()?; assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("cannot execute a component without `--wasm component-model`"), "bad stderr: {stderr}" ); Ok(()) } #[test] #[cfg_attr(not(feature = "component-model"), ignore)] fn component_enabled_by_default() -> Result<()> { let path = "tests/all/cli_tests/component-basic.wat"; let wasm = build_wasm(path)?; let output = get_wasmtime_command()? .arg("-Ccache=n") .arg(wasm.path()) .output()?; assert!(output.status.success()); // also tests with raw *.wat input let output = get_wasmtime_command()? .arg("-Ccache=n") .arg(path) .output()?; assert!(output.status.success()); Ok(()) } // If the text format is invalid then the filename should be mentioned in the // error message. #[test] fn bad_text_syntax() -> Result<()> { let output = get_wasmtime_command()? .arg("-Ccache=n") .arg("tests/all/cli_tests/bad-syntax.wat") .output()?; assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("--> tests/all/cli_tests/bad-syntax.wat"), "bad stderr: {stderr}" ); Ok(()) } #[test] #[cfg_attr(not(feature = "component-model"), ignore)] fn run_basic_component() -> Result<()> { let path = "tests/all/cli_tests/component-basic.wat"; let wasm = build_wasm(path)?; // Run both the `*.wasm` binary and the text format run_wasmtime(&[ "-Ccache=n", "-Wcomponent-model", wasm.path().to_str().unwrap(), ])?; run_wasmtime(&["-Ccache=n", "-Wcomponent-model", path])?; Ok(()) } #[test] #[cfg_attr(not(feature = "component-model"), ignore)] fn run_precompiled_component() -> Result<()> { let td = TempDir::new()?; let cwasm = td.path().join("component-basic.cwasm"); let stdout = run_wasmtime(&[ "compile", "tests/all/cli_tests/component-basic.wat", "-o", cwasm.to_str().unwrap(), "-Wcomponent-model", ])?; assert_eq!(stdout, ""); let stdout = run_wasmtime(&[ "run", "-Wcomponent-model", "--allow-precompiled", cwasm.to_str().unwrap(), ])?; assert_eq!(stdout, ""); Ok(()) } // Disable test on s390x because the large allocation may actually succeed; // the whole 64-bit address space is available on this platform. #[test] #[cfg(not(target_arch = "s390x"))] fn memory_growth_failure() -> Result<()> { let output = get_wasmtime_command()? .args(&[ "run", "-Wmemory64", "-Wtrap-on-grow-failure", "tests/all/cli_tests/memory-grow-failure.wat", ]) .output()?; let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("forcing a memory growth failure to be a trap"), "bad stderr: {stderr}" ); assert!(!output.status.success()); Ok(()) } #[test] fn table_growth_failure() -> Result<()> { let output = get_wasmtime_command()? .args(&[ "run", "-Wtrap-on-grow-failure", "tests/all/cli_tests/table-grow-failure.wat", ]) .output()?; assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("forcing trap when growing table"), "bad stderr: {stderr}" ); Ok(()) } #[test] fn table_growth_failure2() -> Result<()> { let output = get_wasmtime_command()? .args(&[ "run", "-Wtrap-on-grow-failure", "tests/all/cli_tests/table-grow-failure2.wat", ]) .output()?; assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); let expected = if cfg!(target_pointer_width = "32") { "overflow calculating new table size" } else { "forcing trap when growing table to 4294967296 elements" }; assert!(stderr.contains(expected), "bad stderr: {stderr}"); Ok(()) } #[test] fn option_group_help() -> Result<()> { run_wasmtime(&["run", "-Whelp"])?; run_wasmtime(&["run", "-O", "help"])?; run_wasmtime(&["run", "--codegen", "help"])?; run_wasmtime(&["run", "--debug=help"])?; run_wasmtime(&["run", "-Shelp"])?; run_wasmtime(&["run", "-Whelp-long"])?; Ok(()) } #[test] fn option_group_comma_separated() -> Result<()> { run_wasmtime(&[ "run", "-Wrelaxed-simd,simd", "tests/all/cli_tests/simple.wat", ])?; Ok(()) } #[test] fn option_group_boolean_parsing() -> Result<()> { run_wasmtime(&["run", "-Wrelaxed-simd", "tests/all/cli_tests/simple.wat"])?; run_wasmtime(&["run", "-Wrelaxed-simd=n", "tests/all/cli_tests/simple.wat"])?; run_wasmtime(&["run", "-Wrelaxed-simd=y", "tests/all/cli_tests/simple.wat"])?; run_wasmtime(&["run", "-Wrelaxed-simd=no", "tests/all/cli_tests/simple.wat"])?; run_wasmtime(&[ "run", "-Wrelaxed-simd=yes", "tests/all/cli_tests/simple.wat", ])?; run_wasmtime(&[ "run", "-Wrelaxed-simd=true", "tests/all/cli_tests/simple.wat", ])?; run_wasmtime(&[ "run", "-Wrelaxed-simd=false", "tests/all/cli_tests/simple.wat", ])?; Ok(()) } #[test] fn preview2_stdin() -> Result<()> { let test = "tests/all/cli_tests/count-stdin.wat"; let cmd = || -> Result<_> { let mut cmd = get_wasmtime_command()?; cmd.arg("--invoke=count").arg("-Spreview2").arg(test); Ok(cmd) }; // read empty pipe is ok let output = cmd()?.output()?; assert!(output.status.success()); assert_eq!(String::from_utf8_lossy(&output.stdout), "0\n"); // read itself is ok let file = File::open(test)?; let size = file.metadata()?.len(); let output = cmd()?.stdin(File::open(test)?).output()?; assert!(output.status.success()); assert_eq!(String::from_utf8_lossy(&output.stdout), format!("{size}\n")); // read piped input ok is ok let mut child = cmd()? .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; let mut stdin = child.stdin.take().unwrap(); std::thread::spawn(move || { stdin.write_all(b"hello").unwrap(); }); let output = child.wait_with_output()?; assert!(output.status.success()); assert_eq!(String::from_utf8_lossy(&output.stdout), "5\n"); let count_up_to = |n: usize| -> Result<_> { let mut child = get_wasmtime_command()? .arg("--invoke=count-up-to") .arg("-Spreview2") .arg(test) .arg(n.to_string()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; let mut stdin = child.stdin.take().unwrap(); let t = std::thread::spawn(move || { let mut written = 0; let bytes = [0; 64 * 1024]; loop { written += match stdin.write(&bytes) { Ok(n) => n, Err(_) => break written, }; } }); let output = child.wait_with_output()?; assert!(output.status.success()); let written = t.join().unwrap(); let read = String::from_utf8_lossy(&output.stdout) .trim() .parse::() .unwrap(); // The test reads in 1000 byte chunks so make sure that it doesn't read // more than 1000 bytes than requested. assert!(read < n + 1000, "test read too much {read}"); Ok(written) }; // wasmtime shouldn't eat information that the guest never actually tried to // read. // // NB: this may be a bit flaky. Exactly how much we wrote in the above // helper thread depends on how much the OS buffers for us. For now give // some some slop and assume that OSes are unlikely to buffer more than // that. let slop = 256 * 1024; for amt in [0, 100, 100_000] { let written = count_up_to(amt)?; assert!(written < slop + amt, "wrote too much {written}"); } Ok(()) } #[test] fn float_args() -> Result<()> { let result = run_wasmtime(&[ "--invoke", "echo_f32", "tests/all/cli_tests/simple.wat", "1.0", ])?; assert_eq!(result, "1\n"); let result = run_wasmtime(&[ "--invoke", "echo_f64", "tests/all/cli_tests/simple.wat", "1.1", ])?; assert_eq!(result, "1.1\n"); Ok(()) } #[test] fn mpk_without_pooling() -> Result<()> { let output = get_wasmtime_command()? .args(&[ "run", "-O", "memory-protection-keys=y", "--invoke", "echo_f32", "tests/all/cli_tests/simple.wat", "1.0", ]) .env("WASMTIME_NEW_CLI", "1") .output()?; assert!(!output.status.success()); Ok(()) } // Very basic use case: compile binary wasm file and run specific function with arguments. #[test] fn increase_stack_size() -> Result<()> { run_wasmtime(&[ "run", "--invoke", "simple", &format!("-Wmax-wasm-stack={}", 5 << 20), "-Ccache=n", "tests/all/cli_tests/simple.wat", "4", ])?; Ok(()) } mod test_programs { use super::{get_wasmtime_command, run_wasmtime}; use http_body_util::BodyExt; use hyper::header::HeaderValue; use std::io::{self, BufRead, BufReader, Read, Write}; use std::iter; use std::net::SocketAddr; use std::process::{Child, Command, Stdio}; use std::thread::{self, JoinHandle}; use test_programs_artifacts::*; use tokio::net::TcpStream; use wasmtime::{Result, bail, error::Context as _, format_err}; macro_rules! assert_test_exists { ($name:ident) => { #[expect(unused_imports, reason = "just here to assert the test is here")] use self::$name as _; }; } foreach_cli!(assert_test_exists); #[test] fn p2_cli_hello_stdout() -> Result<()> { run_wasmtime(&["run", "-Wcomponent-model", P2_CLI_HELLO_STDOUT_COMPONENT])?; Ok(()) } #[test] fn p2_cli_args() -> Result<()> { run_wasmtime(&[ "run", "-Wcomponent-model", P2_CLI_ARGS_COMPONENT, "hello", "this", "", "is an argument", "with 🚩 emoji", ])?; Ok(()) } #[test] fn p2_cli_stdin_empty() -> Result<()> { let mut child = get_wasmtime_command()? .args(&["run", "-Wcomponent-model", P2_CLI_STDIN_EMPTY_COMPONENT]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .stdin(Stdio::piped()) .spawn()?; child .stdin .take() .unwrap() .write_all(b"not to be read") .unwrap(); let output = child.wait_with_output()?; println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); assert!(output.status.success()); Ok(()) } #[test] fn p2_cli_stdin() -> Result<()> { let mut child = get_wasmtime_command()? .args(&["run", "-Wcomponent-model", P2_CLI_STDIN_COMPONENT]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .stdin(Stdio::piped()) .spawn()?; child .stdin .take() .unwrap() .write_all(b"So rested he by the Tumtum tree") .unwrap(); let output = child.wait_with_output()?; println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); assert!(output.status.success()); Ok(()) } #[test] fn p2_cli_splice_stdin() -> Result<()> { let mut child = get_wasmtime_command()? .args(&["run", "-Wcomponent-model", P2_CLI_SPLICE_STDIN_COMPONENT]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .stdin(Stdio::piped()) .spawn()?; let msg = "So rested he by the Tumtum tree"; child .stdin .take() .unwrap() .write_all(msg.as_bytes()) .unwrap(); let output = child.wait_with_output()?; assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); if !stderr.is_empty() { eprintln!("{stderr}"); } assert_eq!( format!( "before splice\n{msg}\ncompleted splicing {} bytes\n", msg.as_bytes().len() ), stdout ); Ok(()) } #[test] fn p2_cli_env() -> Result<()> { run_wasmtime(&[ "run", "-Wcomponent-model", "--env=frabjous=day", "--env=callooh=callay", P2_CLI_ENV_COMPONENT, ])?; Ok(()) } #[test] fn p2_cli_file_read() -> Result<()> { let dir = tempfile::tempdir()?; std::fs::write(dir.path().join("bar.txt"), b"And stood awhile in thought")?; run_wasmtime(&[ "run", "-Wcomponent-model", &format!("--dir={}::/", dir.path().to_str().unwrap()), P2_CLI_FILE_READ_COMPONENT, ])?; Ok(()) } #[test] fn p2_cli_file_append() -> Result<()> { let dir = tempfile::tempdir()?; std::fs::File::create(dir.path().join("bar.txt"))? .write_all(b"'Twas brillig, and the slithy toves.\n")?; run_wasmtime(&[ "run", "-Wcomponent-model", &format!("--dir={}::/", dir.path().to_str().unwrap()), P2_CLI_FILE_APPEND_COMPONENT, ])?; let contents = std::fs::read(dir.path().join("bar.txt"))?; assert_eq!( std::str::from_utf8(&contents).unwrap(), "'Twas brillig, and the slithy toves.\n\ Did gyre and gimble in the wabe;\n\ All mimsy were the borogoves,\n\ And the mome raths outgrabe.\n" ); Ok(()) } #[test] fn p2_cli_file_dir_sync() -> Result<()> { let dir = tempfile::tempdir()?; std::fs::File::create(dir.path().join("bar.txt"))? .write_all(b"'Twas brillig, and the slithy toves.\n")?; run_wasmtime(&[ "run", "-Wcomponent-model", &format!("--dir={}::/", dir.path().to_str().unwrap()), P2_CLI_FILE_DIR_SYNC_COMPONENT, ])?; Ok(()) } #[test] fn p2_cli_exit_success() -> Result<()> { run_wasmtime(&["run", "-Wcomponent-model", P2_CLI_EXIT_SUCCESS_COMPONENT])?; Ok(()) } #[test] fn p2_cli_exit_default() -> Result<()> { run_wasmtime(&["run", "-Wcomponent-model", P2_CLI_EXIT_DEFAULT_COMPONENT])?; Ok(()) } #[test] fn p2_cli_exit_failure() -> Result<()> { let output = get_wasmtime_command()? .args(&["run", "-Wcomponent-model", P2_CLI_EXIT_FAILURE_COMPONENT]) .output()?; assert!(!output.status.success()); assert_eq!(output.status.code(), Some(1)); Ok(()) } #[test] fn p2_cli_exit_with_code() -> Result<()> { let output = get_wasmtime_command()? .args(&[ "run", "-Wcomponent-model", "-Scli-exit-with-code", P2_CLI_EXIT_WITH_CODE_COMPONENT, ]) .output()?; assert!(!output.status.success()); assert_eq!(output.status.code(), Some(42)); Ok(()) } #[test] fn p2_cli_exit_panic() -> Result<()> { let output = get_wasmtime_command()? .args(&["run", "-Wcomponent-model", P2_CLI_EXIT_PANIC_COMPONENT]) .output()?; assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("Curiouser and curiouser!")); Ok(()) } #[test] fn p2_cli_directory_list() -> Result<()> { let dir = tempfile::tempdir()?; std::fs::File::create(dir.path().join("foo.txt"))?; std::fs::File::create(dir.path().join("bar.txt"))?; std::fs::File::create(dir.path().join("baz.txt"))?; std::fs::create_dir(dir.path().join("sub"))?; std::fs::File::create(dir.path().join("sub").join("wow.txt"))?; std::fs::File::create(dir.path().join("sub").join("yay.txt"))?; run_wasmtime(&[ "run", "-Wcomponent-model", &format!("--dir={}::/", dir.path().to_str().unwrap()), P2_CLI_DIRECTORY_LIST_COMPONENT, ])?; Ok(()) } #[test] fn p2_cli_default_clocks() -> Result<()> { run_wasmtime(&["run", "-Wcomponent-model", P2_CLI_DEFAULT_CLOCKS_COMPONENT])?; Ok(()) } #[test] fn p2_cli_export_cabi_realloc() -> Result<()> { run_wasmtime(&[ "run", "-Wcomponent-model", P2_CLI_EXPORT_CABI_REALLOC_COMPONENT, ])?; Ok(()) } #[test] fn run_wasi_http_component() -> Result<()> { let output = super::run_wasmtime_for_output( &[ "-Ccache=no", "-Wcomponent-model", "-Scli,http,preview2", P2_HTTP_OUTBOUND_REQUEST_RESPONSE_BUILD_COMPONENT, ], None, )?; println!("{}", String::from_utf8_lossy(&output.stderr)); let stdout = String::from_utf8_lossy(&output.stdout); println!("{stdout}"); assert!(stdout.starts_with("Called _start\n")); assert!(stdout.ends_with("Done\n")); assert!(output.status.success()); Ok(()) } // Test to ensure that prints in the guest aren't buffered on the host by // accident. The test here will print something without a newline and then // wait for input on stdin, and the test here is to ensure that the // character shows up here even as the guest is waiting on input via stdin. #[test] fn p2_cli_stdio_write_flushes() -> Result<()> { fn run(args: &[&str]) -> Result<()> { println!("running {args:?}"); let mut child = get_wasmtime_command()? .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; let mut stdout = child.stdout.take().unwrap(); let mut buf = [0; 10]; match stdout.read(&mut buf) { Ok(2) => assert_eq!(&buf[..2], b"> "), e => panic!("unexpected read result {e:?}"), } drop(stdout); drop(child.stdin.take().unwrap()); let status = child.wait()?; assert!(status.success()); Ok(()) } run(&["run", "-Spreview2=n", P2_CLI_STDIO_WRITE_FLUSHES])?; run(&["run", "-Spreview2=y", P2_CLI_STDIO_WRITE_FLUSHES])?; run(&[ "run", "-Wcomponent-model", P2_CLI_STDIO_WRITE_FLUSHES_COMPONENT, ])?; Ok(()) } #[test] fn p2_cli_no_tcp() -> Result<()> { let output = super::run_wasmtime_for_output( &[ "-Wcomponent-model", // Turn on network but turn off TCP "-Sinherit-network,tcp=no", P2_CLI_NO_TCP_COMPONENT, ], None, )?; println!("{}", String::from_utf8_lossy(&output.stderr)); assert!(output.status.success()); Ok(()) } #[test] fn p2_cli_no_udp() -> Result<()> { let output = super::run_wasmtime_for_output( &[ "-Wcomponent-model", // Turn on network but turn off UDP "-Sinherit-network,udp=no", P2_CLI_NO_UDP_COMPONENT, ], None, )?; println!("{}", String::from_utf8_lossy(&output.stderr)); assert!(output.status.success()); Ok(()) } #[test] fn p2_cli_no_ip_name_lookup() -> Result<()> { let output = super::run_wasmtime_for_output( &[ "-Wcomponent-model", // Turn on network but ensure name lookup is disabled "-Sinherit-network,allow-ip-name-lookup=no", P2_CLI_NO_IP_NAME_LOOKUP_COMPONENT, ], None, )?; println!("{}", String::from_utf8_lossy(&output.stderr)); assert!(output.status.success()); Ok(()) } #[test] fn p2_cli_sleep() -> Result<()> { run_wasmtime(&["run", P2_CLI_SLEEP])?; run_wasmtime(&["run", P2_CLI_SLEEP_COMPONENT])?; Ok(()) } #[test] fn p2_cli_sleep_forever() -> Result<()> { for timeout in [ // Tests still pass when we race with going to sleep. "-Wtimeout=1ns", // Tests pass when we wait till the Wasm has (likely) gone to sleep. "-Wtimeout=250ms", ] { let e = run_wasmtime(&["run", timeout, P2_CLI_SLEEP_FOREVER]).unwrap_err(); let e = e.to_string(); println!("Got error: {e}"); assert!(e.contains("interrupt")); let e = run_wasmtime(&["run", timeout, P2_CLI_SLEEP_FOREVER_COMPONENT]).unwrap_err(); let e = e.to_string(); println!("Got error: {e}"); assert!(e.contains("interrupt")); } Ok(()) } /// Helper structure to manage an invocation of `wasmtime serve` struct WasmtimeServe { child: Option, stdout: Option>>>, stderr: Option>>>, addr: SocketAddr, shutdown_addr: SocketAddr, } impl WasmtimeServe { /// Creates a new server which will serve the wasm component pointed to /// by `wasm`. /// /// A `configure` callback is provided to specify how `wasmtime serve` /// will be invoked and configure arguments such as headers. fn new(wasm: &str, configure: impl FnOnce(&mut Command)) -> Result { // Spawn `wasmtime serve` on port 0 which will randomly assign it a // port. let mut cmd = super::get_wasmtime_command()?; cmd.arg("serve").arg("--addr=127.0.0.1:0").arg(wasm); configure(&mut cmd); Self::spawn(&mut cmd) } fn spawn(cmd: &mut Command) -> Result { cmd.arg("--shutdown-addr=127.0.0.1:0"); cmd.stdin(Stdio::null()); cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); let mut child = cmd.spawn()?; // Read the first few lines of stderr which will say which address // it's listening on. The first line is the shutdown line (with // `--shutdown-addr`) and the second is what `--addr` was bound to. // This is done to figure out what `:0` was bound to in the child // process. let mut line = String::new(); let mut stderr = BufReader::new(child.stderr.take().unwrap()); let mut read_addr_from_line = |prefix: &str| -> Result { stderr.read_line(&mut line)?; if !line.starts_with(prefix) { bail!("input line `{line}` didn't start with `{prefix}`"); } match line.find("127.0.0.1").and_then(|addr_start| { let addr = &line[addr_start..]; let addr_end = addr.find("/")?; addr[..addr_end].parse().ok() }) { Some(addr) => { line.truncate(0); Ok(addr) } None => bail!("failed to address from: {line}"), } }; let shutdown_addr = read_addr_from_line("Listening for shutdown"); let addr = read_addr_from_line("Serving HTTP on"); let (shutdown_addr, addr) = match (shutdown_addr, addr) { (Ok(a), Ok(b)) => (a, b), // If either failed kill the child and otherwise try to shepherd // along any contextual information we have. (Err(a), _) | (_, Err(a)) => { child.kill()?; child.wait()?; stderr.read_to_string(&mut line)?; return Err(a.context(line)); } }; let mut stdout = child.stdout.take().unwrap(); Ok(WasmtimeServe { stdout: Some(thread::spawn(move || { let mut dst = Vec::new(); stdout.read_to_end(&mut dst).map(|_| dst) })), stderr: Some(thread::spawn(move || { let mut dst = Vec::new(); stderr.read_to_end(&mut dst).map(|_| dst) })), child: Some(child), addr, shutdown_addr, }) } /// Completes this server gracefully by printing the output on failure. fn finish(mut self) -> Result<(String, String)> { self._finish() } fn _finish(&mut self) -> Result<(String, String)> { let mut child = self.child.take().unwrap(); // If the child process has already exited, then great! Otherwise // the server is still running and it shouldn't be possible to exit // until a shutdown signal is sent, so do that here. Make a TCP // connection to the shutdown port which is used as a shutdown // signal. if child.try_wait()?.is_none() { std::net::TcpStream::connect(&self.shutdown_addr) .context("failed to initiate graceful shutdown")?; } // Regardless of whether we just shut the server down or whether it // was already shut down (e.g. panicked or similar), wait for the // result here. The result should succeed (e.g. 0 exit status), and // if it did then the stdout/stderr are the caller's problem. let mut output = child.wait_with_output()?; output.stdout = self.stdout.take().unwrap().join().unwrap()?; output.stderr = self.stderr.take().unwrap().join().unwrap()?; if !output.status.success() { bail!("child failed {output:?}"); } Ok(( String::from_utf8_lossy(&output.stdout).into_owned(), String::from_utf8_lossy(&output.stderr).into_owned(), )) } /// Send a request to this server and wait for the response. async fn send_request(&self, req: http::Request) -> Result> { let (mut send, conn_task) = self.start_requests().await?; let response = Self::send_request_with(&mut send, req).await?; drop(send); conn_task.await??; Ok(response) } async fn send_request_with( send: &mut hyper::client::conn::http1::SendRequest, req: http::Request, ) -> Result> { let response = send .send_request(req) .await .context("error sending request")?; let (parts, body) = response.into_parts(); let body = body.collect().await.context("failed to read body")?; assert!(body.trailers().is_none()); let body = std::str::from_utf8(&body.to_bytes())?.to_string(); Ok(http::Response::from_parts(parts, body)) } async fn start_requests( &self, ) -> Result<( hyper::client::conn::http1::SendRequest, tokio::task::JoinHandle>, )> { let tcp = TcpStream::connect(&self.addr) .await .context("failed to connect")?; let tcp = wasmtime_wasi_http::io::TokioIo::new(tcp); let (send, conn) = hyper::client::conn::http1::handshake(tcp) .await .context("failed http handshake")?; Ok((send, tokio::task::spawn(conn))) } } // Don't leave child processes running by accident so kill the child process // if our server goes away. impl Drop for WasmtimeServe { fn drop(&mut self) { match &mut self.child { Some(child) => match child.kill() { Ok(()) => {} Err(e) => { eprintln!("failed to kill child process {e}"); return; } }, None => return, } match self._finish() { Ok((stdout, stderr)) => { if !stdout.is_empty() { println!("server stdout:\n{stdout}"); } if !stderr.is_empty() { println!("server stderr:\n{stderr}"); } } Err(e) => println!("failed to wait for child or read stdio: {e}"), } } } #[tokio::test] async fn p2_cli_serve_echo_env() -> Result<()> { let server = WasmtimeServe::new(P2_CLI_SERVE_ECHO_ENV_COMPONENT, |cmd| { cmd.arg("--env=FOO=bar"); cmd.arg("--env=BAR"); cmd.arg("-Scli"); cmd.env_remove("BAR"); })?; let foo_env = server .send_request( hyper::Request::builder() .uri("http://localhost/") .header("env", "FOO") .body(String::new()) .context("failed to make request")?, ) .await?; assert!(foo_env.status().is_success()); assert!(foo_env.body().is_empty()); let headers = foo_env.headers(); assert_eq!(headers.get("env"), Some(&HeaderValue::from_static("bar"))); let bar_env = server .send_request( hyper::Request::builder() .uri("http://localhost/") .header("env", "BAR") .body(String::new()) .context("failed to make request")?, ) .await?; assert!(bar_env.status().is_success()); assert!(bar_env.body().is_empty()); let headers = bar_env.headers(); assert_eq!(headers.get("env"), None); server.finish()?; Ok(()) } #[tokio::test] async fn p2_cli_serve_outgoing_body_config() -> Result<()> { let server = WasmtimeServe::new(P2_CLI_SERVE_ECHO_ENV_COMPONENT, |cmd| { cmd.arg("-Scli"); cmd.arg("-Shttp-outgoing-body-buffer-chunks=2"); cmd.arg("-Shttp-outgoing-body-chunk-size=1024"); })?; let resp = server .send_request( hyper::Request::builder() .uri("http://localhost/") .header("env", "FOO") .body(String::new()) .context("failed to make request")?, ) .await?; assert!(resp.status().is_success()); server.finish()?; Ok(()) } #[tokio::test] #[ignore] // TODO: printing stderr in the child and killing the child at the // end of this test race so the stderr may be present or not. Need // to implement a more graceful shutdown routine for `wasmtime // serve`. async fn p2_cli_serve_respect_pooling_options() -> Result<()> { let server = WasmtimeServe::new(P2_CLI_SERVE_ECHO_ENV_COMPONENT, |cmd| { cmd.arg("-Opooling-total-memories=0").arg("-Scli"); })?; let result = server .send_request( hyper::Request::builder() .uri("http://localhost/") .header("env", "FOO") .body(String::new()) .context("failed to make request")?, ) .await; assert!(result.is_err()); let (_, stderr) = server.finish()?; assert!( stderr.contains("maximum concurrent memory limit of 0 reached"), "bad stderr: {stderr}", ); Ok(()) } #[test] fn p2_cli_large_env() -> Result<()> { for wasm in [P2_CLI_LARGE_ENV, P2_CLI_LARGE_ENV_COMPONENT] { println!("run {wasm:?}"); let mut cmd = get_wasmtime_command()?; cmd.arg("run").arg("-Sinherit-env").arg(wasm); let debug_cmd = format!("{cmd:?}"); for i in 0..512 { let var = format!("KEY{i}"); let val = (0..1024).map(|_| 'x').collect::(); cmd.env(&var, &val); } let output = cmd.output()?; if !output.status.success() { bail!( "Failed to execute wasmtime with: {debug_cmd}\n{}", String::from_utf8_lossy(&output.stderr) ); } } Ok(()) } #[tokio::test] async fn p2_cli_serve_only_one_process_allowed() -> Result<()> { let wasm = P2_CLI_SERVE_ECHO_ENV_COMPONENT; let server = WasmtimeServe::new(wasm, |cmd| { cmd.arg("-Scli"); })?; let err = WasmtimeServe::spawn( super::get_wasmtime_command()? .arg("serve") .arg("-Scli") .arg(format!("--addr={}", server.addr)) .arg(wasm), ) .err() .expect("server spawn should have failed but it succeeded"); drop(server); let err = format!("{err:?}"); println!("{err}"); assert!(err.contains("os error")); Ok(()) } // Technically this test is a little racy. This binds port 0 to acquire a // random port, issues a single request to this port, but then kills this // server while the request is still processing. The port is then rebound // in the next process while it technically could be stolen by another // process. #[tokio::test] async fn p2_cli_serve_quick_rebind_allowed() -> Result<()> { let wasm = P2_CLI_SERVE_ECHO_ENV_COMPONENT; let server = WasmtimeServe::new(wasm, |cmd| { cmd.arg("-Scli"); })?; let addr = server.addr; // Start up a `send` and `conn_task` which represents a connection to // this server. let (mut send, conn_task) = server.start_requests().await?; let _ = send .send_request( hyper::Request::builder() .uri("http://localhost/") .header("env", "FOO") .body(String::new()) .context("failed to make request")?, ) .await; // ... once a response has been received (or at least the status // code/headers) then kill the server. THis is done while `conn_task` // and `send` are still alive so we're guaranteed that the other side // got a request (we got a response) and our connection is still open. // // This forces the address/port into the `TIME_WAIT` state. The rebind // below in the next process will fail if `SO_REUSEADDR` isn't set. drop(server); drop(send); let _ = conn_task.await; // If this is successfully bound then we'll create `WasmtimeServe` // which reads off the first line of output to know which address was // bound. let _server2 = WasmtimeServe::spawn( super::get_wasmtime_command()? .arg("serve") .arg("-Scli") .arg(format!("--addr={addr}")) .arg(wasm), )?; Ok(()) } #[tokio::test] async fn p2_cli_serve_with_print() -> Result<()> { let server = WasmtimeServe::new(P2_CLI_SERVE_WITH_PRINT_COMPONENT, |cmd| { cmd.arg("-Scli"); })?; for _ in 0..2 { let resp = server .send_request( hyper::Request::builder() .uri("http://localhost/") .body(String::new()) .context("failed to make request")?, ) .await?; assert!(resp.status().is_success()); } let (out, err) = server.finish()?; assert_eq!( out, "\ stdout [0] :: this is half a print to stdout stdout [0] :: \n\ stdout [0] :: after empty stdout [1] :: this is half a print to stdout stdout [1] :: \n\ stdout [1] :: after empty " ); assert!( err.contains( "\ stderr [0] :: this is half a print to stderr stderr [0] :: \n\ stderr [0] :: after empty stderr [0] :: start a print 1234 stderr [1] :: this is half a print to stderr stderr [1] :: \n\ stderr [1] :: after empty stderr [1] :: start a print 1234 " ), "bad stderr: {err}" ); Ok(()) } #[tokio::test] async fn p2_cli_serve_with_print_no_prefix() -> Result<()> { let server = WasmtimeServe::new(P2_CLI_SERVE_WITH_PRINT_COMPONENT, |cmd| { cmd.arg("-Scli"); cmd.arg("--no-logging-prefix"); })?; for _ in 0..2 { let resp = server .send_request( hyper::Request::builder() .uri("http://localhost/") .body(String::new()) .context("failed to make request")?, ) .await?; assert!(resp.status().is_success()); } let (out, err) = server.finish()?; assert_eq!( out, "\ this is half a print to stdout \n\ after empty this is half a print to stdout \n\ after empty " ); assert!( err.contains( "\ this is half a print to stderr \n\ after empty start a print 1234 this is half a print to stderr \n\ after empty start a print 1234 " ), "bad stderr {err}", ); Ok(()) } #[tokio::test] async fn p2_cli_serve_authority_and_scheme() -> Result<()> { let server = WasmtimeServe::new(P2_CLI_SERVE_AUTHORITY_AND_SCHEME_COMPONENT, |cmd| { cmd.arg("-Scli"); })?; let resp = server .send_request( hyper::Request::builder() .uri("/") .header("Host", "localhost") .body(String::new()) .context("failed to make request")?, ) .await?; assert!(resp.status().is_success()); let resp = server .send_request( hyper::Request::builder() .method("CONNECT") .uri("http://localhost/") .body(String::new()) .context("failed to make request")?, ) .await?; assert!(resp.status().is_success()); Ok(()) } #[test] fn p2_cli_argv0() -> Result<()> { run_wasmtime(&["run", "--argv0=a", P2_CLI_ARGV0, "a"])?; run_wasmtime(&["run", "--argv0=b", P2_CLI_ARGV0_COMPONENT, "b"])?; run_wasmtime(&["run", "--argv0=foo.wasm", P2_CLI_ARGV0, "foo.wasm"])?; Ok(()) } #[tokio::test] async fn p2_cli_serve_config() -> Result<()> { let server = WasmtimeServe::new(P2_CLI_SERVE_CONFIG_COMPONENT, |cmd| { cmd.arg("-Scli"); cmd.arg("-Sconfig"); cmd.arg("-Sconfig-var=hello=world"); })?; let resp = server .send_request( hyper::Request::builder() .uri("http://localhost/") .body(String::new()) .context("failed to make request")?, ) .await?; assert!(resp.status().is_success()); assert_eq!(resp.body(), "world"); Ok(()) } #[test] fn p2_cli_config() -> Result<()> { run_wasmtime(&[ "run", "-Sconfig", "-Sconfig-var=hello=world", CONFIG_GET_COMPONENT, ])?; Ok(()) } #[tokio::test] async fn p2_cli_serve_keyvalue() -> Result<()> { let server = WasmtimeServe::new(P2_CLI_SERVE_KEYVALUE_COMPONENT, |cmd| { cmd.arg("-Scli"); cmd.arg("-Skeyvalue"); cmd.arg("-Skeyvalue-in-memory-data=hello=world"); })?; let resp = server .send_request( hyper::Request::builder() .uri("http://localhost/") .body(String::new()) .context("failed to make request")?, ) .await?; assert!(resp.status().is_success()); assert_eq!(resp.body(), "world"); Ok(()) } #[test] fn p2_cli_keyvalue() -> Result<()> { run_wasmtime(&[ "run", "-Skeyvalue", "-Skeyvalue-in-memory-data=atomics_key=5", KEYVALUE_MAIN_COMPONENT, ])?; Ok(()) } #[test] fn p2_cli_multiple_preopens() -> Result<()> { run_wasmtime(&[ "run", "--dir=/::/a", "--dir=/::/b", "--dir=/::/c", P2_CLI_MULTIPLE_PREOPENS_COMPONENT, ])?; Ok(()) } async fn p2_cli_serve_guest_never_invoked_set(wasm: &str) -> Result<()> { let server = WasmtimeServe::new(wasm, |cmd| { cmd.arg("-Scli"); })?; for _ in 0..2 { let res = server .send_request( hyper::Request::builder() .uri("http://localhost/") .body(String::new()) .context("failed to make request")?, ) .await .expect("got response from wasmtime"); assert_eq!(res.status(), http::StatusCode::INTERNAL_SERVER_ERROR); } let (stdout, stderr) = server.finish()?; println!("stdout: {stdout}"); println!("stderr: {stderr}"); assert!(stderr.contains("guest never invoked `response-outparam::set` method")); assert!(!stderr.contains("panicked")); Ok(()) } #[tokio::test] async fn p2_cli_serve_return_before_set() -> Result<()> { p2_cli_serve_guest_never_invoked_set(P2_CLI_SERVE_RETURN_BEFORE_SET_COMPONENT).await } #[tokio::test] async fn p2_cli_serve_trap_before_set() -> Result<()> { p2_cli_serve_guest_never_invoked_set(P2_CLI_SERVE_TRAP_BEFORE_SET_COMPONENT).await } #[test] fn p3_cli_hello_stdout() -> Result<()> { let output = run_wasmtime(&[ "run", "-Wcomponent-model-async", "-Sp3", P3_CLI_HELLO_STDOUT_COMPONENT, ]); if cfg!(feature = "component-model-async") { let output = output?; assert_eq!(output, "hello, world\n"); } else { assert!(output.is_err()); } Ok(()) } #[test] fn p2_cli_hello_stdout_invoke() -> Result<()> { println!("{P2_CLI_HELLO_STDOUT_COMPONENT}"); let output = run_wasmtime(&[ "run", "-Wcomponent-model", "--invoke", "run()", P2_CLI_HELLO_STDOUT_COMPONENT, ])?; // First this component prints "hello, world", then the invoke // result is printed as "ok". assert_eq!(output, "hello, world\nok\n"); Ok(()) } #[test] fn p3_cli_hello_stdout_post_return() -> Result<()> { let output = run_wasmtime(&[ "run", "-Wcomponent-model-async", "-Sp3", P3_CLI_HELLO_STDOUT_POST_RETURN_COMPONENT, ]); if cfg!(feature = "component-model-async") { let output = output?; assert_eq!(output, "hello, world\n"); } else { assert!(output.is_err()); } Ok(()) } #[test] fn p3_cli_hello_stdout_post_return_invoke() -> Result<()> { let output = run_wasmtime(&[ "run", "-Wcomponent-model-async", "-Sp3", "--invoke", "run()", P3_CLI_HELLO_STDOUT_POST_RETURN_COMPONENT, ]); if cfg!(feature = "component-model-async") { let output = output?; assert_eq!(output, "hello, world\nok\n"); } else { assert!(output.is_err()); } Ok(()) } #[test] fn p2_random_limits() -> Result<()> { println!("{P2_RANDOM_COMPONENT}"); // By default, p2_random.rs fits within the random limits - always // asks for 256 bytes. let output = run_wasmtime(&["run", P2_RANDOM_COMPONENT])?; assert_eq!(output, ""); // Lowering limit to 255 bytes should produce a trap in the first // call, which goes by way of the wasip1 adapter: let output = run_wasmtime(&["run", "-Smax-random-size=255", P2_RANDOM_COMPONENT]) .err() .ok_or_else(|| format_err!("execution with max-random-size=255 should trap"))?; let output = format!("{output:?}"); assert!( output.contains("lib_generated::random_get"), "expected error stack frames to contain 'wasip1::lib_generated::random_get'. Got:\n{output}" ); assert!( output.contains("requested len 256 exceeds limit 255"), "expected error stack frames to contain 'requested len 256 exceeds limit 255'. Got:\n{output}" ); // Lowering limit to 255 bytes and setting environment variable for // the first call to only request 255 bytes should be OK, and then the // program produce a trap in the second call, which calls the wasip2 // random import directly: let output = run_wasmtime(&[ "run", "-Smax-random-size=255", "--env", "TEST_P1_RANDOM_LEN=255", P2_RANDOM_COMPONENT, ]) .err() .ok_or_else(|| format_err!("execution with max-random-size=255 should trap"))?; let output = format!("{output:?}"); assert!( output.contains("random::random::get_random_bytes"), "expected error stack frames to contain 'wasi::random::random::get_random_bytes'. Got:\n{output}" ); assert!( output.contains("requested len 256 exceeds limit 255"), "expected error stack frames to contain 'requested len 256 exceeds limit 255'. Got:\n{output}" ); // Lowering limit to 255 bytes and setting environment variable for // the first and second calls to be at the limit should produce a trap // in the third call, which calls the wasip2 insecure random import: let output = run_wasmtime(&[ "run", "-Smax-random-size=255", "--env", "TEST_P1_RANDOM_LEN=255", "--env", "TEST_P2_RANDOM_LEN=255", P2_RANDOM_COMPONENT, ]) .err() .ok_or_else(|| format_err!("execution with max-random-size=255 should trap"))?; let output = format!("{output:?}"); assert!( output.contains("random::insecure::get_insecure_random_bytes"), "expected error stack frames to contain 'wasi::random::insecure::get_insecure_random_bytes'. Got:\n{output}" ); assert!( output.contains("requested len 256 exceeds limit 255"), "expected error stack frames to contain 'requested len 256 exceeds limit 255'. Got:\n{output}" ); // Lowering limit to 255 bytes and setting environment variable for // the first and second calls to be under the limit should trap in the // third call, which calls the wasip2 insecure random import: let output = run_wasmtime(&[ "run", "-Smax-random-size=255", "--env", "TEST_P1_RANDOM_LEN=255", "--env", "TEST_P2_RANDOM_LEN=255", "--env", "TEST_P2_INSECURE_RANDOM_LEN=255", P2_RANDOM_COMPONENT, ]) .context("setting all calls to be equal to the limit should pass")?; assert_eq!(output, ""); Ok(()) } #[test] fn p2_cli_http_headers() -> Result<()> { let td = tempfile::TempDir::new()?; let cwasm = td.path().join("http_headers.cwasm"); let cwasm = cwasm.to_str().unwrap(); run_wasmtime(&["compile", P2_CLI_HTTP_HEADERS_COMPONENT, "-o", cwasm])?; // p2 traps on too-many-fields/size-too-big, so expect error messages. for test in ["append", "append-empty", "append-same", "append-same-empty"] { let err = run_wasmtime(&[ "run", "-Shttp,p3", "-Smax-http-fields-size=1048576", "--allow-precompiled", cwasm, &format!("p2-{test}"), ]) .unwrap_err(); assert!( err.to_string() .contains("total size of fields exceeds limit") || err.to_string().contains("too many fields in the field map"), "bad error message: {err:?}" ); // gated by default too let err = run_wasmtime(&[ "run", "-Shttp,p3", "--allow-precompiled", cwasm, &format!("p2-{test}"), ]) .unwrap_err(); assert!( err.to_string() .contains("total size of fields exceeds limit"), "bad error message: {err:?}" ); } // With an extremely large limit Wasmtime still shouldn't panic (p2). let err = run_wasmtime(&[ "run", "-Shttp,p3", &format!("-Smax-http-fields-size={}", 1 << 30), "--allow-precompiled", cwasm, "p2-append", ]) .unwrap_err(); assert!( err.to_string().contains("too many fields in the field map"), "bad error message: {err:?}" ); // p3 returns an error code instead of trapping, so the guest // program exits successfully after receiving the error. for test in ["append", "append-empty", "append-same", "append-same-empty"] { let output = run_wasmtime(&[ "run", "-Shttp,p3", "-Smax-http-fields-size=1048576", "--allow-precompiled", cwasm, &format!("p3-{test}"), ])?; assert!( output.contains("error received"), "p3-{test}: guest should have received an error" ); // gated by default too let output = run_wasmtime(&[ "run", "-Shttp,p3", "--allow-precompiled", cwasm, &format!("p3-{test}"), ])?; assert!( output.contains("error received"), "p3-{test} (default limit): guest should have received an error" ); } // With an extremely large limit Wasmtime still shouldn't panic (p3). let output = run_wasmtime(&[ "run", "-Shttp,p3", &format!("-Smax-http-fields-size={}", 1 << 30), "--allow-precompiled", cwasm, "p3-append", ])?; assert!( output.contains("error received"), "p3-append (large limit): guest should have received an error" ); Ok(()) } #[test] #[cfg_attr(not(feature = "component-model-async"), ignore)] fn p2_cli_invoke_async() -> Result<()> { let output = run_wasmtime(&[ "run", "-Wcomponent-model-async", "--invoke", "echo(\"hello?\")", P2_CLI_INVOKE_ASYNC_COMPONENT, ])?; assert_eq!(output, "\"hello?\"\n"); Ok(()) } fn run_much_stdout(component: &str, extra_flags: &[&str]) -> Result<()> { let total_write_size = 1 << 18; let expected = iter::repeat('a').take(total_write_size).collect::(); for i in 10..15 { let string = iter::repeat('a').take(1 << i).collect::(); let times = (total_write_size >> i).to_string(); println!("writing {} bytes {times} times", string.len()); let mut args = Vec::new(); args.push("run"); args.extend_from_slice(extra_flags); args.push(component); args.push(&string); args.push(×); let output = run_wasmtime(&args)?; println!( "expected {} bytes, got {} bytes", expected.len(), output.len() ); assert!(output == expected); } Ok(()) } #[test] fn p1_cli_much_stdout() -> Result<()> { run_much_stdout(P1_CLI_MUCH_STDOUT_COMPONENT, &[]) } #[test] fn p2_cli_much_stdout() -> Result<()> { run_much_stdout(P2_CLI_MUCH_STDOUT_COMPONENT, &[]) } #[test] #[cfg_attr(not(feature = "component-model-async"), ignore)] fn p3_cli_much_stdout() -> Result<()> { run_much_stdout( P3_CLI_MUCH_STDOUT_COMPONENT, &["-Wcomponent-model-async", "-Sp3"], ) } #[test] fn p2_cli_max_resources() -> Result<()> { let err = run_wasmtime(&["run", "-Smax-resources=50", P2_CLI_MAX_RESOURCES_COMPONENT]) .unwrap_err(); assert!( err.to_string().contains("resource table has no free keys"), "bad error message: {err}" ); run_wasmtime(&["run", "-Smax-resources=200", P2_CLI_MAX_RESOURCES_COMPONENT])?; Ok(()) } #[tokio::test] #[cfg_attr(not(feature = "component-model-async"), ignore)] async fn p3_cli_serve_hello_world() -> Result<()> { cli_serve_hello_world(P3_CLI_SERVE_HELLO_WORLD_COMPONENT, 1, 1, |cmd| { cmd.arg("-Wcomponent-model-async"); cmd.arg("-Sp3,cli"); }) .await } #[tokio::test] async fn p2_cli_serve_hello_world() -> Result<()> { cli_serve_hello_world(P2_CLI_SERVE_HELLO_WORLD_COMPONENT, 1, 1, |cmd| { cmd.arg("-Scli"); }) .await } const CONNECTION_COUNT_MANY: usize = 20; const REQUESTS_PER_CONNECTION_MANY: usize = 5; #[tokio::test] #[cfg_attr(not(feature = "component-model-async"), ignore)] async fn p3_cli_serve_hello_world_many() -> Result<()> { cli_serve_hello_world( P3_CLI_SERVE_HELLO_WORLD_COMPONENT, CONNECTION_COUNT_MANY, REQUESTS_PER_CONNECTION_MANY, |cmd| { cmd.arg("-Wcomponent-model-async"); cmd.arg("-Sp3,cli"); }, ) .await } #[tokio::test] #[cfg_attr(not(feature = "component-model-async"), ignore)] async fn p3_cli_serve_hello_world_many_no_reuse() -> Result<()> { cli_serve_hello_world( P3_CLI_SERVE_HELLO_WORLD_COMPONENT, CONNECTION_COUNT_MANY, REQUESTS_PER_CONNECTION_MANY, |cmd| { cmd.arg("-Wcomponent-model-async"); cmd.arg("-Sp3,cli"); cmd.arg("--max-instance-reuse-count=1"); }, ) .await } #[tokio::test] #[cfg_attr(not(feature = "component-model-async"), ignore)] async fn p3_cli_serve_hello_world_many_no_concurrent_reuse() -> Result<()> { cli_serve_hello_world( P3_CLI_SERVE_HELLO_WORLD_COMPONENT, CONNECTION_COUNT_MANY, REQUESTS_PER_CONNECTION_MANY, |cmd| { cmd.arg("-Wcomponent-model-async"); cmd.arg("-Sp3,cli"); cmd.arg("--max-instance-concurrent-reuse-count=1"); }, ) .await } #[tokio::test] async fn p2_cli_serve_hello_world_many() -> Result<()> { cli_serve_hello_world( P2_CLI_SERVE_HELLO_WORLD_COMPONENT, CONNECTION_COUNT_MANY, REQUESTS_PER_CONNECTION_MANY, |cmd| { cmd.arg("-Scli"); }, ) .await } #[tokio::test] async fn p2_cli_serve_hello_world_many_with_reuse() -> Result<()> { cli_serve_hello_world( P2_CLI_SERVE_HELLO_WORLD_COMPONENT, CONNECTION_COUNT_MANY, REQUESTS_PER_CONNECTION_MANY, |cmd| { cmd.arg("-Scli"); cmd.arg("--max-instance-reuse-count=128"); }, ) .await } async fn cli_serve_hello_world( component: &str, connection_count: usize, requests_per_connection: usize, configure: impl FnOnce(&mut Command), ) -> Result<()> { let server = std::sync::Arc::new(WasmtimeServe::new(component, configure)?); let tasks = (0..connection_count).map({ let server = server.clone(); move |_| { tokio::task::spawn({ let server = server.clone(); async move { let (mut send, conn_task) = server.start_requests().await?; for _ in 0..requests_per_connection { let result = WasmtimeServe::send_request_with( &mut send, hyper::Request::builder() .uri("http://localhost/") .body(String::new()) .context("failed to make request")?, ) .await?; assert!(result.status().is_success()); assert_eq!(result.body(), "Hello, WASI!"); } drop(send); conn_task.await??; wasmtime::error::Ok(()) } }) } }); for task in tasks { task.await??; } std::sync::Arc::into_inner(server).unwrap().finish()?; Ok(()) } #[tokio::test] async fn p2_cli_serve_sleep() -> Result<()> { cli_serve_sleep(P2_CLI_SERVE_SLEEP_COMPONENT, 1, 1, |cmd| { cmd.arg("-Scli"); }) .await } #[tokio::test] #[cfg_attr(not(feature = "component-model-async"), ignore)] async fn p3_cli_serve_sleep() -> Result<()> { cli_serve_sleep(P3_CLI_SERVE_SLEEP_COMPONENT, 1, 1, |cmd| { cmd.arg("-Wcomponent-model-async"); cmd.arg("-Sp3,cli"); }) .await } #[tokio::test] async fn p2_cli_serve_sleep_many() -> Result<()> { cli_serve_sleep( P2_CLI_SERVE_SLEEP_COMPONENT, CONNECTION_COUNT_MANY, REQUESTS_PER_CONNECTION_MANY, |cmd| { cmd.arg("-Scli"); }, ) .await } #[tokio::test] #[cfg_attr(not(feature = "component-model-async"), ignore)] async fn p3_cli_serve_sleep_many() -> Result<()> { cli_serve_sleep( P3_CLI_SERVE_SLEEP_COMPONENT, CONNECTION_COUNT_MANY, REQUESTS_PER_CONNECTION_MANY, |cmd| { cmd.arg("-Wcomponent-model-async"); cmd.arg("-Sp3,cli"); }, ) .await } async fn cli_serve_sleep( component: &str, connection_count: usize, requests_per_connection: usize, configure: impl FnOnce(&mut Command), ) -> Result<()> { let server = std::sync::Arc::new(WasmtimeServe::new(component, move |cmd| { configure(cmd); cmd.arg("-Wtimeout=100us"); })?); let tasks = (0..connection_count).map({ let server = server.clone(); move |_| { tokio::task::spawn({ let server = server.clone(); async move { let (mut send, conn_task) = server.start_requests().await?; for _ in 0..requests_per_connection { let result = WasmtimeServe::send_request_with( &mut send, hyper::Request::builder() .uri("http://localhost/") .body(String::new()) .context("failed to make request")?, ) .await?; assert!(result.status().is_server_error()); } drop(send); conn_task.await??; wasmtime::error::Ok(()) } }) } }); for task in tasks { task.await??; } let (stdout, stderr) = std::sync::Arc::into_inner(server).unwrap().finish()?; assert_eq!(stdout, ""); assert!(stderr.contains("guest timed out"), "bad stderr: {stderr}"); Ok(()) } #[test] fn p2_cli_many_resources() -> Result<()> { let err = run_wasmtime(&[ "run", "-Smax-resources=100", P2_CLI_MANY_RESOURCES_COMPONENT, ]) .unwrap_err(); assert!( err.to_string().contains("resource table has no free keys"), "bad error message: {err}" ); Ok(()) } #[test] fn p3_cli_many_tasks() -> Result<()> { let err = run_wasmtime(&[ "run", "-Smax-resources=100", "-Sp3", "-Wcomponent-model-async", dbg!(P3_CLI_MANY_TASKS_COMPONENT), ]) .unwrap_err(); assert!( err.to_string().contains("resource table has no free keys"), "bad error message: {err}" ); Ok(()) } #[test] fn p1_cli_hostcall_fuel() -> Result<()> { let dir = tempfile::tempdir()?; run_wasmtime(&[ "run", &format!("--dir={}::.", dir.path().to_str().unwrap()), "-Shostcall-fuel=1000", P1_CLI_HOSTCALL_FUEL, ])?; Ok(()) } #[test] fn p2_cli_hostcall_fuel() -> Result<()> { enum Exit { Ok, NoFuel, TooManyZeroes, BufferTooLarge, } let dir = tempfile::tempdir()?; let file = dir.path().join("1mb"); std::fs::write(&file, vec![0; 1024 * 1024])?; for (arg, exit) in [ ("poll", Exit::NoFuel), ("read", Exit::Ok), ("write", Exit::NoFuel), ("mkdir", Exit::NoFuel), ("write-stream", Exit::NoFuel), ("write-stream-blocking", Exit::NoFuel), ("resolve", Exit::NoFuel), ("udp-send-many", Exit::NoFuel), ("udp-send-big", Exit::NoFuel), ("write-zeroes", Exit::TooManyZeroes), ("write-stream-buffer-too-large", Exit::BufferTooLarge), ("write-zeroes-buffer-too-large", Exit::BufferTooLarge), ("read-file-big", Exit::Ok), ("read-tcp-big", Exit::Ok), ] { println!("test: {arg}"); let result = run_wasmtime(&[ "run", "-Shostcall-fuel=5000", "-Sinherit-network", &format!("--dir={}::.", dir.path().to_str().unwrap()), P2_CLI_HOSTCALL_FUEL_COMPONENT, arg, ]); match exit { Exit::Ok => { result.unwrap(); } Exit::NoFuel => { let err = result.unwrap_err(); assert!( err.to_string() .contains("fuel allocated for hostcalls has been exhausted"), "bad error message: {err}" ); } Exit::TooManyZeroes => { let err = result.unwrap_err(); assert!( err.to_string() .contains("cannot write more zeroes than `check_write` allows"), "bad error message: {err}" ); } Exit::BufferTooLarge => { let err = result.unwrap_err(); assert!( err.to_string().contains("Buffer too large"), "bad error message: {err}" ); } } } Ok(()) } #[test] fn p3_cli_random_limits() -> Result<()> { let c = P3_CLI_RANDOM_LIMITS_COMPONENT; for rand in ["random", "insecure"] { run_wasmtime(&["run", "-Sp3", "-Wcomponent-model-async", c, rand, "256"])?; run_wasmtime(&[ "run", "-Sp3", "-Wcomponent-model-async", "-Smax-random-size=255", c, rand, "256", ])?; } Ok(()) } #[test] fn p3_cli_read_stdin() -> Result<()> { let mut cmd = get_wasmtime_command()?; let mut child = cmd .arg("-Sp3") .arg("-Wcomponent-model-async") .arg(P3_CLI_READ_STDIN_COMPONENT) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .unwrap(); child.stdin.take().unwrap().write_all(b"hello!").unwrap(); let output = child.wait_with_output()?; println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); assert!(output.status.success()); Ok(()) } } #[test] fn settings_command() -> Result<()> { // Skip this test on platforms that Cranelift doesn't support. if cranelift_native::builder().is_err() { return Ok(()); } let output = run_wasmtime(&["settings"])?; assert!(output.contains("Cranelift settings for target")); Ok(()) } #[cfg(target_arch = "x86_64")] #[test] fn profile_with_vtune() -> Result<()> { if !is_vtune_available() { println!("> `vtune` is not available on the system path; skipping test"); return Ok(()); } let mut bin = Command::new("vtune"); bin.args(&[ // Configure VTune... "-verbose", "-collect", "hotspots", "-user-data-dir", &std::env::temp_dir().to_string_lossy(), // ...then run Wasmtime with profiling enabled: get_wasmtime_path(), "--profile=vtune", "tests/all/cli_tests/simple.wat", ]); println!("> executing: {bin:?}"); let output = bin.output()?; let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); println!("> stdout:\n{stdout}"); println!("> stderr:\n{stderr}"); assert!(output.status.success()); assert!(!stderr.contains("Error")); assert!(stdout.contains("CPU Time")); Ok(()) } #[cfg(target_arch = "x86_64")] fn is_vtune_available() -> bool { Command::new("vtune").arg("-version").output().is_ok() } #[test] fn profile_guest() -> Result<()> { let tmpdir = std::env::temp_dir(); let dir = tmpdir.to_string_lossy(); let output = run_wasmtime_for_output( &[ &format!("--profile=guest,{dir}/out.json"), "--env", "FOO=bar", "tests/all/cli_tests/print_env.wat", ], None, )?; assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); println!("> stdout:\n{stdout}"); println!("> stderr:\n{stderr}"); assert!(!stderr.contains("Error")); let out_json = std::fs::read_to_string(format!("{dir}/out.json")).unwrap(); println!("> out.json:\n{out_json}"); Ok(()) } #[test] fn unreachable_without_wasi() -> Result<()> { let output = run_wasmtime_for_output( &[ "-Scli=n", "-Ccache=n", "tests/all/cli_tests/unreachable.wat", ], None, )?; assert_ne!(output.stderr, b""); assert_eq!(output.stdout, b""); assert_trap_code(&output.status); Ok(()) } #[test] fn config_cli_flag() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/simple.wat")?; // Test some valid TOML values let (mut cfg, cfg_path) = tempfile::NamedTempFile::new()?.into_parts(); cfg.write_all( br#" [optimize] opt-level = 2 signals-based-traps = false [codegen] collector = "null" [debug] address-map = true [wasm] max-wasm-stack = 65536 [wasi] cli = true "#, )?; let output = run_wasmtime(&[ "run", "--config", cfg_path.to_str().unwrap(), "--invoke", "get_f64", wasm.path().to_str().unwrap(), ])?; assert_eq!(output, "100\n"); // Make sure CLI flags overrides TOML values let output = run_wasmtime(&[ "run", "--config", cfg_path.to_str().unwrap(), "--invoke", "get_f64", "-W", "max-wasm-stack=0", // should override TOML value 65536 specified above and execution should fail wasm.path().to_str().unwrap(), ]); assert!( output .as_ref() .unwrap_err() .to_string() .contains("max_wasm_stack size cannot be zero"), "'{output:?}' did not contain expected error message", ); // Test invalid TOML key let (mut cfg, cfg_path) = tempfile::NamedTempFile::new()?.into_parts(); cfg.write_all( br#" [optimize] this-key-does-not-exist = true "#, )?; let output = run_wasmtime(&[ "run", "--config", cfg_path.to_str().unwrap(), wasm.path().to_str().unwrap(), ]); assert!( output .as_ref() .unwrap_err() .to_string() .contains("unknown field `this-key-does-not-exist`"), "'{output:?}' did not contain expected error message" ); // Test invalid TOML table let (mut cfg, cfg_path) = tempfile::NamedTempFile::new()?.into_parts(); cfg.write_all( br#" [invalid_table] "#, )?; let output = run_wasmtime(&[ "run", "--config", cfg_path.to_str().unwrap(), wasm.path().to_str().unwrap(), ]); assert!( output .as_ref() .unwrap_err() .to_string() .contains("unknown field `invalid_table`, expected one of `optimize`, `codegen`, `debug`, `wasm`, `wasi`"), "'{output:?}' did not contain expected error message", ); Ok(()) } #[test] fn invalid_subcommand() -> Result<()> { let output = run_wasmtime_for_output(&["invalid-subcommand"], None)?; dbg!(&output); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("invalid-subcommand")); Ok(()) } #[test] fn numeric_args() -> Result<()> { let wasm = build_wasm("tests/all/cli_tests/numeric_args.wat")?; // Test decimal i32 let output = run_wasmtime_for_output( &[ "run", "--invoke", "i32_test", wasm.path().to_str().unwrap(), "42", ], None, )?; assert_eq!(output.status.success(), true); assert_eq!(output.stdout, b"42\n"); // Test hexadecimal i32 with lowercase prefix let output = run_wasmtime_for_output( &[ "run", "--invoke", "i32_test", wasm.path().to_str().unwrap(), "0x2A", ], None, )?; assert_eq!(output.status.success(), true); assert_eq!(output.stdout, b"42\n"); // Test hexadecimal i32 with uppercase prefix let output = run_wasmtime_for_output( &[ "run", "--invoke", "i32_test", wasm.path().to_str().unwrap(), "0X2a", ], None, )?; assert_eq!(output.status.success(), true); assert_eq!(output.stdout, b"42\n"); // Test that non-prefixed hex strings are not interpreted as hex let output = run_wasmtime_for_output( &[ "run", "--invoke", "i32_test", wasm.path().to_str().unwrap(), "ff", ], None, )?; assert!(!output.status.success()); // Should fail as "ff" is not a valid decimal number // Test decimal i64 let output = run_wasmtime_for_output( &[ "run", "--invoke", "i64_test", wasm.path().to_str().unwrap(), "42", ], None, )?; assert_eq!(output.status.success(), true); assert_eq!(output.stdout, b"42\n"); // Test hexadecimal i64 let output = run_wasmtime_for_output( &[ "run", "--invoke", "i64_test", wasm.path().to_str().unwrap(), "0x2A", ], None, )?; assert_eq!(output.status.success(), true); assert_eq!(output.stdout, b"42\n"); Ok(()) } #[test] fn compilation_logs() -> Result<()> { let temp = tempfile::NamedTempFile::new()?; let output = get_wasmtime_command()? .args(&[ "compile", "-Wgc", "tests/all/cli_tests/issue-10353.wat", "--output", &temp.path().display().to_string(), ]) .env("WASMTIME_LOG", "trace") .env("RUST_BACKTRACE", "1") .output()?; if !output.status.success() { println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); panic!("wasmtime compilation failed when logs requested"); } Ok(()) } #[test] fn big_table_in_pooling_allocator() -> Result<()> { // Works by default run_wasmtime(&["tests/all/cli_tests/big_table.wat"])?; // Does not work by default in the pooling allocator, and the error message // should mention something about the pooling allocator. let output = run_wasmtime_for_output( &["-Opooling-allocator", "tests/all/cli_tests/big_table.wat"], None, )?; assert!(!output.status.success()); println!("{}", String::from_utf8_lossy(&output.stderr)); assert!(String::from_utf8_lossy(&output.stderr).contains("pooling allocator")); // Does work with `-Wmax-table-elements` run_wasmtime(&[ "-Opooling-allocator", "-Wmax-table-elements=25000", "tests/all/cli_tests/big_table.wat", ])?; // Also works with `-Opooling-table-elements` run_wasmtime(&[ "-Opooling-allocator", "-Opooling-table-elements=25000", "tests/all/cli_tests/big_table.wat", ])?; Ok(()) } fn wizen(args: &[&str], wat: &str) -> Result { let mut cmd = get_wasmtime_command()?; cmd.arg("wizer").args(args).arg("-"); cmd.stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); let mut child = cmd.spawn()?; let mut stdin = child.stdin.take().unwrap(); stdin.write_all(wat.as_bytes())?; drop(stdin); let output = child.wait_with_output()?; if !output.status.success() { println!( "Failed to execute wasmtime wizer with: {cmd:?}\n{}", String::from_utf8_lossy(&output.stderr) ); } Ok(output) } #[test] fn wizer_no_imports_by_default() -> Result<()> { let result = wizen( &[], r#"(module (func (export "wizer-initialize")) )"#, )?; assert!(result.status.success()); let result = wizen( &[], r#"(module (import "foo" "bar" (func)) (func (export "wizer-initialize")) )"#, )?; assert!(!result.status.success()); let result = wizen( &[], r#"(module (import "wasi_snapshot_preview1" "fd_write" (func (param i32 i32 i32 i32) (result i32))) (func (export "wizer-initialize")) )"#, )?; assert!(!result.status.success()); let result = wizen( &["-Scli"], r#"(module (import "wasi_snapshot_preview1" "fd_write" (func (param i32 i32 i32 i32) (result i32))) (func (export "wizer-initialize")) )"#, )?; assert!(result.status.success()); Ok(()) } #[test] fn wizer_components() -> Result<()> { let result = wizen( &[], r#" (component (core module $a (global (mut i32) (i32.const 0)) (func (export "init") i32.const 100 global.set 0) ) (core instance $a (instantiate $a)) (func (export "wizer-initialize") (canon lift (core func $a "init"))) ) "#, )?; assert!(result.status.success()); let component_with_wasi = r#" (component (import "wasi:cli/environment@0.2.0" (instance (export "get-arguments" (func (result (list string)))) )) (core module $a (global (mut i32) (i32.const 0)) (func (export "init") i32.const 100 global.set 0) ) (core instance $a (instantiate $a)) (func (export "wizer-initialize") (canon lift (core func $a "init"))) ) "#; let result = wizen(&[], component_with_wasi)?; assert!(!result.status.success()); let result = wizen(&["-Scli"], component_with_wasi)?; assert!(result.status.success()); Ok(()) }