diff --git a/Cargo.lock b/Cargo.lock index cd3366ee..68d9fc64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1470,6 +1470,7 @@ dependencies = [ "criterion", "javy-config", "javy-runner", + "javy-test-macros", "lazy_static", "num-format", "serde", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index ca2a0dda..9df16fa8 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -44,6 +44,7 @@ num-format = "0.4.4" wasmparser = "0.215.0" javy-runner = { path = "../runner/" } uuid = { workspace = true } +javy-test-macros = { path = "../test-macros/" } [build-dependencies] anyhow = "1.0.86" diff --git a/crates/cli/src/codegen/builder.rs b/crates/cli/src/codegen/builder.rs index bc2c33fd..d6fa9661 100644 --- a/crates/cli/src/codegen/builder.rs +++ b/crates/cli/src/codegen/builder.rs @@ -4,7 +4,7 @@ use javy_config::Config; use std::path::PathBuf; /// Options for using WIT in the code generation process. -#[derive(Default, Clone, Debug)] +#[derive(Default, Clone, Debug, PartialEq)] pub(crate) struct WitOptions { /// The path of the .wit file to use. pub path: Option, diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index dbbbc17d..c1e57a57 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -151,14 +151,14 @@ where } /// Code generation option group. -#[derive(Clone, Debug)] +/// This group gets configured from the [`CodegenOption`] enum. +// +// NB: The documentation for each field is ommitted given that it's similar to +// the enum used to configured the group. +#[derive(Clone, Debug, PartialEq)] pub struct CodegenOptionGroup { - /// Creates a smaller module that requires a dynamically linked QuickJS provider Wasm - /// module to execute (see `emit-provider` command). pub dynamic: bool, - /// The WIT options. pub wit: WitOptions, - /// Enable source code compression, which generates smaller WebAssembly files at the cost of increased compile time. Defaults to enabled. pub source_compression: bool, } @@ -175,16 +175,17 @@ impl Default for CodegenOptionGroup { option_group! { #[derive(Clone, Debug)] pub enum CodegenOption { - /// Creates a smaller module that requires a dynamically linked QuickJS provider Wasm - /// module to execute (see `emit-provider` command). + /// Creates a smaller module that requires a dynamically linked QuickJS + /// provider Wasm module to execute (see `emit-provider` command). Dynamic(bool), - /// Optional path to WIT file describing exported functions. - /// Only supports function exports with no arguments and no return values. + /// Optional path to WIT file describing exported functions. Only + /// supports function exports with no arguments and no return values. Wit(PathBuf), - /// Optional path to WIT file describing exported functions. - /// Only supports function exports with no arguments and no return values. + /// Optional path to WIT file describing exported functions. Only + /// supports function exports with no arguments and no return values. WitWorld(String), - /// Enable source code compression, which generates smaller WebAssembly files at the cost of increased compile time. Defaults to enabled. + /// Enable source code compression, which generates smaller WebAssembly + /// files at the cost of increased compile time. SourceCompression(bool), } } @@ -211,10 +212,18 @@ impl TryFrom>> for CodegenOptionGroup { } } +/// JavaScript option group. +/// This group gets configured from the [`JsOption`] enum. +// +// NB: The documentation for each field is ommitted given that it's similar to +// the enum used to configured the group. #[derive(Clone, Debug, PartialEq)] pub struct JsOptionGroup { - /// Whether to redirect console.log to stderr. pub redirect_stdout_to_stderr: bool, + pub javy_json: bool, + pub simd_json_builtins: bool, + pub javy_stream_io: bool, + pub text_encoding: bool, } impl Default for JsOptionGroup { @@ -228,6 +237,17 @@ option_group! { pub enum JsOption { /// Whether to redirect the output of console.log to standard error. RedirectStdoutToStderr(bool), + /// Whether to enable the `Javy.JSON` builtins. + JavyJson(bool), + /// Whether to enable the `Javy.readSync` and `Javy.writeSync` builtins. + JavyStreamIo(bool), + /// Whether to override the `JSON.parse` and `JSON.stringify` + /// implementations with an alternative, more performant, SIMD based + /// implemetation. + SimdJsonBuiltins(bool), + /// Whether to enable support for the `TextEncoder` and `TextDecoder` + /// APIs. + TextEncoding(bool), } } @@ -240,6 +260,10 @@ impl From>> for JsOptionGroup { JsOption::RedirectStdoutToStderr(enabled) => { group.redirect_stdout_to_stderr = *enabled; } + JsOption::JavyJson(enable) => group.javy_json = *enable, + JsOption::SimdJsonBuiltins(enable) => group.simd_json_builtins = *enable, + JsOption::TextEncoding(enable) => group.text_encoding = *enable, + JsOption::JavyStreamIo(enable) => group.javy_stream_io = *enable, } } @@ -254,6 +278,10 @@ impl From for Config { Config::REDIRECT_STDOUT_TO_STDERR, value.redirect_stdout_to_stderr, ); + config.set(Config::JAVY_JSON, value.javy_json); + config.set(Config::SIMD_JSON_BUILTINS, value.simd_json_builtins); + config.set(Config::JAVY_STREAM_IO, value.javy_stream_io); + config.set(Config::TEXT_ENCODING, value.text_encoding); config } } @@ -262,6 +290,121 @@ impl From for JsOptionGroup { fn from(value: Config) -> Self { Self { redirect_stdout_to_stderr: value.contains(Config::REDIRECT_STDOUT_TO_STDERR), + javy_json: value.contains(Config::JAVY_JSON), + simd_json_builtins: value.contains(Config::SIMD_JSON_BUILTINS), + javy_stream_io: value.contains(Config::JAVY_STREAM_IO), + text_encoding: value.contains(Config::TEXT_ENCODING), } } } + +#[cfg(test)] +mod tests { + use super::{CodegenOption, CodegenOptionGroup, GroupOption, JsOption, JsOptionGroup}; + use anyhow::Result; + use javy_config::Config; + + #[test] + fn js_group_conversion_between_vector_of_options_and_group() -> Result<()> { + let group: JsOptionGroup = vec![].into(); + + assert_eq!(group, JsOptionGroup::default()); + + let raw = vec![GroupOption(vec![JsOption::RedirectStdoutToStderr(false)])]; + let group: JsOptionGroup = raw.into(); + let expected = JsOptionGroup { + redirect_stdout_to_stderr: false, + ..Default::default() + }; + + assert_eq!(group, expected); + + let raw = vec![GroupOption(vec![JsOption::JavyJson(false)])]; + let group: JsOptionGroup = raw.into(); + let expected = JsOptionGroup { + javy_json: false, + ..Default::default() + }; + assert_eq!(group, expected); + + let raw = vec![GroupOption(vec![JsOption::JavyStreamIo(false)])]; + let group: JsOptionGroup = raw.into(); + let expected = JsOptionGroup { + javy_stream_io: false, + ..Default::default() + }; + assert_eq!(group, expected); + + let raw = vec![GroupOption(vec![JsOption::SimdJsonBuiltins(false)])]; + let group: JsOptionGroup = raw.into(); + + let expected = JsOptionGroup { + simd_json_builtins: false, + ..Default::default() + }; + assert_eq!(group, expected); + + let raw = vec![GroupOption(vec![JsOption::TextEncoding(false)])]; + let group: JsOptionGroup = raw.into(); + + let expected = JsOptionGroup { + text_encoding: false, + ..Default::default() + }; + assert_eq!(group, expected); + + let raw = vec![GroupOption(vec![ + JsOption::JavyStreamIo(false), + JsOption::JavyJson(false), + JsOption::RedirectStdoutToStderr(false), + JsOption::TextEncoding(false), + JsOption::SimdJsonBuiltins(false), + ])]; + let group: JsOptionGroup = raw.into(); + let expected = JsOptionGroup { + javy_stream_io: false, + javy_json: false, + redirect_stdout_to_stderr: false, + text_encoding: false, + simd_json_builtins: false, + }; + assert_eq!(group, expected); + + Ok(()) + } + + #[test] + fn codegen_group_conversion_between_vector_of_options_and_group() -> Result<()> { + let group: CodegenOptionGroup = vec![].try_into()?; + assert_eq!(group, CodegenOptionGroup::default()); + + let raw = vec![GroupOption(vec![CodegenOption::Dynamic(true)])]; + let group: CodegenOptionGroup = raw.try_into()?; + let expected = CodegenOptionGroup { + dynamic: true, + ..Default::default() + }; + + assert_eq!(group, expected); + + let raw = vec![GroupOption(vec![CodegenOption::SourceCompression(false)])]; + let group: CodegenOptionGroup = raw.try_into()?; + let expected = CodegenOptionGroup { + source_compression: false, + ..Default::default() + }; + + assert_eq!(group, expected); + + Ok(()) + } + + #[test] + fn js_conversion_between_group_and_config() -> Result<()> { + assert_eq!(JsOptionGroup::default(), Config::default().into()); + + let cfg: Config = JsOptionGroup::default().into(); + assert_eq!(cfg, Config::default()); + Ok(()) + } +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a09772b7..011606ac 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -64,7 +64,7 @@ fn main() -> Result<()> { builder .wit_opts(codegen.wit) .source_compression(codegen.source_compression) - .provider_version("2"); + .provider_version("3"); let js_opts: JsOptionGroup = opts.js.clone().into(); let mut gen = if codegen.dynamic { diff --git a/crates/cli/tests/common/mod.rs b/crates/cli/tests/common/mod.rs deleted file mode 100644 index 7a9351e3..00000000 --- a/crates/cli/tests/common/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -use anyhow::Result; -use javy_runner::{Builder, JavyCommand}; - -pub fn run_with_compile_and_build(test: F) -> Result<()> -where - F: Fn(&mut Builder) -> Result<()>, -{ - test(Builder::default().command(JavyCommand::Compile))?; - test(Builder::default().command(JavyCommand::Build))?; - Ok(()) -} diff --git a/crates/cli/tests/dynamic_linking_test.rs b/crates/cli/tests/dynamic_linking_test.rs index c73e727f..57376060 100644 --- a/crates/cli/tests/dynamic_linking_test.rs +++ b/crates/cli/tests/dynamic_linking_test.rs @@ -1,165 +1,108 @@ use anyhow::Result; -use javy_runner::{Builder, JavyCommand}; -use std::path::{Path, PathBuf}; -use std::str; - -mod common; -use common::run_with_compile_and_build; - -static ROOT: &str = env!("CARGO_MANIFEST_DIR"); -static BIN: &str = env!("CARGO_BIN_EXE_javy"); - -#[test] -pub fn test_dynamic_linking() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .root(root()) - .bin(BIN) - .input("console.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .build()?; - - let (_, logs, _) = runner.exec(&[])?; - assert_eq!("42\n", String::from_utf8(logs)?); - Ok(()) - }) +use javy_runner::Builder; +use javy_test_macros::javy_cli_test; + +#[javy_cli_test(dyn = true, root = "tests/dynamic-linking-scripts")] +pub fn test_dynamic_linking(builder: &mut Builder) -> Result<()> { + let mut runner = builder.input("console.js").build()?; + + let (_, logs, _) = runner.exec(&[])?; + assert_eq!("42\n", String::from_utf8(logs)?); + Ok(()) } -#[test] -pub fn test_dynamic_linking_with_func() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .root(root()) - .bin(BIN) - .input("linking-with-func.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .wit("linking-with-func.wit") - .world("foo-test") - .build()?; - - let (_, logs, _) = runner.exec_func("foo-bar", &[])?; - - assert_eq!("Toplevel\nIn foo\n", String::from_utf8(logs)?); - Ok(()) - }) +#[javy_cli_test(dyn = true, root = "tests/dynamic-linking-scripts")] +pub fn test_dynamic_linking_with_func(builder: &mut Builder) -> Result<()> { + let mut runner = builder + .input("linking-with-func.js") + .wit("linking-with-func.wit") + .world("foo-test") + .build()?; + + let (_, logs, _) = runner.exec_func("foo-bar", &[])?; + + assert_eq!("Toplevel\nIn foo\n", String::from_utf8(logs)?); + Ok(()) } -#[test] -pub fn test_dynamic_linking_with_func_without_flag() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .root(root()) - .bin(BIN) - .input("linking-with-func-without-flag.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .build()?; - - let res = runner.exec_func("foo", &[]); - - assert_eq!( - "failed to find function export `foo`", - res.err().unwrap().to_string() - ); - Ok(()) - }) +#[javy_cli_test(dyn = true, root = "tests/dynamic-linking-scripts")] +pub fn test_dynamic_linking_with_func_without_flag(builder: &mut Builder) -> Result<()> { + let mut runner = builder.input("linking-with-func-without-flag.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_errors_in_exported_functions_are_correctly_reported() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .root(root()) - .bin(BIN) - .input("errors-in-exported-functions.js") - .wit("errors-in-exported-functions.wit") - .world("foo-test") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .build()?; - - let res = runner.exec_func("foo", &[]); - - assert!(res - .err() - .unwrap() - .to_string() - .contains("error while executing")); - Ok(()) - }) +#[javy_cli_test(dyn = true, root = "tests/dynamic-linking-scripts")] +fn test_errors_in_exported_functions_are_correctly_reported(builder: &mut Builder) -> Result<()> { + let mut runner = builder + .input("errors-in-exported-functions.js") + .wit("errors-in-exported-functions.wit") + .world("foo-test") + .build()?; + + let res = runner.exec_func("foo", &[]); + + assert!(res + .err() + .unwrap() + .to_string() + .contains("error while executing")); + Ok(()) } -#[test] +#[javy_cli_test(dyn = true, root = "tests/dynamic-linking-scripts")] // If you need to change this test, then you've likely made a breaking change. -pub fn check_for_new_imports() -> Result<()> { - run_with_compile_and_build(|builder| { - let runner = builder - .root(root()) - .bin(BIN) - .input("console.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .build()?; - - runner.assert_known_base_imports() - }) +pub fn check_for_new_imports(builder: &mut Builder) -> Result<()> { + let runner = builder.input("console.js").build()?; + runner.assert_known_base_imports() } -#[test] +#[javy_cli_test(dyn = true, root = "tests/dynamic-linking-scripts")] // If you need to change this test, then you've likely made a breaking change. -pub fn check_for_new_imports_for_exports() -> Result<()> { - run_with_compile_and_build(|builder| { - let runner = builder - .root(root()) - .bin(BIN) - .input("linking-with-func.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .wit("linking-with-func.wit") - .world("foo-test") - .build()?; - - runner.assert_known_named_function_imports() - }) +pub fn check_for_new_imports_for_exports(builder: &mut Builder) -> Result<()> { + let runner = builder + .input("linking-with-func.js") + .wit("linking-with-func.wit") + .world("foo-test") + .build()?; + + runner.assert_known_named_function_imports() } -#[test] -pub fn test_dynamic_linking_with_arrow_fn() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .root(root()) - .bin(BIN) - .input("linking-arrow-func.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .wit("linking-arrow-func.wit") - .world("exported-arrow") - .build()?; - - let (_, logs, _) = runner.exec_func("default", &[])?; - - assert_eq!("42\n", String::from_utf8(logs)?); - Ok(()) - }) +#[javy_cli_test(dyn = true, root = "tests/dynamic-linking-scripts")] +pub fn test_dynamic_linking_with_arrow_fn(builder: &mut Builder) -> Result<()> { + let mut runner = builder + .input("linking-arrow-func.js") + .wit("linking-arrow-func.wit") + .world("exported-arrow") + .build()?; + + let (_, logs, _) = runner.exec_func("default", &[])?; + + assert_eq!("42\n", String::from_utf8(logs)?); + Ok(()) } -#[test] -fn test_producers_section_present() -> Result<()> { - run_with_compile_and_build(|builder| { - let runner = builder - .root(root()) - .bin(BIN) - .input("console.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .build()?; - - runner.assert_producers() - }) +#[javy_cli_test(dyn = true, root = "tests/dynamic-linking-scripts")] +fn test_producers_section_present(builder: &mut Builder) -> Result<()> { + let runner = builder.input("console.js").build()?; + runner.assert_producers() } -#[test] -fn test_using_runtime_flag_with_dynamic_triggers_error() -> Result<()> { - let build_result = Builder::default() - .root(root()) - .bin(BIN) +#[javy_cli_test( + dyn = true, + root = "tests/dynamic-linking-scripts", + commands(not(Compile)) +)] +fn test_using_runtime_flag_with_dynamic_triggers_error(builder: &mut Builder) -> Result<()> { + let build_result = builder .input("console.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .command(JavyCommand::Build) .redirect_stdout_to_stderr(false) .build(); assert!(build_result.is_err_and(|e| e @@ -168,44 +111,21 @@ fn test_using_runtime_flag_with_dynamic_triggers_error() -> Result<()> { Ok(()) } -#[test] -// Temporarily ignore given that Javy.JSON is disabled by default. -#[ignore] -fn javy_json_identity() -> Result<()> { - let mut runner = Builder::default() - .root(root()) - .bin(BIN) - .input("javy-json-id.js") - .preload("javy_quickjs_provider_v2".into(), provider_module_path()) - .build()?; +#[javy_cli_test( + dyn = true, + root = "tests/dynamic-linking-scripts", + commands(not(Compile)) +)] +fn javy_json_identity(builder: &mut Builder) -> Result<()> { + let mut runner = builder.input("javy-json-id.js").build()?; let input = "{\"x\":5}"; let bytes = String::from(input).into_bytes(); let (out, logs, _) = runner.exec(&bytes)?; - assert_eq!(String::from_utf8(out)?, "undefined\n"); - assert_eq!(String::from(input), String::from_utf8(logs)?); + assert_eq!(String::from_utf8(out)?, input); + assert_eq!(String::from_utf8(logs)?, "undefined\n"); Ok(()) } - -fn provider_module_path() -> PathBuf { - let mut lib_path = PathBuf::from(ROOT); - lib_path.pop(); - lib_path.pop(); - lib_path = lib_path.join( - Path::new("target") - .join("wasm32-wasi") - .join("release") - .join("javy_quickjs_provider_wizened.wasm"), - ); - - lib_path -} - -fn root() -> PathBuf { - PathBuf::from(ROOT) - .join("tests") - .join("dynamic-linking-scripts") -} diff --git a/crates/cli/tests/integration_test.rs b/crates/cli/tests/integration_test.rs index dd1f7001..2d0624da 100644 --- a/crates/cli/tests/integration_test.rs +++ b/crates/cli/tests/integration_test.rs @@ -1,107 +1,71 @@ use anyhow::Result; use javy_runner::{Builder, Runner, RunnerError}; -use std::path::PathBuf; use std::str; -mod common; -use common::run_with_compile_and_build; +use javy_test_macros::javy_cli_test; -static BIN: &str = env!("CARGO_BIN_EXE_javy"); -static ROOT: &str = env!("CARGO_MANIFEST_DIR"); +#[javy_cli_test] +fn test_identity(builder: &mut Builder) -> Result<()> { + let mut runner = builder.build()?; -#[test] -fn test_identity() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder.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(()) - }) + 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() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .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(()) - }) +#[javy_cli_test] +fn test_fib(builder: &mut Builder) -> Result<()> { + let mut runner = builder.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() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .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(()) - }) +#[javy_cli_test] +fn test_recursive_fib(builder: &mut Builder) -> Result<()> { + let mut runner = builder.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() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .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(()) - }) +#[javy_cli_test] +fn test_str(builder: &mut Builder) -> Result<()> { + let mut runner = builder.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() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .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); - assert_fuel_consumed_within_threshold(258_197, fuel_consumed); - - let (output, _, _) = run(&mut runner, "invalid".as_bytes()); - assert_eq!("true".as_bytes(), output); - - let (output, _, _) = run(&mut runner, "invalid_fatal".as_bytes()); - assert_eq!("The encoded data was not valid utf-8".as_bytes(), output); - - let (output, _, _) = run(&mut runner, "test".as_bytes()); - assert_eq!("test2".as_bytes(), output); - Ok(()) - }) +#[javy_cli_test] +fn test_encoding(builder: &mut Builder) -> Result<()> { + let mut runner = builder.input("text-encoding.js").build()?; + + let (output, _, fuel_consumed) = run(&mut runner, "hello".as_bytes()); + assert_eq!("el".as_bytes(), output); + assert_fuel_consumed_within_threshold(258_197, fuel_consumed); + + let (output, _, _) = run(&mut runner, "invalid".as_bytes()); + assert_eq!("true".as_bytes(), output); + + let (output, _, _) = run(&mut runner, "invalid_fatal".as_bytes()); + assert_eq!("The encoded data was not valid utf-8".as_bytes(), output); + + let (output, _, _) = run(&mut runner, "test".as_bytes()); + assert_eq!("test2".as_bytes(), output); + Ok(()) } -#[test] -fn test_logging_with_compile() -> Result<()> { - let mut runner = Builder::default() - .root(sample_scripts()) - .bin(BIN) - .input("logging.js") - .command(javy_runner::JavyCommand::Compile) - .build()?; +#[javy_cli_test(commands(not(Build)))] +fn test_logging_with_compile(builder: &mut Builder) -> Result<()> { + let mut runner = builder.input("logging.js").build()?; let (output, logs, fuel_consumed) = run(&mut runner, &[]); assert!(output.is_empty()); @@ -113,13 +77,10 @@ fn test_logging_with_compile() -> Result<()> { Ok(()) } -#[test] -fn test_logging_without_redirect() -> Result<()> { - let mut runner = Builder::default() - .root(sample_scripts()) - .bin(BIN) +#[javy_cli_test(commands(not(Compile)))] +fn test_logging_without_redirect(builder: &mut Builder) -> Result<()> { + let mut runner = builder .input("logging.js") - .command(javy_runner::JavyCommand::Build) .redirect_stdout_to_stderr(false) .build()?; @@ -130,13 +91,10 @@ fn test_logging_without_redirect() -> Result<()> { Ok(()) } -#[test] -fn test_logging_with_redirect() -> Result<()> { - let mut runner = Builder::default() - .root(sample_scripts()) - .bin(BIN) +#[javy_cli_test(commands(not(Compile)))] +fn test_logging_with_redirect(builder: &mut Builder) -> Result<()> { + let mut runner = builder .input("logging.js") - .command(javy_runner::JavyCommand::Build) .redirect_stdout_to_stderr(true) .build()?; @@ -150,231 +108,183 @@ fn test_logging_with_redirect() -> Result<()> { Ok(()) } -#[test] -fn test_readme_script() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .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(()) - }) +#[javy_cli_test(commands(not(Compile)), root = "tests/dynamic-linking-scripts")] +fn test_javy_json_enabled(builder: &mut Builder) -> Result<()> { + let mut runner = builder.input("javy-json-id.js").build()?; + + let input = "{\"x\":5}"; + let (output, logs, _) = run(&mut runner, input.as_bytes()); + + assert_eq!(logs, "undefined\n"); + assert_eq!(String::from_utf8(output)?, input); + + Ok(()) +} + +#[javy_cli_test(commands(not(Compile)), root = "tests/dynamic-linking-scripts")] +fn test_javy_json_disabled(builder: &mut Builder) -> Result<()> { + let mut runner = builder + .input("javy-json-id.js") + .simd_json_builtins(false) + .build()?; + + let result = runner.exec(&[]); + assert!(result.is_err()); + + Ok(()) +} + +#[javy_cli_test] +fn test_readme_script(builder: &mut Builder) -> Result<()> { + let mut runner = builder.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() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .bin(BIN) - .root(sample_scripts()) - .input("promise.js") - .build()?; - - let (output, _, _) = run(&mut runner, &[]); - assert_eq!("\"foo\"\"bar\"".as_bytes(), output); - Ok(()) - }) +#[javy_cli_test] +fn test_promises(builder: &mut Builder) -> Result<()> { + let mut runner = builder.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() -> Result<()> { +#[javy_cli_test] +fn test_promises(builder: &mut Builder) -> Result<()> { use javy_runner::RunnerError; - run_with_compile_and_build(|builder| { - let mut runner = builder - .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(()) - }) + let mut runner = builder.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(()) } #[cfg(feature = "experimental_event_loop")] -#[test] -fn test_promise_top_level_await() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .bin(BIN) - .root(sample_scripts()) - .input("top-level-await.js") - .build()?; - let (out, _, _) = run(&mut runner, &[]); - - assert_eq!("bar", String::from_utf8(out)?); - Ok(()) - }) +#[javy_cli_test] +fn test_promise_top_level_await(builder: &mut Builder) -> Result<()> { + let mut runner = builder.input("top-level-await.js").build()?; + let (out, _, _) = run(&mut runner, &[]); + + assert_eq!("bar", String::from_utf8(out)?); + Ok(()) } -#[test] -fn test_exported_functions() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .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(()) - }) +#[javy_cli_test] +fn test_exported_functions(builder: &mut Builder) -> Result<()> { + let mut runner = builder + .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() -> Result<()> { - use clap::builder; - - run_with_compile_and_build(|builder| { - let mut runner = builder - .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(()) - }) +#[javy_cli_test] +fn test_exported_promises(builder: &mut Builder) -> Result<()> { + let mut runner = builder + .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() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .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(()) - }) +#[javy_cli_test] +fn test_exported_functions_without_flag(builder: &mut Builder) -> Result<()> { + let mut runner = builder.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() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .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(()) - }) +#[javy_cli_test] +fn test_exported_function_without_semicolons(builder: &mut Builder) -> Result<()> { + let mut runner = builder + .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() -> Result<()> { - run_with_compile_and_build(|builder| { - let runner = builder - .root(sample_scripts()) - .bin(BIN) - .input("readme.js") - .build()?; - - runner.assert_producers() - }) -} +#[javy_cli_test] +fn test_producers_section_present(builder: &mut Builder) -> Result<()> { + let runner = builder.input("readme.js").build()?; -#[test] -fn test_error_handling() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .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(()) - }) + runner.assert_producers() } -#[test] -fn test_same_module_outputs_different_random_result() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .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(()) - }) +#[javy_cli_test] +fn test_error_handling(builder: &mut Builder) -> Result<()> { + let mut runner = builder.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_exported_default_arrow_fn() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .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(()) - }) +#[javy_cli_test] +fn test_same_module_outputs_different_random_result(builder: &mut Builder) -> Result<()> { + let mut runner = builder.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_fn() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .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(()) - }) +#[javy_cli_test] +fn test_exported_default_arrow_fn(builder: &mut Builder) -> Result<()> { + let mut runner = builder + .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(()) } -fn sample_scripts() -> PathBuf { - PathBuf::from(ROOT).join("tests").join("sample-scripts") +#[javy_cli_test] +fn test_exported_default_fn(builder: &mut Builder) -> Result<()> { + let mut runner = builder + .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 run_with_u8s(r: &mut Runner, stdin: u8) -> (u8, String, u64) { diff --git a/crates/cli/tests/javy_quickjs_provider_v2.wasm b/crates/cli/tests/javy_quickjs_provider_v2.wasm new file mode 100644 index 00000000..b9cfee79 Binary files /dev/null and b/crates/cli/tests/javy_quickjs_provider_v2.wasm differ diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 581ce482..ab816aca 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -24,9 +24,9 @@ use bitflags::bitflags; bitflags! { - #[derive(Eq, PartialEq)] + #[derive(Eq, PartialEq, Debug)] pub struct Config: u32 { - const OVERRIDE_JSON_PARSE_AND_STRINGIFY = 1; + const SIMD_JSON_BUILTINS = 1; const JAVY_JSON = 1 << 1; const JAVY_STREAM_IO = 1 << 2; const REDIRECT_STDOUT_TO_STDERR = 1 << 3; @@ -37,8 +37,8 @@ bitflags! { impl Default for Config { fn default() -> Self { let mut config = Config::empty(); - config.set(Config::OVERRIDE_JSON_PARSE_AND_STRINGIFY, false); - config.set(Config::JAVY_JSON, false); + config.set(Config::SIMD_JSON_BUILTINS, true); + config.set(Config::JAVY_JSON, true); config.set(Config::JAVY_STREAM_IO, true); config.set(Config::REDIRECT_STDOUT_TO_STDERR, true); config.set(Config::TEXT_ENCODING, true); @@ -51,7 +51,7 @@ mod tests { use super::Config; #[test] fn check_bits() { - assert!(Config::OVERRIDE_JSON_PARSE_AND_STRINGIFY == Config::from_bits(1).unwrap()); + assert!(Config::SIMD_JSON_BUILTINS == Config::from_bits(1).unwrap()); assert!(Config::JAVY_JSON == Config::from_bits(1 << 1).unwrap()); assert!(Config::JAVY_STREAM_IO == Config::from_bits(1 << 2).unwrap()); assert!(Config::REDIRECT_STDOUT_TO_STDERR == Config::from_bits(1 << 3).unwrap()); diff --git a/crates/core/src/runtime.rs b/crates/core/src/runtime.rs index e50beacc..b707c732 100644 --- a/crates/core/src/runtime.rs +++ b/crates/core/src/runtime.rs @@ -8,9 +8,7 @@ pub(crate) fn new(shared_config: SharedConfig) -> Result { .text_encoding(shared_config.contains(SharedConfig::TEXT_ENCODING)) .redirect_stdout_to_stderr(shared_config.contains(SharedConfig::REDIRECT_STDOUT_TO_STDERR)) .javy_stream_io(shared_config.contains(SharedConfig::JAVY_STREAM_IO)) - .override_json_parse_and_stringify( - shared_config.contains(SharedConfig::OVERRIDE_JSON_PARSE_AND_STRINGIFY), - ) + .simd_json_builtins(shared_config.contains(SharedConfig::SIMD_JSON_BUILTINS)) .javy_json(shared_config.contains(SharedConfig::JAVY_JSON)); Runtime::new(std::mem::take(config)) diff --git a/crates/javy/src/apis/console/mod.rs b/crates/javy/src/apis/console/mod.rs index b7135e24..2bbcb629 100644 --- a/crates/javy/src/apis/console/mod.rs +++ b/crates/javy/src/apis/console/mod.rs @@ -112,12 +112,12 @@ mod tests { ctx.with(|this| { register(this.clone(), stream.clone(), stream.clone()).unwrap(); - this.eval("console.log(\"hello world\");")?; + this.eval::<(), _>("console.log(\"hello world\");")?; assert_eq!(b"hello world\n", stream.buffer.borrow().as_slice()); stream.clear(); macro_rules! test_console_log { ($js:expr, $expected:expr) => {{ - this.eval($js)?; + this.eval::<(), _>($js)?; assert_eq!( $expected, std::str::from_utf8(stream.buffer.borrow().as_slice()).unwrap() @@ -195,13 +195,13 @@ mod tests { ctx.with(|this| { register(this.clone(), log_stream.clone(), error_stream.clone()).unwrap(); - this.eval("console.log(\"hello world\");")?; + this.eval::<(), _>("console.log(\"hello world\");")?; assert_eq!(b"hello world\n", log_stream.buffer.borrow().as_slice()); assert!(error_stream.buffer.borrow().is_empty()); log_stream.clear(); - this.eval("console.error(\"hello world\");")?; + this.eval::<(), _>("console.error(\"hello world\");")?; assert_eq!(b"hello world\n", error_stream.buffer.borrow().as_slice()); assert!(log_stream.buffer.borrow().is_empty()); diff --git a/crates/javy/src/apis/random/mod.rs b/crates/javy/src/apis/random/mod.rs index f27bd7ab..5bf4d67e 100644 --- a/crates/javy/src/apis/random/mod.rs +++ b/crates/javy/src/apis/random/mod.rs @@ -31,7 +31,7 @@ mod tests { runtime.context().with(|this| { let mut eval_opts = EvalOptions::default(); eval_opts.strict = false; - this.eval_with_options("result = Math.random()", eval_opts)?; + this.eval_with_options::<(), _>("result = Math.random()", eval_opts)?; let result: f64 = this .globals() .get::<&str, Value<'_>>("result")? diff --git a/crates/javy/src/apis/stream_io/mod.rs b/crates/javy/src/apis/stream_io/mod.rs index a02c8205..c0076ffb 100644 --- a/crates/javy/src/apis/stream_io/mod.rs +++ b/crates/javy/src/apis/stream_io/mod.rs @@ -37,7 +37,7 @@ fn register(this: Ctx<'_>) -> Result<()> { }), )?; - this.eval(include_str!("io.js"))?; + this.eval::<(), _>(include_str!("io.js"))?; Ok::<_, Error>(()) } diff --git a/crates/javy/src/apis/text_encoding/mod.rs b/crates/javy/src/apis/text_encoding/mod.rs index 6772937d..7bfdd6af 100644 --- a/crates/javy/src/apis/text_encoding/mod.rs +++ b/crates/javy/src/apis/text_encoding/mod.rs @@ -36,7 +36,7 @@ fn register(this: Ctx<'_>) -> Result<()> { )?; let mut opts = EvalOptions::default(); opts.strict = false; - this.eval_with_options(include_str!("./text-encoding.js"), opts)?; + this.eval_with_options::<(), _>(include_str!("./text-encoding.js"), opts)?; Ok::<_, Error>(()) } diff --git a/crates/javy/src/config.rs b/crates/javy/src/config.rs index 6f542642..6521aca7 100644 --- a/crates/javy/src/config.rs +++ b/crates/javy/src/config.rs @@ -58,7 +58,7 @@ pub struct Config { /// serde_json and simd_json. /// This setting requires the `JSON` intrinsic to be enabled, and the `json` /// crate feature to be enabled as well. - pub(crate) override_json_parse_and_stringify: bool, + pub(crate) simd_json_builtins: bool, } impl Default for Config { @@ -70,7 +70,7 @@ impl Default for Config { intrinsics, javy_intrinsics: JavyIntrinsics::empty(), redirect_stdout_to_stderr: false, - override_json_parse_and_stringify: false, + simd_json_builtins: false, } } } @@ -193,13 +193,13 @@ impl Config { /// crate feature to be enabled as well. /// Disabled by default. #[cfg(feature = "json")] - pub fn override_json_parse_and_stringify(&mut self, enable: bool) -> &mut Self { - self.override_json_parse_and_stringify = enable; + pub fn simd_json_builtins(&mut self, enable: bool) -> &mut Self { + self.simd_json_builtins = enable; self } pub(crate) fn validate(self) -> Result { - if self.override_json_parse_and_stringify && !self.intrinsics.contains(JSIntrinsics::JSON) { + if self.simd_json_builtins && !self.intrinsics.contains(JSIntrinsics::JSON) { bail!("JSON Intrinsic is required to override JSON.parse and JSON.stringify"); } @@ -215,7 +215,7 @@ mod tests { #[test] fn err_config_validation() { let mut config = Config::default(); - config.override_json_parse_and_stringify(true); + config.simd_json_builtins(true); config.json(false); assert!(config.validate().is_err()); @@ -224,7 +224,7 @@ mod tests { #[test] fn ok_config_validation() { let mut config = Config::default(); - config.override_json_parse_and_stringify(true); + config.simd_json_builtins(true); assert!(config.validate().is_ok()); } diff --git a/crates/javy/src/runtime.rs b/crates/javy/src/runtime.rs index 22d2f9ab..1559666a 100644 --- a/crates/javy/src/runtime.rs +++ b/crates/javy/src/runtime.rs @@ -85,7 +85,7 @@ impl Runtime { unsafe { intrinsic::Json::add_intrinsic(ctx.as_raw()) } } - if cfg.override_json_parse_and_stringify { + if cfg.simd_json_builtins { #[cfg(feature = "json")] unsafe { Json::add_intrinsic(ctx.as_raw()) diff --git a/crates/javy/tests/misc.rs b/crates/javy/tests/misc.rs index b359c6ab..1c75599e 100644 --- a/crates/javy/tests/misc.rs +++ b/crates/javy/tests/misc.rs @@ -7,7 +7,7 @@ use javy::{quickjs::context::EvalOptions, Config, Runtime}; #[test] fn string_keys_and_ref_counting() -> Result<()> { let mut config = Config::default(); - config.override_json_parse_and_stringify(true); + config.simd_json_builtins(true); let source = include_bytes!("string_keys_and_ref_counting.js"); let rt = Runtime::new(config)?; @@ -26,7 +26,7 @@ fn string_keys_and_ref_counting() -> Result<()> { #[test] fn json_stringify_cycle_checks() -> Result<()> { let mut config = Config::default(); - config.override_json_parse_and_stringify(true); + config.simd_json_builtins(true); let source = include_bytes!("stringify_cycle.js"); let rt = Runtime::new(config)?; diff --git a/crates/runner/src/lib.rs b/crates/runner/src/lib.rs index a54fe505..805a15d2 100644 --- a/crates/runner/src/lib.rs +++ b/crates/runner/src/lib.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; use std::error::Error; use std::fmt::{self, Display, Formatter}; use std::io::{self, Cursor, Write}; @@ -9,9 +9,7 @@ use tempfile::TempDir; use wasi_common::pipe::{ReadPipe, WritePipe}; use wasi_common::sync::WasiCtxBuilder; use wasi_common::WasiCtx; -use wasmtime::{ - AsContextMut, Config, Engine, ExternType, Instance, Linker, Module, OptLevel, Store, -}; +use wasmtime::{AsContextMut, Config, Engine, Instance, Linker, Module, OptLevel, Store}; #[derive(Clone)] pub enum JavyCommand { @@ -33,11 +31,23 @@ pub struct Builder { world: Option, /// Whether console.log should write to stderr. redirect_stdout_to_stderr: Option, + /// Whether to enable the `Javy.JSON` builtins. + javy_json: Option, + /// Whether to enable the `Javy.IO` builtins. + javy_stream_io: Option, + /// Whether to override JSON.parse and JSON.stringify with a SIMD based + /// implementation. + simd_json_builtins: Option, + /// Whether to enable the `TextEncoder` and `TextDecoder` APIs. + text_encoding: Option, built: bool, /// Preload the module at path, using the given instance name. preload: Option<(String, PathBuf)>, /// Whether to use the `compile` or `build` command. command: JavyCommand, + /// The javy provider version. + /// Used for import validation purposes only. + provider_version: u8, } impl Default for Builder { @@ -52,6 +62,11 @@ impl Default for Builder { preload: None, command: JavyCommand::Build, redirect_stdout_to_stderr: None, + javy_stream_io: None, + javy_json: None, + simd_json_builtins: None, + text_encoding: None, + provider_version: 3, } } } @@ -92,11 +107,36 @@ impl Builder { self } + pub fn javy_json(&mut self, enabled: bool) -> &mut Self { + self.javy_json = Some(enabled); + self + } + + pub fn javy_stream_io(&mut self, enabled: bool) -> &mut Self { + self.javy_stream_io = Some(enabled); + self + } + + pub fn simd_json_builtins(&mut self, enabled: bool) -> &mut Self { + self.simd_json_builtins = Some(enabled); + self + } + + pub fn text_encoding(&mut self, enabled: bool) -> &mut Self { + self.text_encoding = Some(enabled); + self + } + pub fn command(&mut self, command: JavyCommand) -> &mut Self { self.command = command; self } + pub fn provider_version(&mut self, vsn: u8) -> &mut Self { + self.provider_version = vsn; + self + } + pub fn build(&mut self) -> Result { if self.built { bail!("Builder already used to build a runner") @@ -115,9 +155,14 @@ impl Builder { world, root, redirect_stdout_to_stderr, + javy_json, + javy_stream_io, + simd_json_builtins, + text_encoding, built: _, preload, command, + provider_version, } = std::mem::take(self); self.built = true; @@ -125,9 +170,17 @@ impl Builder { match command { JavyCommand::Compile => { if let Some(preload) = preload { - Runner::compile_dynamic(bin_path, root, input, wit, world, preload) + Runner::compile_dynamic( + bin_path, + root, + input, + wit, + world, + preload, + provider_version, + ) } else { - Runner::compile_static(bin_path, root, input, wit, world) + Runner::compile_static(bin_path, root, input, wit, world, provider_version) } } JavyCommand::Build => Runner::build( @@ -137,6 +190,10 @@ impl Builder { wit, world, redirect_stdout_to_stderr, + javy_json, + javy_stream_io, + simd_json_builtins, + text_encoding, preload, ), } @@ -148,6 +205,7 @@ pub struct Runner { linker: Linker, initial_fuel: u64, preload: Option<(String, Vec)>, + provider_version: u8, } #[derive(Debug)] @@ -194,6 +252,7 @@ impl StoreContext { } impl Runner { + #[allow(clippy::too_many_arguments)] fn build( bin: String, root: PathBuf, @@ -201,6 +260,10 @@ impl Runner { wit: Option, world: Option, redirect_stdout_to_stderr: Option, + javy_json: Option, + javy_stream_io: Option, + override_json_parse_and_stringify: Option, + text_encoding: Option, preload: Option<(String, PathBuf)>, ) -> Result { // This directory is unique and will automatically get deleted @@ -217,6 +280,10 @@ impl Runner { &world, preload.is_some(), &redirect_stdout_to_stderr, + &javy_json, + &javy_stream_io, + &override_json_parse_and_stringify, + &text_encoding, ); Self::exec_command(bin, root, args)?; @@ -238,6 +305,7 @@ impl Runner { linker, initial_fuel: u64::MAX, preload, + provider_version: 3, }) } @@ -247,6 +315,7 @@ impl Runner { source: impl AsRef, wit: Option, world: Option, + vsn: u8, ) -> Result { // This directory is unique and will automatically get deleted // when `tempdir` goes out of scope. @@ -269,6 +338,7 @@ impl Runner { linker, initial_fuel: u64::MAX, preload: None, + provider_version: vsn, }) } @@ -279,6 +349,7 @@ impl Runner { wit: Option, world: Option, preload: (String, PathBuf), + vsn: u8, ) -> Result { let tempdir = tempfile::tempdir()?; let wasm_file = Self::out_wasm(&tempdir); @@ -301,6 +372,7 @@ impl Runner { linker, initial_fuel: u64::MAX, preload: Some((preload.0, preload_module)), + provider_version: vsn, }) } @@ -311,48 +383,71 @@ impl Runner { linker: Self::setup_linker(&engine)?, initial_fuel: u64::MAX, preload: None, + provider_version: 3, }) } pub fn assert_known_base_imports(&self) -> Result<()> { let module = Module::from_binary(self.linker.engine(), &self.wasm)?; + let instance_name = format!("javy_quickjs_provider_v{}", self.provider_version); + + let result = module.imports().filter(|i| { + if i.module() == instance_name && i.name() == "canonical_abi_realloc" { + let ty = i.ty(); + let f = ty.unwrap_func(); + return f.params().all(|p| p.is_i32()) + && f.params().len() == 4 + && f.results().len() == 1 + && f.results().all(|r| r.is_i32()); + } - for import in module.imports() { - match (import.module(), import.name(), import.ty()) { - ("javy_quickjs_provider_v2", "canonical_abi_realloc", ExternType::Func(f)) - if f.params().map(|t| t.is_i32()).eq([true, true, true, true]) - && f.results().map(|t| t.is_i32()).eq([true]) => {} - ("javy_quickjs_provider_v2", "eval_bytecode", ExternType::Func(f)) - if f.params().map(|t| t.is_i32()).eq([true, true]) - && f.results().len() == 0 => {} - ("javy_quickjs_provider_v2", "memory", ExternType::Memory(_)) => (), - _ => panic!("Unknown import {:?}", import), + if i.module() == instance_name && i.name() == "eval_bytecode" { + let ty = i.ty(); + let f = ty.unwrap_func(); + return f.params().all(|p| p.is_i32()) + && f.params().len() == 2 + && f.results().len() == 0; } - } - Ok(()) + if i.module() == instance_name && i.name() == "memory" { + let ty = i.ty(); + return ty.memory().is_some(); + } + + false + }); + + let count = result.count(); + if count == 3 { + Ok(()) + } else { + Err(anyhow!("Unexpected number of imports: {}", count)) + } } pub fn assert_known_named_function_imports(&self) -> Result<()> { - let module = Module::from_binary(self.linker.engine(), &self.wasm)?; + self.assert_known_base_imports()?; - for import in module.imports() { - match (import.module(), import.name(), import.ty()) { - ("javy_quickjs_provider_v2", "canonical_abi_realloc", ExternType::Func(f)) - if f.params().map(|t| t.is_i32()).eq([true, true, true, true]) - && f.results().map(|t| t.is_i32()).eq([true]) => {} - ("javy_quickjs_provider_v2", "eval_bytecode", ExternType::Func(f)) - if f.params().map(|t| t.is_i32()).eq([true, true]) - && f.results().len() == 0 => {} - ("javy_quickjs_provider_v2", "memory", ExternType::Memory(_)) => (), - ("javy_quickjs_provider_v2", "invoke", ExternType::Func(f)) - if f.params().map(|t| t.is_i32()).eq([true, true, true, true]) - && f.results().len() == 0 => {} - _ => panic!("Unknown import {:?}", import), + let module = Module::from_binary(self.linker.engine(), &self.wasm)?; + let instance_name = format!("javy_quickjs_provider_v{}", self.provider_version); + let result = module.imports().filter(|i| { + if i.module() == instance_name && i.name() == "invoke" { + let ty = i.ty(); + let f = ty.unwrap_func(); + + return f.params().len() == 4 + && f.params().all(|p| p.is_i32()) + && f.results().len() == 0; } - } - Ok(()) + false + }); + let count = result.count(); + if count == 1 { + Ok(()) + } else { + Err(anyhow!("Unexpected number of imports: {}", count)) + } } pub fn assert_producers(&self) -> Result<()> { @@ -399,6 +494,13 @@ impl Runner { file } + // TODO: Some of the methods in the Runner (`build`, `build_args`) could be + // refactored to take structs as parameters rather than individual + // parameters to avoid verbosity. + // + // This refactoring will be a bit challenging until we fully deprecate the + // `compile` command. + #[allow(clippy::too_many_arguments)] fn build_args( input: &Path, out: &Path, @@ -406,6 +508,10 @@ impl Runner { world: &Option, dynamic: bool, redirect_stdout_to_stderr: &Option, + javy_json: &Option, + javy_stream_io: &Option, + simd_json_builtins: &Option, + text_encoding: &Option, ) -> Vec { let mut args = vec![ "build".to_string(), @@ -434,6 +540,32 @@ impl Runner { )); } + if let Some(enabled) = *javy_json { + args.push("-J".to_string()); + args.push(format!("javy-json={}", if enabled { "y" } else { "n" })); + } + + if let Some(enabled) = *javy_stream_io { + args.push("-J".to_string()); + args.push(format!( + "javy-stream-io={}", + if enabled { "y" } else { "n" } + )); + } + + if let Some(enabled) = *simd_json_builtins { + args.push("-J".to_string()); + args.push(format!( + "simd-json-builtins={}", + if enabled { "y" } else { "n" } + )); + } + + if let Some(enabled) = *text_encoding { + args.push("-J".to_string()); + args.push(format!("text-encoding={}", if enabled { "y" } else { "n" })); + } + args } diff --git a/crates/test-macros/src/lib.rs b/crates/test-macros/src/lib.rs index 7d87e894..a4acf3d4 100644 --- a/crates/test-macros/src/lib.rs +++ b/crates/test-macros/src/lib.rs @@ -15,7 +15,7 @@ use proc_macro::TokenStream; use proc_macro2::Span; use quote::quote; use std::path::{Path, PathBuf}; -use syn::{parse_macro_input, Ident, LitStr, Result}; +use syn::{meta::ParseNestedMeta, parse_macro_input, Ident, LitBool, LitStr, Result, ReturnType}; struct Config262 { root: PathBuf, @@ -61,7 +61,7 @@ pub fn t262(stream: TokenStream) -> TokenStream { parse_macro_input!(stream with config_parser); - match expand(&config) { + match expand_262(&config) { Ok(tok) => tok, Err(e) => e.into_compile_error().into(), } @@ -84,7 +84,7 @@ fn ignore(test_name: &str) -> bool { .contains(&test_name) } -fn expand(config: &Config262) -> Result { +fn expand_262(config: &Config262) -> Result { let harness = config.root.join("harness"); let harness_str = harness.into_os_string().into_string().unwrap(); let json_parse = config @@ -141,7 +141,7 @@ fn gen_tests( fn #test_name() { let mut config = ::javy::Config::default(); config - .override_json_parse_and_stringify(true); + .simd_json_builtins(true); let runtime = ::javy::Runtime::new(config).expect("runtime to be created"); let harness_path = ::std::path::PathBuf::from(#harness_str); @@ -172,3 +172,169 @@ fn gen_tests( #(#spec)* } } + +struct CliTestConfig { + /// Root directory to load test scripts from, relative to the crate's + /// directory (i.e., `CARGO_MANIFEST_DIR`) + scripts_root: String, + /// Which commands to generate the test for. It can be either `compile` or + /// `build`. + commands: Vec, + /// Tests Javy's dynamic linking capabilities. + dynamic: bool, +} + +impl CliTestConfig { + fn commands_from(&mut self, meta: &ParseNestedMeta) -> Result<()> { + meta.parse_nested_meta(|meta| { + if meta.path.is_ident("not") { + meta.parse_nested_meta(|meta| { + if meta.path.is_ident("Compile") || meta.path.is_ident("Build") { + let id = meta.path.require_ident()?.clone(); + self.commands.retain(|s| *s != id); + Ok(()) + } else { + Err(meta.error("Unknown command")) + } + }) + } else { + Err(meta.error("Unknown identifier")) + } + })?; + Ok(()) + } + + fn root_from(&mut self, meta: &ParseNestedMeta) -> Result<()> { + if meta.path.is_ident("root") { + let val = meta.value()?; + let val: LitStr = val.parse()?; + self.scripts_root = val.value(); + Ok(()) + } else { + Err(meta.error("Unknown value")) + } + } + + fn dynamic_from(&mut self, meta: &ParseNestedMeta) -> Result<()> { + if meta.path.is_ident("dyn") { + let val = meta.value()?; + let val: LitBool = val.parse()?; + self.dynamic = val.value(); + Ok(()) + } else { + Err(meta.error("Unknown value")) + } + } +} + +impl Default for CliTestConfig { + fn default() -> Self { + Self { + scripts_root: String::from("tests/sample-scripts"), + commands: vec![ + Ident::new("Compile", Span::call_site()), + Ident::new("Build", Span::call_site()), + ], + dynamic: false, + } + } +} + +#[proc_macro_attribute] +pub fn javy_cli_test(attrs: TokenStream, item: TokenStream) -> TokenStream { + let mut config = CliTestConfig::default(); + let config_parser = syn::meta::parser(|meta| { + if meta.path.is_ident("commands") { + config.commands_from(&meta) + } else if meta.path.is_ident("root") { + config.root_from(&meta) + } else if meta.path.is_ident("dyn") { + config.dynamic_from(&meta) + } else { + Err(meta.error("Unsupported attributes")) + } + }); + + parse_macro_input!(attrs with config_parser); + + match expand_cli_tests(&config, parse_macro_input!(item as syn::ItemFn)) { + Ok(tok) => tok, + Err(e) => e.into_compile_error().into(), + } +} + +fn expand_cli_tests(test_config: &CliTestConfig, func: syn::ItemFn) -> Result { + let mut tests = vec![quote! { #func }]; + let attrs = &func.attrs; + + for ident in &test_config.commands { + let command_name = ident.to_string(); + let func_name = &func.sig.ident; + let ret = match &func.sig.output { + ReturnType::Default => quote! { () }, + ReturnType::Type(_, ty) => quote! { -> #ty }, + }; + let test_name = Ident::new( + &format!("{}_{}", command_name.to_lowercase(), func_name), + func_name.span(), + ); + + let preload_setup = if test_config.dynamic { + // The compile commmand will remain frozen until it becomes + // deprecated in Javy v4.0.0. Until then we test with a frozen + // artifact downloaded from the releases. + if command_name == "Compile" { + quote! { + let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + builder.preload( + "javy_quickjs_provider_v2".into(), + root.join("tests").join("javy_quickjs_provider_v2.wasm") + ); + builder.provider_version(2); + } + } else { + quote! { + let mut root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + root.pop(); + root.pop(); + root = root.join( + std::path::Path::new("target") + .join("wasm32-wasi") + .join("release") + .join("javy_quickjs_provider_wizened.wasm"), + ); + // TODO: Deriving the current provider version could be done + // automatically somehow. It's fine for now, given that if the + // version changes and this is not updated, tests will fail. + builder.preload("javy_quickjs_provider_v3".into(), root); + builder.provider_version(3); + } + } + } else { + quote! {} + }; + + let root = test_config.scripts_root.clone(); + + let tok = quote! { + #[test] + #(#attrs)* + fn #test_name() #ret { + let mut builder = javy_runner::Builder::default(); + builder.command(javy_runner::JavyCommand::#ident); + builder.root(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(#root)); + builder.bin(env!("CARGO_BIN_EXE_javy")); + + #preload_setup + + #func_name(&mut builder) + } + }; + + tests.push(tok); + } + Ok(quote! { + #(#tests)* + } + .into()) +} diff --git a/fuzz/fuzz_targets/json_differential.rs b/fuzz/fuzz_targets/json_differential.rs index 04e6f955..7abb0324 100644 --- a/fuzz/fuzz_targets/json_differential.rs +++ b/fuzz/fuzz_targets/json_differential.rs @@ -18,9 +18,7 @@ static SETUP: Once = Once::new(); fuzz_target!(|data: ArbitraryValue| { SETUP.call_once(|| { let mut config = Config::default(); - config - .override_json_parse_and_stringify(true) - .javy_json(true); + config.simd_json_builtins(true).javy_json(true); unsafe { RT = Some(Runtime::new(std::mem::take(&mut config)).expect("Runtime to be created"));