From bd3e6b35a5e284d10e5ad3c805ca097984555817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Cabrera?= Date: Sat, 29 Jun 2024 12:45:50 -0400 Subject: [PATCH] Extract the `runner` module into its own crate This commit extracts the utiility runner module, used in the integration tests into its own private crate. The main motivation for this change is to enable sharing this functionality between integration tests and the upcoming `javy-fuzz` crate. Additionally, this change promotes some dependencies to the workspace level, namely: - `tempfile` - `uuid` --- Cargo.lock | 13 ++ Cargo.toml | 3 + crates/cli/Cargo.toml | 5 +- crates/cli/tests/integration_test.rs | 202 +++++++++++++----- crates/runner/Cargo.toml | 15 ++ .../tests/runner/mod.rs => runner/src/lib.rs} | 134 ++++++++---- 6 files changed, 277 insertions(+), 95 deletions(-) create mode 100644 crates/runner/Cargo.toml rename crates/{cli/tests/runner/mod.rs => runner/src/lib.rs} (69%) diff --git a/Cargo.lock b/Cargo.lock index 634cf56f..baa355dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1714,6 +1714,7 @@ dependencies = [ "clap", "convert_case", "criterion", + "javy-runner", "lazy_static", "num-format", "serde", @@ -1749,6 +1750,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "javy-runner" +version = "3.0.0" +dependencies = [ + "anyhow", + "tempfile", + "uuid", + "wasi-common", + "wasmtime", + "wasmtime-wasi", +] + [[package]] name = "javy-test-macros" version = "3.0.0" diff --git a/Cargo.toml b/Cargo.toml index 3f9684db..786e8af4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/cli", "crates/javy-test-macros", "crates/javy-config", + "crates/runner", ] resolver = "2" @@ -27,6 +28,8 @@ once_cell = "1.19" bitflags = "2.5.0" javy-config = { path = "crates/javy-config" } javy = { path = "crates/javy", version = "3.0.0" } +tempfile = "3.10.1" +uuid = { version = "1.8", features = ["v4"] } [profile.release] lto = true diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 7b1777ae..a769be21 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -31,17 +31,18 @@ swc_core = { version = "0.92.8", features = [ wit-parser = "0.209.1" convert_case = "0.6.0" wasm-opt = "0.116.1" -tempfile = "3.10.1" +tempfile = { workspace = true } clap = { version = "4.5.7", features = ["derive"] } [dev-dependencies] serde_json = "1.0" -uuid = { version = "1.8", features = ["v4"] } lazy_static = "1.4" serde = { version = "1.0", default-features = false, features = ["derive"] } criterion = "0.5" num-format = "0.4.4" wasmparser = "0.209.1" +javy-runner = { path = "../runner/" } +uuid = { workspace = true } [build-dependencies] anyhow = "1.0.86" diff --git a/crates/cli/tests/integration_test.rs b/crates/cli/tests/integration_test.rs index 6c2df553..9d866ff6 100644 --- a/crates/cli/tests/integration_test.rs +++ b/crates/cli/tests/integration_test.rs @@ -1,48 +1,72 @@ mod common; -mod runner; -use runner::{Runner, RunnerError}; +use anyhow::Result; +use javy_runner::{Builder, Runner, RunnerError}; +use std::path::PathBuf; use std::str; +static BIN: &'static str = env!("CARGO_BIN_EXE_javy"); +static ROOT: &'static str = env!("CARGO_MANIFEST_DIR"); + #[test] -fn test_identity() { - let mut runner = Runner::default(); +fn test_identity() -> Result<()> { + let mut runner = Builder::default().root(sample_scripts()).bin(BIN).build()?; let (output, _, fuel_consumed) = run_with_u8s(&mut runner, 42); assert_eq!(42, output); assert_fuel_consumed_within_threshold(47_773, fuel_consumed); + Ok(()) } #[test] -fn test_fib() { - let mut runner = Runner::new("fib.js"); +fn test_fib() -> Result<()> { + let mut runner = Builder::default() + .root(sample_scripts()) + .bin(BIN) + .input("fib.js") + .build()?; let (output, _, fuel_consumed) = run_with_u8s(&mut runner, 5); assert_eq!(8, output); assert_fuel_consumed_within_threshold(66_007, fuel_consumed); + Ok(()) } #[test] -fn test_recursive_fib() { - let mut runner = Runner::new("recursive-fib.js"); +fn test_recursive_fib() -> Result<()> { + let mut runner = Builder::default() + .bin(BIN) + .root(sample_scripts()) + .input("recursive-fib.js") + .build()?; let (output, _, fuel_consumed) = run_with_u8s(&mut runner, 5); assert_eq!(8, output); assert_fuel_consumed_within_threshold(69_306, fuel_consumed); + Ok(()) } #[test] -fn test_str() { - let mut runner = Runner::new("str.js"); +fn test_str() -> Result<()> { + let mut runner = Builder::default() + .root(sample_scripts()) + .bin(BIN) + .input("str.js") + .build()?; let (output, _, fuel_consumed) = run(&mut runner, "hello".as_bytes()); assert_eq!("world".as_bytes(), output); assert_fuel_consumed_within_threshold(142_849, fuel_consumed); + Ok(()) } #[test] -fn test_encoding() { - let mut runner = Runner::new("text-encoding.js"); +fn test_encoding() -> Result<()> { + let mut runner = Builder::default() + .root(sample_scripts()) + .bin(BIN) + .input("text-encoding.js") + .build()?; let (output, _, fuel_consumed) = run(&mut runner, "hello".as_bytes()); assert_eq!("el".as_bytes(), output); @@ -56,11 +80,16 @@ fn test_encoding() { let (output, _, _) = run(&mut runner, "test".as_bytes()); assert_eq!("test2".as_bytes(), output); + Ok(()) } #[test] -fn test_logging() { - let mut runner = Runner::new("logging.js"); +fn test_logging() -> Result<()> { + let mut runner = Builder::default() + .root(sample_scripts()) + .bin(BIN) + .input("logging.js") + .build()?; let (_output, logs, fuel_consumed) = run(&mut runner, &[]); assert_eq!( @@ -68,131 +97,192 @@ fn test_logging() { logs.as_str(), ); assert_fuel_consumed_within_threshold(34169, fuel_consumed); + Ok(()) } #[test] -fn test_readme_script() { - let mut runner = Runner::new("readme.js"); +fn test_readme_script() -> Result<()> { + let mut runner = Builder::default() + .root(sample_scripts()) + .bin(BIN) + .input("readme.js") + .build()?; let (output, _, fuel_consumed) = run(&mut runner, r#"{ "n": 2, "bar": "baz" }"#.as_bytes()); assert_eq!(r#"{"foo":3,"newBar":"baz!"}"#.as_bytes(), output); assert_fuel_consumed_within_threshold(270_919, fuel_consumed); + Ok(()) } #[cfg(feature = "experimental_event_loop")] #[test] -fn test_promises() { - let mut runner = Runner::new("promise.js"); +fn test_promises() -> Result<()> { + let mut runner = Builder::default() + .bin(BIN) + .root(sample_scripts()) + .input("promise.js") + .build()?; let (output, _, _) = run(&mut runner, &[]); assert_eq!("\"foo\"\"bar\"".as_bytes(), output); + Ok(()) } #[cfg(not(feature = "experimental_event_loop"))] #[test] -fn test_promises() { - use crate::runner::RunnerError; - - let mut runner = Runner::new("promise.js"); +fn test_promises() -> Result<()> { + use javy_runner::RunnerError; + + let mut runner = Builder::default() + .root(sample_scripts()) + .bin(BIN) + .input("promise.js") + .build()?; let res = runner.exec(&[]); let err = res.err().unwrap().downcast::().unwrap(); assert!(str::from_utf8(&err.stderr) .unwrap() .contains("Pending jobs in the event queue.")); + + Ok(()) } #[test] -fn test_exported_functions() { - let mut runner = Runner::new_with_exports("exported-fn.js", "exported-fn.wit", "exported-fn"); +fn test_exported_functions() -> Result<()> { + let mut runner = Builder::default() + .bin(BIN) + .root(sample_scripts()) + .input("exported-fn.js") + .wit("exported-fn.wit") + .world("exported-fn") + .build()?; let (_, logs, fuel_consumed) = run_fn(&mut runner, "foo", &[]); assert_eq!("Hello from top-level\nHello from foo\n", logs); assert_fuel_consumed_within_threshold(80023, fuel_consumed); let (_, logs, _) = run_fn(&mut runner, "foo-bar", &[]); assert_eq!("Hello from top-level\nHello from fooBar\n", logs); + Ok(()) } #[cfg(feature = "experimental_event_loop")] #[test] -fn test_exported_promises() { - let mut runner = Runner::new_with_exports( - "exported-promise-fn.js", - "exported-promise-fn.wit", - "exported-promise-fn", - ); +fn test_exported_promises() -> Result<()> { + let mut runner = Builder::default() + .bin(BIN) + .root(sample_scripts()) + .input("exported-promise-fn.js") + .wit("exported-promise-fn.wit") + .world("exported-promise-fn") + .build()?; let (_, logs, _) = run_fn(&mut runner, "foo", &[]); assert_eq!("Top-level\ninside foo\n", logs); + Ok(()) } #[test] -fn test_exported_functions_without_flag() { - let mut runner = Runner::new("exported-fn.js"); +fn test_exported_functions_without_flag() -> Result<()> { + let mut runner = Builder::default() + .root(sample_scripts()) + .bin(BIN) + .input("exported-fn.js") + .build()?; let res = runner.exec_func("foo", &[]); assert_eq!( "failed to find function export `foo`", res.err().unwrap().to_string() ); + Ok(()) } #[test] -fn test_exported_function_without_semicolons() { - let mut runner = Runner::new_with_exports( - "exported-fn-no-semicolon.js", - "exported-fn-no-semicolon.wit", - "exported-fn", - ); +fn test_exported_function_without_semicolons() -> Result<()> { + let mut runner = Builder::default() + .root(sample_scripts()) + .bin(BIN) + .input("exported-fn-no-semicolon.js") + .wit("exported-fn-no-semicolon.wit") + .world("exported-fn") + .build()?; run_fn(&mut runner, "foo", &[]); + Ok(()) } #[test] -fn test_producers_section_present() { - let runner = Runner::new("readme.js"); +fn test_producers_section_present() -> Result<()> { + let runner = Builder::default() + .root(sample_scripts()) + .bin(BIN) + .input("readme.js") + .build()?; common::assert_producers_section_is_correct(&runner.wasm).unwrap(); + Ok(()) } #[test] -fn test_error_handling() { - let mut runner = Runner::new("error.js"); +fn test_error_handling() -> Result<()> { + let mut runner = Builder::default() + .root(sample_scripts()) + .bin(BIN) + .input("error.js") + .build()?; let result = runner.exec(&[]); let err = result.err().unwrap().downcast::().unwrap(); let expected_log_output = "Error:2:9 error\n at error (function.mjs:2:9)\n at (function.mjs:5:1)\n\n"; assert_eq!(expected_log_output, str::from_utf8(&err.stderr).unwrap()); + Ok(()) } #[test] -fn test_same_module_outputs_different_random_result() { - let mut runner = Runner::new("random.js"); +fn test_same_module_outputs_different_random_result() -> Result<()> { + let mut runner = Builder::default() + .bin(BIN) + .root(sample_scripts()) + .input("random.js") + .build()?; let (output, _, _) = runner.exec(&[]).unwrap(); let (output2, _, _) = runner.exec(&[]).unwrap(); // In theory these could be equal with a correct implementation but it's very unlikely. assert!(output != output2); // Don't check fuel consumed because fuel consumed can be different from run to run. See // https://github.com/bytecodealliance/javy/issues/401 for investigating the cause. + Ok(()) } #[test] -fn test_exported_default_arrow_fn() { - let mut runner = Runner::new_with_exports( - "exported-default-arrow-fn.js", - "exported-default-arrow-fn.wit", - "exported-arrow", - ); +fn test_exported_default_arrow_fn() -> Result<()> { + let mut runner = Builder::default() + .bin(BIN) + .root(sample_scripts()) + .input("exported-default-arrow-fn.js") + .wit("exported-default-arrow-fn.wit") + .world("exported-arrow") + .build()?; + let (_, logs, fuel_consumed) = run_fn(&mut runner, "default", &[]); assert_eq!(logs, "42\n"); assert_fuel_consumed_within_threshold(76706, fuel_consumed); + Ok(()) } #[test] -fn test_exported_default_fn() { - let mut runner = Runner::new_with_exports( - "exported-default-fn.js", - "exported-default-fn.wit", - "exported-default", - ); +fn test_exported_default_fn() -> Result<()> { + let mut runner = Builder::default() + .bin(BIN) + .root(sample_scripts()) + .input("exported-default-fn.js") + .wit("exported-default-fn.wit") + .world("exported-default") + .build()?; let (_, logs, fuel_consumed) = run_fn(&mut runner, "default", &[]); assert_eq!(logs, "42\n"); assert_fuel_consumed_within_threshold(77909, fuel_consumed); + Ok(()) +} + +fn sample_scripts() -> PathBuf { + PathBuf::from(ROOT).join("tests").join("sample-scripts") } fn run_with_u8s(r: &mut Runner, stdin: u8) -> (u8, String, u64) { diff --git a/crates/runner/Cargo.toml b/crates/runner/Cargo.toml new file mode 100644 index 00000000..f49a7105 --- /dev/null +++ b/crates/runner/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "javy-runner" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +wasmtime = { workspace = true } +wasmtime-wasi = { workspace = true } +wasi-common = { workspace = true } +anyhow = { workspace = true } +tempfile = { workspace = true } +uuid = { workspace = true } diff --git a/crates/cli/tests/runner/mod.rs b/crates/runner/src/lib.rs similarity index 69% rename from crates/cli/tests/runner/mod.rs rename to crates/runner/src/lib.rs index 08760fb7..2853dc21 100644 --- a/crates/cli/tests/runner/mod.rs +++ b/crates/runner/src/lib.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{bail, Result}; use std::error::Error; use std::fmt::{self, Display, Formatter}; use std::io::{self, Cursor, Write}; @@ -10,10 +10,93 @@ use wasi_common::sync::WasiCtxBuilder; use wasi_common::WasiCtx; use wasmtime::{Config, Engine, Linker, Module, OptLevel, Store}; +pub struct Builder { + /// The JS source. + input: PathBuf, + /// Root path. Used resolve the absolute path of the JS source. + root: PathBuf, + /// `javy` binary path. + bin_path: String, + /// The path to the wit file. + wit: Option, + /// The name of the wit world. + world: Option, + /// The logger capacity, in bytes. + capacity: usize, + built: bool, +} + +impl Default for Builder { + fn default() -> Self { + Self { + capacity: usize::MAX, + input: PathBuf::from("identity.js"), + wit: None, + world: None, + bin_path: "javy".into(), + root: Default::default(), + built: false, + } + } +} + +impl Builder { + pub fn root(&mut self, root: impl Into) -> &mut Self { + self.root = root.into(); + self + } + + pub fn input(&mut self, path: impl Into) -> &mut Self { + self.input = path.into(); + self + } + + pub fn bin(&mut self, bin: impl Into) -> &mut Self { + self.bin_path = bin.into(); + self + } + + pub fn wit(&mut self, wit: impl Into) -> &mut Self { + self.wit = Some(wit.into()); + self + } + + pub fn world(&mut self, world: impl Into) -> &mut Self { + self.world = Some(world.into()); + self + } + + pub fn build(&mut self) -> Result { + if self.built { + bail!("Builder already used to build a runner") + } + + if (self.wit.is_some() && self.world.is_none()) + || (self.wit.is_none() && self.world.is_some()) + { + bail!("Both `wit` and `world` must be defined") + } + + let Self { + bin_path, + input, + wit, + world, + capacity, + root, + built: _, + } = std::mem::replace(self, Default::default()); + + self.built = true; + + Ok(Runner::new(bin_path, root, input, wit, world, capacity)) + } +} + pub struct Runner { pub wasm: Vec, linker: Linker, - log_capacity: usize, + capacity: usize, } #[derive(Debug)] @@ -58,48 +141,25 @@ impl StoreContext { } } -impl Default for Runner { - fn default() -> Self { - Self::new("identity.js") - } -} - impl Runner { - pub fn new(js_file: impl AsRef) -> Self { - Self::new_with_fixed_logging_capacity(js_file, None, None, usize::MAX) - } - - pub fn new_with_exports( - js_file: impl AsRef, - wit_path: impl AsRef, - world: &str, - ) -> Self { - Self::new_with_fixed_logging_capacity( - js_file, - Some(wit_path.as_ref()), - Some(world), - usize::MAX, - ) - } - - fn new_with_fixed_logging_capacity( - js_file: impl AsRef, - wit_path: Option<&Path>, - wit_world: Option<&str>, + fn new( + bin: String, + root: PathBuf, + source: impl AsRef, + wit: Option, + world: Option, capacity: usize, ) -> Self { let wasm_file_name = format!("{}.wasm", uuid::Uuid::new_v4()); - let root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); - let sample_scripts = root.join("tests").join("sample-scripts"); // This directory is unique and will automatically get deleted // when `tempdir` goes out of scope. let Ok(tempdir) = tempfile::tempdir() else { panic!("Could not create temporary directory for .wasm test artifacts"); }; let wasm_file = tempdir.path().join(wasm_file_name); - let js_file = sample_scripts.join(js_file); - let wit_file = wit_path.map(|p| sample_scripts.join(p)); + let js_file = root.join(source); + let wit_file = wit.map(|p| root.join(p)); let mut args = vec![ "compile".to_string(), @@ -108,14 +168,14 @@ impl Runner { wasm_file.to_str().unwrap().to_string(), ]; - if let (Some(wit_file), Some(world)) = (wit_file, wit_world) { + if let (Some(wit_file), Some(world)) = (wit_file, world) { args.push("--wit".to_string()); args.push(wit_file.to_str().unwrap().to_string()); args.push("-n".to_string()); args.push(world.to_string()); } - let output = Command::new(env!("CARGO_BIN_EXE_javy")) + let output = Command::new(bin) .current_dir(root) .args(args) .output() @@ -136,7 +196,7 @@ impl Runner { Self { wasm, linker, - log_capacity: capacity, + capacity, } } @@ -147,7 +207,7 @@ impl Runner { pub fn exec_func(&mut self, func: &str, input: &[u8]) -> Result<(Vec, Vec, u64)> { let mut store = Store::new( self.linker.engine(), - StoreContext::new(input, self.log_capacity), + StoreContext::new(input, self.capacity), ); const INITIAL_FUEL: u64 = u64::MAX; store.set_fuel(INITIAL_FUEL)?;