From a53a7a4f475f54e3c2862096af53ae167d2e6fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Cabrera?= Date: Wed, 14 Aug 2024 16:33:55 -0400 Subject: [PATCH] Refactor integration tests (#727) * Refactor integration tests This commit refactors the integration tests to use the `javy-runner` crate. The main motivation for this change is to reduce duplication and make it easier to test more complex scenarios in the future e.g., the upcoming changes in https://github.com/bytecodealliance/javy/issues/702 * Style changes * More style changes --- Cargo.lock | 1 + crates/cli/tests/common/mod.rs | 60 --- crates/cli/tests/dylib_test.rs | 166 ++----- .../tests/dynamic-linking-scripts/console.js | 1 + .../errors-in-exported-functions.js | 3 + .../errors-in-exported-functions.wit | 5 + .../dynamic-linking-scripts/javy-json-id.js | 1 + .../linking-arrow-func.js | 1 + .../linking-arrow-func.wit | 5 + .../linking-with-func-without-flag.js | 5 + .../linking-with-func.js | 5 + .../linking-with-func.wit | 5 + crates/cli/tests/dynamic_linking_test.rs | 300 +++++-------- crates/cli/tests/integration_test.rs | 19 +- crates/runner/Cargo.toml | 1 + crates/runner/src/lib.rs | 410 +++++++++++++++--- 16 files changed, 529 insertions(+), 459 deletions(-) delete mode 100644 crates/cli/tests/common/mod.rs create mode 100644 crates/cli/tests/dynamic-linking-scripts/console.js create mode 100644 crates/cli/tests/dynamic-linking-scripts/errors-in-exported-functions.js create mode 100644 crates/cli/tests/dynamic-linking-scripts/errors-in-exported-functions.wit create mode 100644 crates/cli/tests/dynamic-linking-scripts/javy-json-id.js create mode 100644 crates/cli/tests/dynamic-linking-scripts/linking-arrow-func.js create mode 100644 crates/cli/tests/dynamic-linking-scripts/linking-arrow-func.wit create mode 100644 crates/cli/tests/dynamic-linking-scripts/linking-with-func-without-flag.js create mode 100644 crates/cli/tests/dynamic-linking-scripts/linking-with-func.js create mode 100644 crates/cli/tests/dynamic-linking-scripts/linking-with-func.wit diff --git a/Cargo.lock b/Cargo.lock index 354ef265..543d4846 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1523,6 +1523,7 @@ dependencies = [ "tempfile", "uuid", "wasi-common", + "wasmparser 0.212.0", "wasmtime", "wasmtime-wasi", ] diff --git a/crates/cli/tests/common/mod.rs b/crates/cli/tests/common/mod.rs deleted file mode 100644 index 3da9ffe7..00000000 --- a/crates/cli/tests/common/mod.rs +++ /dev/null @@ -1,60 +0,0 @@ -use anyhow::Result; -use std::path::{Path, PathBuf}; -use wasmtime::{Engine, Module}; - -// Allows dead code b/c each integration test suite is considered its own -// application and this function is used by 2 of 3 suites. -#[allow(dead_code)] -pub fn create_quickjs_provider_module(engine: &Engine) -> Result { - let mut lib_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - 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"), - ); - Module::from_file(engine, lib_path) -} - -// Allows dead code b/c each integration test suite is considered its own -// application and this function is used by 2 of 3 suites. -#[allow(dead_code)] -pub fn assert_producers_section_is_correct(wasm: &[u8]) -> Result<()> { - let producers_section = wasmparser::Parser::new(0) - .parse_all(wasm) - .find_map(|payload| { - if let Ok(wasmparser::Payload::CustomSection(c)) = payload { - if let wasmparser::KnownCustom::Producers(r) = c.as_known() { - return Some(r); - } - } - None - }) - .expect("Should have producers custom section"); - let fields = producers_section - .into_iter() - .collect::, _>>()?; - - assert_eq!(2, fields.len()); - - let language_field = &fields[0]; - assert_eq!("language", language_field.name); - assert_eq!(1, language_field.values.count()); - let language_value = language_field.values.clone().into_iter().next().unwrap()?; - assert_eq!("JavaScript", language_value.name); - assert_eq!("ES2020", language_value.version); - - let processed_by_field = &fields[1]; - assert_eq!("processed-by", processed_by_field.name); - assert_eq!(1, processed_by_field.values.count()); - let processed_by_value = processed_by_field - .values - .clone() - .into_iter() - .next() - .unwrap()?; - assert_eq!("Javy", processed_by_value.name); - Ok(()) -} diff --git a/crates/cli/tests/dylib_test.rs b/crates/cli/tests/dylib_test.rs index 89463072..a1ed9f99 100644 --- a/crates/cli/tests/dylib_test.rs +++ b/crates/cli/tests/dylib_test.rs @@ -1,19 +1,17 @@ use anyhow::Result; -use std::boxed::Box; +use javy_runner::{Runner, RunnerError}; +use std::path::{Path, PathBuf}; use std::str; -use wasi_common::{pipe::WritePipe, sync::WasiCtxBuilder, WasiCtx, WasiFile}; -use wasmtime::{AsContextMut, Engine, Instance, Linker, Store}; -mod common; +static ROOT: &str = env!("CARGO_MANIFEST_DIR"); #[test] fn test_dylib() -> Result<()> { let js_src = "console.log(42);"; - let stderr = WritePipe::new_in_memory(); - run_js_src(js_src, &stderr)?; + let mut runner = Runner::with_dylib(provider_module()?)?; - let output = stderr.try_into_inner().unwrap().into_inner(); - assert_eq!("42\n", str::from_utf8(&output)?); + let (_, logs, _) = runner.exec_through_dylib(js_src, None)?; + assert_eq!("42\n", str::from_utf8(&logs)?); Ok(()) } @@ -21,14 +19,19 @@ fn test_dylib() -> Result<()> { #[test] fn test_dylib_with_error() -> Result<()> { let js_src = "function foo() { throw new Error('foo error'); } foo();"; - let stderr = WritePipe::new_in_memory(); - let result = run_js_src(js_src, &stderr); - assert!(result.is_err()); - let output = stderr.try_into_inner().unwrap().into_inner(); + let mut runner = Runner::with_dylib(provider_module()?)?; + let res = runner.exec_through_dylib(js_src, None); + + assert!(res.is_err()); + + let e = res.err().unwrap(); let expected_log_output = "Error:1:24 foo error\n at foo (function.mjs:1:24)\n at (function.mjs:1:50)\n\n"; - assert_eq!(expected_log_output, str::from_utf8(&output)?); + assert_eq!( + expected_log_output, + String::from_utf8(e.downcast_ref::().unwrap().stderr.clone())? + ); Ok(()) } @@ -36,134 +39,25 @@ fn test_dylib_with_error() -> Result<()> { #[test] fn test_dylib_with_exported_func() -> Result<()> { let js_src = "export function foo() { console.log('In foo'); }; console.log('Toplevel');"; - let stderr = WritePipe::new_in_memory(); - run_invoke(js_src, "foo", &stderr)?; - let output = stderr.try_into_inner().unwrap().into_inner(); - assert_eq!("Toplevel\nIn foo\n", str::from_utf8(&output)?); + let mut runner = Runner::with_dylib(provider_module()?)?; - Ok(()) -} + let (_, logs, _) = runner.exec_through_dylib(js_src, Some("foo"))?; + assert_eq!("Toplevel\nIn foo\n", str::from_utf8(&logs)?); -fn run_js_src(js_src: &str, stderr: &T) -> Result<()> { - let (instance, mut store) = create_wasm_env(stderr)?; - - let eval_bytecode_func = - instance.get_typed_func::<(u32, u32), ()>(store.as_context_mut(), "eval_bytecode")?; - let (bytecode_ptr, bytecode_len) = - compile_src(js_src.as_bytes(), &instance, store.as_context_mut())?; - eval_bytecode_func.call(store.as_context_mut(), (bytecode_ptr, bytecode_len))?; Ok(()) } -fn run_invoke( - js_src: &str, - fn_to_invoke: &str, - stderr: &T, -) -> Result<()> { - let (instance, mut store) = create_wasm_env(stderr)?; - - let invoke_func = - instance.get_typed_func::<(u32, u32, u32, u32), ()>(store.as_context_mut(), "invoke")?; - let (bytecode_ptr, bytecode_len) = - compile_src(js_src.as_bytes(), &instance, store.as_context_mut())?; - let (fn_name_ptr, fn_name_len) = - copy_func_name(fn_to_invoke, &instance, store.as_context_mut())?; - invoke_func.call( - store.as_context_mut(), - (bytecode_ptr, bytecode_len, fn_name_ptr, fn_name_len), - )?; - Ok(()) -} - -fn create_wasm_env( - stderr: &T, -) -> Result<(Instance, Store)> { - let engine = Engine::default(); - let mut linker = Linker::new(&engine); - wasi_common::sync::add_to_linker(&mut linker, |s| s)?; - let wasi = WasiCtxBuilder::new() - .stderr(Box::new(stderr.clone())) - .build(); - let module = common::create_quickjs_provider_module(&engine)?; - - let mut store = Store::new(&engine, wasi); - let instance = linker.instantiate(store.as_context_mut(), &module)?; - - Ok((instance, store)) -} - -fn compile_src( - js_src: &[u8], - instance: &Instance, - mut store: impl AsContextMut, -) -> Result<(u32, u32)> { - let memory = instance - .get_memory(store.as_context_mut(), "memory") - .unwrap(); - let compile_src_func = - instance.get_typed_func::<(u32, u32), u32>(store.as_context_mut(), "compile_src")?; - - let js_src_ptr = allocate_memory( - instance, - store.as_context_mut(), - 1, - js_src.len().try_into()?, - )?; - memory.write(store.as_context_mut(), js_src_ptr.try_into()?, js_src)?; - - let ret_ptr = compile_src_func.call( - store.as_context_mut(), - (js_src_ptr, js_src.len().try_into()?), - )?; - let mut ret_buffer = [0; 8]; - memory.read(store.as_context(), ret_ptr.try_into()?, &mut ret_buffer)?; - let bytecode_ptr = u32::from_le_bytes(ret_buffer[0..4].try_into()?); - let bytecode_len = u32::from_le_bytes(ret_buffer[4..8].try_into()?); - - Ok((bytecode_ptr, bytecode_len)) -} - -fn copy_func_name( - fn_name: &str, - instance: &Instance, - mut store: impl AsContextMut, -) -> Result<(u32, u32)> { - let memory = instance - .get_memory(store.as_context_mut(), "memory") - .unwrap(); - let fn_name_bytes = fn_name.as_bytes(); - let fn_name_ptr = allocate_memory( - instance, - store.as_context_mut(), - 1, - fn_name_bytes.len().try_into()?, - )?; - memory.write( - store.as_context_mut(), - fn_name_ptr.try_into()?, - fn_name_bytes, - )?; - - Ok((fn_name_ptr, fn_name_bytes.len().try_into()?)) -} +fn provider_module() -> Result> { + 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"), + ); -fn allocate_memory( - instance: &Instance, - mut store: impl AsContextMut, - alignment: u32, - new_size: u32, -) -> Result { - let realloc_func = instance.get_typed_func::<(u32, u32, u32, u32), u32>( - store.as_context_mut(), - "canonical_abi_realloc", - )?; - let orig_ptr = 0; - let orig_size = 0; - realloc_func - .call( - store.as_context_mut(), - (orig_ptr, orig_size, alignment, new_size), - ) - .map_err(Into::into) + std::fs::read(lib_path).map_err(Into::into) } diff --git a/crates/cli/tests/dynamic-linking-scripts/console.js b/crates/cli/tests/dynamic-linking-scripts/console.js new file mode 100644 index 00000000..753a47d5 --- /dev/null +++ b/crates/cli/tests/dynamic-linking-scripts/console.js @@ -0,0 +1 @@ +console.log(42); diff --git a/crates/cli/tests/dynamic-linking-scripts/errors-in-exported-functions.js b/crates/cli/tests/dynamic-linking-scripts/errors-in-exported-functions.js new file mode 100644 index 00000000..8c35c074 --- /dev/null +++ b/crates/cli/tests/dynamic-linking-scripts/errors-in-exported-functions.js @@ -0,0 +1,3 @@ +export function foo() { + throw "Error"; +} diff --git a/crates/cli/tests/dynamic-linking-scripts/errors-in-exported-functions.wit b/crates/cli/tests/dynamic-linking-scripts/errors-in-exported-functions.wit new file mode 100644 index 00000000..3ce75bc3 --- /dev/null +++ b/crates/cli/tests/dynamic-linking-scripts/errors-in-exported-functions.wit @@ -0,0 +1,5 @@ +package local:main + +world foo-test { + export foo: func() +} diff --git a/crates/cli/tests/dynamic-linking-scripts/javy-json-id.js b/crates/cli/tests/dynamic-linking-scripts/javy-json-id.js new file mode 100644 index 00000000..a253ac64 --- /dev/null +++ b/crates/cli/tests/dynamic-linking-scripts/javy-json-id.js @@ -0,0 +1 @@ +console.log(Javy.JSON.toStdout(Javy.JSON.fromStdin())); diff --git a/crates/cli/tests/dynamic-linking-scripts/linking-arrow-func.js b/crates/cli/tests/dynamic-linking-scripts/linking-arrow-func.js new file mode 100644 index 00000000..74413b1c --- /dev/null +++ b/crates/cli/tests/dynamic-linking-scripts/linking-arrow-func.js @@ -0,0 +1 @@ +export default () => console.log(42) diff --git a/crates/cli/tests/dynamic-linking-scripts/linking-arrow-func.wit b/crates/cli/tests/dynamic-linking-scripts/linking-arrow-func.wit new file mode 100644 index 00000000..5efc8a3b --- /dev/null +++ b/crates/cli/tests/dynamic-linking-scripts/linking-arrow-func.wit @@ -0,0 +1,5 @@ +package local:test + +world exported-arrow { + export default: func() +} diff --git a/crates/cli/tests/dynamic-linking-scripts/linking-with-func-without-flag.js b/crates/cli/tests/dynamic-linking-scripts/linking-with-func-without-flag.js new file mode 100644 index 00000000..97893cac --- /dev/null +++ b/crates/cli/tests/dynamic-linking-scripts/linking-with-func-without-flag.js @@ -0,0 +1,5 @@ +export function foo() { + console.log('In foo'); +}; + +console.log('Toplevel'); diff --git a/crates/cli/tests/dynamic-linking-scripts/linking-with-func.js b/crates/cli/tests/dynamic-linking-scripts/linking-with-func.js new file mode 100644 index 00000000..dde17cac --- /dev/null +++ b/crates/cli/tests/dynamic-linking-scripts/linking-with-func.js @@ -0,0 +1,5 @@ +export function fooBar() { + console.log('In foo'); +}; + +console.log('Toplevel'); diff --git a/crates/cli/tests/dynamic-linking-scripts/linking-with-func.wit b/crates/cli/tests/dynamic-linking-scripts/linking-with-func.wit new file mode 100644 index 00000000..f420c5af --- /dev/null +++ b/crates/cli/tests/dynamic-linking-scripts/linking-with-func.wit @@ -0,0 +1,5 @@ +package local:main + +world foo-test { + export foo-bar: func() +} diff --git a/crates/cli/tests/dynamic_linking_test.rs b/crates/cli/tests/dynamic_linking_test.rs index cd2dc340..a827c477 100644 --- a/crates/cli/tests/dynamic_linking_test.rs +++ b/crates/cli/tests/dynamic_linking_test.rs @@ -1,44 +1,53 @@ use anyhow::Result; -use std::fs::{self, File}; -use std::io::{Cursor, Read, Write}; -use std::process::Command; +use javy_runner::Builder; +use std::path::{Path, PathBuf}; use std::str; -use uuid::Uuid; -use wasi_common::pipe::{ReadPipe, WritePipe}; -use wasi_common::sync::WasiCtxBuilder; -use wasi_common::WasiCtx; -use wasmtime::{AsContextMut, Config, Engine, ExternType, Linker, Module, Store}; -mod common; +static ROOT: &str = env!("CARGO_MANIFEST_DIR"); +static BIN: &str = env!("CARGO_BIN_EXE_javy"); #[test] pub fn test_dynamic_linking() -> Result<()> { - let js_src = "console.log(42);"; - let log_output = invoke_fn_on_generated_module(js_src, "_start", None, None, None)?; - assert_eq!("42\n", &log_output); + let mut runner = Builder::default() + .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(()) } #[test] pub fn test_dynamic_linking_with_func() -> Result<()> { - let js_src = "export function fooBar() { console.log('In foo'); }; console.log('Toplevel');"; - let wit = " - package local:main - - world foo-test { - export foo-bar: func() - } - "; - let log_output = - invoke_fn_on_generated_module(js_src, "foo-bar", Some((wit, "foo-test")), None, None)?; - assert_eq!("Toplevel\nIn foo\n", &log_output); + let mut runner = Builder::default() + .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(()) } #[test] pub fn test_dynamic_linking_with_func_without_flag() -> Result<()> { - let js_src = "export function foo() { console.log('In foo'); }; console.log('Toplevel');"; - let res = invoke_fn_on_generated_module(js_src, "foo", None, None, None); + let mut runner = Builder::default() + .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() @@ -48,15 +57,17 @@ pub fn test_dynamic_linking_with_func_without_flag() -> Result<()> { #[test] fn test_errors_in_exported_functions_are_correctly_reported() -> Result<()> { - let js_src = "export function foo() { throw \"Error\" }"; - let wit = " - package local:main + let mut runner = Builder::default() + .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", &[]); - world foo-test { - export foo: func() - } - "; - let res = invoke_fn_on_generated_module(js_src, "foo", Some((wit, "foo-test")), None, None); assert!(res .err() .unwrap() @@ -66,195 +77,100 @@ fn test_errors_in_exported_functions_are_correctly_reported() -> Result<()> { } #[test] +// If you need to change this test, then you've likely made a breaking change. pub fn check_for_new_imports() -> Result<()> { - // If you need to change this test, then you've likely made a breaking change. - let js_src = "console.log(42);"; - let wasm = create_dynamically_linked_wasm_module(js_src, None)?; - let (engine, _linker, _store) = create_wasm_env(WritePipe::new_in_memory(), None, None)?; - let module = Module::from_binary(&engine, &wasm)?; - 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), - } - } - Ok(()) + let runner = Builder::default() + .root(root()) + .bin(BIN) + .input("console.js") + .preload("javy_quickjs_provider_v2".into(), provider_module_path()) + .build()?; + + runner.assert_known_base_imports() } #[test] +// If you need to change this test, then you've likely made a breaking change. pub fn check_for_new_imports_for_exports() -> Result<()> { - // If you need to change this test, then you've likely made a breaking change. - let js_src = "export function foo() { console.log('In foo'); }; console.log('Toplevel');"; - let wit = " - package local:main - - world foo-test { - export foo: func() - } - "; - let wasm = create_dynamically_linked_wasm_module(js_src, Some((wit, "foo-test")))?; - let (engine, _linker, _store) = create_wasm_env(WritePipe::new_in_memory(), None, None)?; - let module = Module::from_binary(&engine, &wasm)?; - 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", "invoke", ExternType::Func(f)) - if f.params().map(|t| t.is_i32()).eq([true, true, true, true]) - && f.results().len() == 0 => {} - ("javy_quickjs_provider_v2", "memory", ExternType::Memory(_)) => (), - _ => panic!("Unknown import {:?}", import), - } - } - Ok(()) + let runner = Builder::default() + .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() } #[test] pub fn test_dynamic_linking_with_arrow_fn() -> Result<()> { - let js_src = "export default () => console.log(42)"; - let wit = " - package local:test - - world exported-arrow { - export default: func() - } - "; - let log_output = invoke_fn_on_generated_module( - js_src, - "default", - Some((wit, "exported-arrow")), - None, - None, - )?; - assert_eq!("42\n", log_output); + let mut runner = Builder::default() + .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(()) } #[test] fn test_producers_section_present() -> Result<()> { - let js_wasm = create_dynamically_linked_wasm_module("console.log(42)", None)?; - common::assert_producers_section_is_correct(&js_wasm)?; - Ok(()) + let runner = Builder::default() + .root(root()) + .bin(BIN) + .input("console.js") + .preload("javy_quickjs_provider_v2".into(), provider_module_path()) + .build()?; + + runner.assert_producers() } #[test] // Temporarily ignore given that Javy.JSON is disabled by default. #[ignore] fn javy_json_identity() -> Result<()> { - let src = r#" - console.log(Javy.JSON.toStdout(Javy.JSON.fromStdin())); - "#; + let mut runner = Builder::default() + .root(root()) + .bin(BIN) + .input("javy-json-id.js") + .preload("javy_quickjs_provider_v2".into(), provider_module_path()) + .build()?; let input = "{\"x\":5}"; let bytes = String::from(input).into_bytes(); - let stdin = Some(ReadPipe::from(bytes)); - let stdout = WritePipe::new_in_memory(); - let out = invoke_fn_on_generated_module(src, "_start", None, stdin, Some(stdout.clone()))?; + let (out, logs, _) = runner.exec(&bytes)?; - assert_eq!(out, "undefined\n"); - assert_eq!( - String::from(input), - String::from_utf8(stdout.try_into_inner().unwrap().into_inner()).unwrap(), - ); + assert_eq!(String::from_utf8(out)?, "undefined\n"); + assert_eq!(String::from(input), String::from_utf8(logs)?); Ok(()) } -fn create_dynamically_linked_wasm_module( - js_src: &str, - wit: Option<(&str, &str)>, -) -> Result> { - let Ok(tempdir) = tempfile::tempdir() else { - panic!("Could not create temporary directory for .wasm test artifacts"); - }; - let js_path = tempdir.path().join(Uuid::new_v4().to_string()); - let wit_path = tempdir.path().join(Uuid::new_v4().to_string()); - let wasm_path = tempdir.path().join(Uuid::new_v4().to_string()); - - let mut js_file = File::create(&js_path)?; - js_file.write_all(js_src.as_bytes())?; - if let Some((wit, _)) = wit { - fs::write(&wit_path, wit)?; - } - let mut args = vec![ - "compile", - js_path.to_str().unwrap(), - "-o", - wasm_path.to_str().unwrap(), - "-d", - ]; - if let Some((_, world)) = wit { - args.push("--wit"); - args.push(wit_path.to_str().unwrap()); - args.push("-n"); - args.push(world); - } - let output = Command::new(env!("CARGO_BIN_EXE_javy")) - .args(args) - .output()?; - assert!(output.status.success()); - - let mut wasm_file = File::open(&wasm_path)?; - let mut contents = vec![]; - wasm_file.read_to_end(&mut contents)?; - Ok(contents) -} - -fn invoke_fn_on_generated_module( - js_src: &str, - func: &str, - wit: Option<(&str, &str)>, - stdin: Option>>>, - stdout: Option>>>, -) -> Result { - let js_wasm = create_dynamically_linked_wasm_module(js_src, wit)?; - - let stderr = WritePipe::new_in_memory(); - let (engine, mut linker, mut store) = create_wasm_env(stderr.clone(), stdin, stdout)?; - let quickjs_provider_module = common::create_quickjs_provider_module(&engine)?; - let js_module = Module::from_binary(&engine, &js_wasm)?; - - let quickjs_provider_instance = - linker.instantiate(store.as_context_mut(), &quickjs_provider_module)?; - linker.instance( - store.as_context_mut(), - "javy_quickjs_provider_v2", - quickjs_provider_instance, - )?; - let js_instance = linker.instantiate(store.as_context_mut(), &js_module)?; - let func = js_instance.get_typed_func::<(), ()>(store.as_context_mut(), func)?; - func.call(store.as_context_mut(), ())?; - - drop(store); // Need to drop store to access contents of stderr. - let log_output = stderr.try_into_inner().unwrap().into_inner(); +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"), + ); - Ok(String::from_utf8(log_output)?) + lib_path } -fn create_wasm_env( - stderr: WritePipe>>, - stdin: Option>>>, - stdout: Option>>>, -) -> Result<(Engine, Linker, Store)> { - let engine = Engine::new(Config::new().wasm_multi_memory(true))?; - let mut linker = Linker::new(&engine); - wasi_common::sync::add_to_linker(&mut linker, |s| s)?; - let wasi = WasiCtxBuilder::new().stderr(Box::new(stderr)).build(); - if let Some(stdout) = stdout { - wasi.set_stdout(Box::new(stdout)); - } - if let Some(stdin) = stdin { - wasi.set_stdin(Box::new(stdin)); - } - let store = Store::new(&engine, wasi); - Ok((engine, linker, store)) +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 ab8e85ac..1c3a42b5 100644 --- a/crates/cli/tests/integration_test.rs +++ b/crates/cli/tests/integration_test.rs @@ -1,5 +1,3 @@ -mod common; - use anyhow::Result; use javy_runner::{Builder, Runner, RunnerError}; use std::path::PathBuf; @@ -228,8 +226,8 @@ fn test_producers_section_present() -> Result<()> { .bin(BIN) .input("readme.js") .build()?; - common::assert_producers_section_is_correct(&runner.wasm).unwrap(); - Ok(()) + + runner.assert_producers() } #[test] @@ -315,12 +313,17 @@ fn run_fn(r: &mut Runner, func: &str, stdin: &[u8]) -> (Vec, String, u64) { (output, logs, fuel_consumed) } -/// Used to detect any significant changes in the fuel consumption when making changes in Javy. +/// Used to detect any significant changes in the fuel consumption when making +/// changes in Javy. /// -/// A threshold is used here so that we can decide how much of a change is acceptable. The threshold value needs to be sufficiently large enough to account for fuel differences between different operating systems. +/// A threshold is used here so that we can decide how much of a change is +/// acceptable. The threshold value needs to be sufficiently large enough to +/// account for fuel differences between different operating systems. /// -/// If the fuel_consumed is less than target_fuel, then great job decreasing the fuel consumption! -/// However, if the fuel_consumed is greater than target_fuel and over the threshold, please consider if the changes are worth the increase in fuel consumption. +/// If the fuel_consumed is less than target_fuel, then great job decreasing the +/// fuel consumption! However, if the fuel_consumed is greater than target_fuel +/// and over the threshold, please consider if the changes are worth the +/// increase in fuel consumption. fn assert_fuel_consumed_within_threshold(target_fuel: u64, fuel_consumed: u64) { let target_fuel = target_fuel as f64; let fuel_consumed = fuel_consumed as f64; diff --git a/crates/runner/Cargo.toml b/crates/runner/Cargo.toml index f49a7105..7be3f9f8 100644 --- a/crates/runner/Cargo.toml +++ b/crates/runner/Cargo.toml @@ -13,3 +13,4 @@ wasi-common = { workspace = true } anyhow = { workspace = true } tempfile = { workspace = true } uuid = { workspace = true } +wasmparser = "0.212.0" diff --git a/crates/runner/src/lib.rs b/crates/runner/src/lib.rs index 74b2cc56..6042862d 100644 --- a/crates/runner/src/lib.rs +++ b/crates/runner/src/lib.rs @@ -5,10 +5,13 @@ use std::io::{self, Cursor, Write}; use std::path::{Path, PathBuf}; use std::process::Command; use std::{cmp, fs}; +use tempfile::TempDir; use wasi_common::pipe::{ReadPipe, WritePipe}; use wasi_common::sync::WasiCtxBuilder; use wasi_common::WasiCtx; -use wasmtime::{Config, Engine, Linker, Module, OptLevel, Store}; +use wasmtime::{ + AsContextMut, Config, Engine, ExternType, Instance, Linker, Module, OptLevel, Store, +}; pub struct Builder { /// The JS source. @@ -22,6 +25,8 @@ pub struct Builder { /// The name of the wit world. world: Option, built: bool, + /// Preload the module at path, using the given instance name. + preload: Option<(String, PathBuf)>, } impl Default for Builder { @@ -33,6 +38,7 @@ impl Default for Builder { bin_path: "javy".into(), root: Default::default(), built: false, + preload: None, } } } @@ -63,6 +69,11 @@ impl Builder { self } + pub fn preload(&mut self, ns: String, wasm: impl Into) -> &mut Self { + self.preload = Some((ns, wasm.into())); + self + } + pub fn build(&mut self) -> Result { if self.built { bail!("Builder already used to build a runner") @@ -81,17 +92,24 @@ impl Builder { world, root, built: _, + preload, } = std::mem::take(self); self.built = true; - Ok(Runner::new(bin_path, root, input, wit, world)) + if let Some(preload) = preload { + Runner::build_dynamic(bin_path, root, input, wit, world, preload) + } else { + Runner::build_static(bin_path, root, input, wit, world) + } } } pub struct Runner { pub wasm: Vec, linker: Linker, + initial_fuel: u64, + preload: Option<(String, Vec)>, } #[derive(Debug)] @@ -114,80 +132,245 @@ impl Display for RunnerError { } struct StoreContext { - wasi_output: WritePipe>>, - wasi: WasiCtx, - log_stream: WritePipe, + wasi: Option, + logs: WritePipe, + output: WritePipe>>, } impl StoreContext { - fn new(input: &[u8], capacity: usize) -> Self { - let wasi_output = WritePipe::new_in_memory(); - let log_stream = WritePipe::new(LogWriter::new(capacity)); + fn new(capacity: usize, input: &[u8]) -> Self { + let output = WritePipe::new_in_memory(); + let logs = WritePipe::new(LogWriter::new(capacity)); let wasi = WasiCtxBuilder::new() - .stdout(Box::new(wasi_output.clone())) .stdin(Box::new(ReadPipe::from(input))) - .stderr(Box::new(log_stream.clone())) + .stdout(Box::new(output.clone())) + .stderr(Box::new(logs.clone())) .build(); + Self { - wasi, - wasi_output, - log_stream, + wasi: Some(wasi), + output, + logs, } } } impl Runner { - fn new( + fn build_static( bin: String, root: PathBuf, source: impl AsRef, wit: Option, world: Option, - ) -> Self { - let wasm_file_name = format!("{}.wasm", uuid::Uuid::new_v4()); - + ) -> Result { // 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 tempdir = tempfile::tempdir()?; + let wasm_file = Self::out_wasm(&tempdir); + let js_file = root.join(source); + let wit_file = wit.map(|p| root.join(p)); + + let args = Self::base_build_args(&js_file, &wasm_file, &wit_file, &world); + + Self::exec_command(bin, root, args)?; + + let wasm = fs::read(&wasm_file)?; + + let engine = Self::setup_engine(); + let linker = Self::setup_linker(&engine)?; + + Ok(Self { + wasm, + linker, + initial_fuel: u64::MAX, + preload: None, + }) + } + + pub fn build_dynamic( + bin: String, + root: PathBuf, + source: impl AsRef, + wit: Option, + world: Option, + preload: (String, PathBuf), + ) -> Result { + let tempdir = tempfile::tempdir()?; + let wasm_file = Self::out_wasm(&tempdir); let js_file = root.join(source); let wit_file = wit.map(|p| root.join(p)); + let mut args = Self::base_build_args(&js_file, &wasm_file, &wit_file, &world); + args.push("-d".to_string()); + + Self::exec_command(bin, root, args)?; + + let wasm = fs::read(&wasm_file)?; + let preload_module = fs::read(&preload.1)?; + + let engine = Self::setup_engine(); + let linker = Self::setup_linker(&engine)?; + + Ok(Self { + wasm, + linker, + initial_fuel: u64::MAX, + preload: Some((preload.0, preload_module)), + }) + } + + pub fn with_dylib(wasm: Vec) -> Result { + let engine = Self::setup_engine(); + Ok(Self { + wasm, + linker: Self::setup_linker(&engine)?, + initial_fuel: u64::MAX, + preload: None, + }) + } + + pub fn assert_known_base_imports(&self) -> Result<()> { + let module = Module::from_binary(self.linker.engine(), &self.wasm)?; + + 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), + } + } + + Ok(()) + } + + pub fn assert_known_named_function_imports(&self) -> Result<()> { + let module = Module::from_binary(self.linker.engine(), &self.wasm)?; + + 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), + } + } + + Ok(()) + } + + pub fn assert_producers(&self) -> Result<()> { + let producers_section = wasmparser::Parser::new(0) + .parse_all(&self.wasm) + .find_map(|payload| { + if let Ok(wasmparser::Payload::CustomSection(c)) = payload { + if let wasmparser::KnownCustom::Producers(r) = c.as_known() { + return Some(r); + } + } + None + }) + .expect("Should have producers custom section"); + let fields = producers_section + .into_iter() + .collect::, _>>()?; + + assert_eq!(2, fields.len()); + + let language_field = &fields[0]; + assert_eq!("language", language_field.name); + assert_eq!(1, language_field.values.count()); + let language_value = language_field.values.clone().into_iter().next().unwrap()?; + assert_eq!("JavaScript", language_value.name); + assert_eq!("ES2020", language_value.version); + + let processed_by_field = &fields[1]; + assert_eq!("processed-by", processed_by_field.name); + assert_eq!(1, processed_by_field.values.count()); + let processed_by_value = processed_by_field + .values + .clone() + .into_iter() + .next() + .unwrap()?; + assert_eq!("Javy", processed_by_value.name); + Ok(()) + } + + fn out_wasm(dir: &TempDir) -> PathBuf { + let name = format!("{}.wasm", uuid::Uuid::new_v4()); + let file = dir.path().join(name); + file + } + + fn base_build_args( + input: &Path, + out: &Path, + wit: &Option, + world: &Option, + ) -> Vec { let mut args = vec![ "compile".to_string(), - js_file.to_str().unwrap().to_string(), + input.to_str().unwrap().to_string(), "-o".to_string(), - wasm_file.to_str().unwrap().to_string(), + out.to_str().unwrap().to_string(), ]; - if let (Some(wit_file), Some(world)) = (wit_file, world) { + if let (Some(wit), Some(world)) = (wit, world) { args.push("--wit".to_string()); - args.push(wit_file.to_str().unwrap().to_string()); + args.push(wit.to_str().unwrap().to_string()); args.push("-n".to_string()); args.push(world.to_string()); } - let output = Command::new(bin) - .current_dir(root) - .args(args) - .output() - .expect("failed to run command"); + args + } + + fn exec_command(bin: String, root: PathBuf, args: Vec) -> Result<()> { + let output = Command::new(bin).current_dir(root).args(args).output()?; - io::stdout().write_all(&output.stdout).unwrap(); - io::stderr().write_all(&output.stderr).unwrap(); + io::stdout().write_all(&output.stdout)?; + io::stderr().write_all(&output.stderr)?; if !output.status.success() { - panic!("terminated with status = {}", output.status); + bail!("terminated with status = {}", output.status); } - let wasm = fs::read(&wasm_file).expect("failed to read wasm module"); + Ok(()) + } + + fn setup_engine() -> Engine { + let mut config = Config::new(); + config.cranelift_opt_level(OptLevel::SpeedAndSize); + config.consume_fuel(true); + Engine::new(&config).expect("failed to create engine") + } + + fn setup_linker(engine: &Engine) -> Result> { + let mut linker = Linker::new(engine); + + wasi_common::sync::add_to_linker(&mut linker, |ctx: &mut StoreContext| { + ctx.wasi.as_mut().unwrap() + })?; - let engine = setup_engine(); - let linker = setup_linker(&engine); + Ok(linker) + } - Self { wasm, linker } + fn setup_store(engine: &Engine, input: &[u8]) -> Result> { + let mut store = Store::new(engine, StoreContext::new(usize::MAX, input)); + store.set_fuel(u64::MAX)?; + Ok(store) } pub fn exec(&mut self, input: &[u8]) -> Result<(Vec, Vec, u64)> { @@ -195,31 +378,148 @@ 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, usize::MAX)); - const INITIAL_FUEL: u64 = u64::MAX; - store.set_fuel(INITIAL_FUEL)?; + let mut store = Self::setup_store(self.linker.engine(), input)?; + let module = Module::from_binary(self.linker.engine(), &self.wasm)?; + + if let Some((name, bytes)) = &self.preload { + let module = Module::from_binary(self.linker.engine(), bytes)?; + let instance = self.linker.instantiate(store.as_context_mut(), &module)?; + self.linker.allow_shadowing(true); + self.linker + .instance(store.as_context_mut(), name, instance)?; + } + + let instance = self.linker.instantiate(store.as_context_mut(), &module)?; + let run = instance.get_typed_func::<(), ()>(store.as_context_mut(), func)?; + + let res = run.call(store.as_context_mut(), ()); + + self.extract_store_data(res, store) + } + pub fn exec_through_dylib( + &mut self, + src: &str, + named: Option<&'static str>, + ) -> Result<(Vec, Vec, u64)> { + let mut store = Self::setup_store(self.linker.engine(), &[])?; let module = Module::from_binary(self.linker.engine(), &self.wasm)?; - let instance = self.linker.instantiate(&mut store, &module)?; - let run = instance.get_typed_func::<(), ()>(&mut store, func)?; + let instance = self.linker.instantiate(store.as_context_mut(), &module)?; + + let res = if let Some(invoke) = named { + let invoke_fn = instance + .get_typed_func::<(u32, u32, u32, u32), ()>(store.as_context_mut(), "invoke")?; + let (bc_ptr, bc_len) = + Self::compile(src.as_bytes(), store.as_context_mut(), &instance)?; + let (ptr, len) = Self::copy_func_name(invoke, &instance, store.as_context_mut())?; + + invoke_fn.call(store.as_context_mut(), (bc_ptr, bc_len, ptr, len)) + } else { + let eval = instance + .get_typed_func::<(u32, u32), ()>(store.as_context_mut(), "eval_bytecode")?; + let (ptr, len) = Self::compile(src.as_bytes(), store.as_context_mut(), &instance)?; + eval.call(store.as_context_mut(), (ptr, len)) + }; + + self.extract_store_data(res, store) + } + + fn copy_func_name( + name: &str, + instance: &Instance, + mut store: impl AsContextMut, + ) -> Result<(u32, u32)> { + let memory = instance + .get_memory(store.as_context_mut(), "memory") + .unwrap(); + let fn_name_bytes = name.as_bytes(); + let fn_name_ptr = Self::allocate_memory( + instance, + store.as_context_mut(), + 1, + fn_name_bytes.len().try_into()?, + )?; + memory.write( + store.as_context_mut(), + fn_name_ptr.try_into()?, + fn_name_bytes, + )?; + + Ok((fn_name_ptr, fn_name_bytes.len().try_into()?)) + } + + fn compile( + source: &[u8], + mut store: impl AsContextMut, + instance: &Instance, + ) -> Result<(u32, u32)> { + let memory = instance + .get_memory(store.as_context_mut(), "memory") + .unwrap(); + let compile_src_func = + instance.get_typed_func::<(u32, u32), u32>(store.as_context_mut(), "compile_src")?; + + let js_src_ptr = Self::allocate_memory( + instance, + store.as_context_mut(), + 1, + source.len().try_into()?, + )?; + memory.write(store.as_context_mut(), js_src_ptr.try_into()?, source)?; + + let ret_ptr = compile_src_func.call( + store.as_context_mut(), + (js_src_ptr, source.len().try_into()?), + )?; + let mut ret_buffer = [0; 8]; + memory.read(store.as_context(), ret_ptr.try_into()?, &mut ret_buffer)?; + let bytecode_ptr = u32::from_le_bytes(ret_buffer[0..4].try_into()?); + let bytecode_len = u32::from_le_bytes(ret_buffer[4..8].try_into()?); + + Ok((bytecode_ptr, bytecode_len)) + } - let res = run.call(&mut store, ()); - let fuel_consumed = INITIAL_FUEL - store.get_fuel()?; + fn allocate_memory( + instance: &Instance, + mut store: impl AsContextMut, + alignment: u32, + new_size: u32, + ) -> Result { + let realloc_func = instance.get_typed_func::<(u32, u32, u32, u32), u32>( + store.as_context_mut(), + "canonical_abi_realloc", + )?; + let orig_ptr = 0; + let orig_size = 0; + realloc_func + .call( + store.as_context_mut(), + (orig_ptr, orig_size, alignment, new_size), + ) + .map_err(Into::into) + } + + fn extract_store_data( + &self, + call_result: Result<()>, + mut store: Store, + ) -> Result<(Vec, Vec, u64)> { + let fuel_consumed = self.initial_fuel - store.as_context_mut().get_fuel()?; let store_context = store.into_data(); drop(store_context.wasi); let logs = store_context - .log_stream + .logs .try_into_inner() .expect("log stream reference still exists") .buffer; let output = store_context - .wasi_output + .output .try_into_inner() .expect("Output stream reference still exists") .into_inner(); - match res { + match call_result { Ok(_) => Ok((output, logs, fuel_consumed)), Err(err) => Err(RunnerError { stdout: output, @@ -231,22 +531,6 @@ impl Runner { } } -fn setup_engine() -> Engine { - let mut config = Config::new(); - config.cranelift_opt_level(OptLevel::SpeedAndSize); - config.consume_fuel(true); - Engine::new(&config).expect("failed to create engine") -} - -fn setup_linker(engine: &Engine) -> Linker { - let mut linker = Linker::new(engine); - - wasi_common::sync::add_to_linker(&mut linker, |ctx: &mut StoreContext| &mut ctx.wasi) - .expect("failed to add wasi context"); - - linker -} - #[derive(Debug)] pub struct LogWriter { pub buffer: Vec,