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)?;