diff --git a/Cargo.lock b/Cargo.lock index 4c65b7d3..471a5f9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2456,7 +2456,18 @@ checksum = "1eb4e60eb2f8c6e02b1f1e7634ef91738b1104b5bc2fa30458d10cd00917dbbf" dependencies = [ "bumpalo", "rmp", - "shopify_function_wasm_api_core", + "shopify_function_wasm_api_core 0.1.0", +] + +[[package]] +name = "shopify_function_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92a23bc3d343786dd32049e8e2453a058f8e37a4bf953504ebe52189c1b1ebf" +dependencies = [ + "bumpalo", + "rmp", + "shopify_function_wasm_api_core 0.2.0", ] [[package]] @@ -2467,8 +2478,22 @@ checksum = "a57a2e64ef7d28cbe26bf591fd084093327d9d359e38355010720d818cd92ba9" dependencies = [ "rmp-serde", "serde_json", - "shopify_function_provider", - "shopify_function_wasm_api_core", + "shopify_function_provider 1.0.1", + "shopify_function_wasm_api_core 0.1.0", + "thiserror 2.0.14", +] + +[[package]] +name = "shopify_function_wasm_api" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "132521368234c994edd82c1ccec033dd8128ec82d21e3dddca4ceb16d94bb2cc" +dependencies = [ + "rmp-serde", + "seq-macro", + "serde_json", + "shopify_function_provider 2.0.0", + "shopify_function_wasm_api_core 0.2.0", "thiserror 2.0.14", ] @@ -2481,6 +2506,15 @@ dependencies = [ "strum", ] +[[package]] +name = "shopify_function_wasm_api_core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7367c758d2ec5644f78f662e3748564dd71c50462ad7eec679f0af139984948" +dependencies = [ + "strum", +] + [[package]] name = "slab" version = "0.4.11" @@ -2545,9 +2579,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] @@ -3129,7 +3163,15 @@ name = "wasm_api_v1" version = "0.1.0" dependencies = [ "anyhow", - "shopify_function_wasm_api", + "shopify_function_wasm_api 0.1.0", +] + +[[package]] +name = "wasm_api_v2" +version = "0.1.0" +dependencies = [ + "anyhow", + "shopify_function_wasm_api 0.3.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1cd7213b..46daff68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "tests/fixtures/messagepack-valid", "tests/fixtures/messagepack-invalid", "tests/fixtures/wasm_api_v1", + "tests/fixtures/wasm_api_v2", ] [package] diff --git a/providers/shopify_function_v2.wasm b/providers/shopify_function_v2.wasm new file mode 100644 index 00000000..d1ace903 Binary files /dev/null and b/providers/shopify_function_v2.wasm differ diff --git a/providers/shopify_functions_javy_v3.wasm b/providers/shopify_functions_javy_v3.wasm new file mode 100644 index 00000000..a25108cf Binary files /dev/null and b/providers/shopify_functions_javy_v3.wasm differ diff --git a/src/engine.rs b/src/engine.rs index 064fe9db..af1d6f11 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,13 +1,13 @@ use anyhow::{anyhow, Result}; -use rust_embed::RustEmbed; +use std::path::PathBuf; use std::string::String; -use std::{collections::HashSet, path::PathBuf}; use wasmtime::{AsContextMut, Config, Engine, Linker, Module, ResourceLimiter, Store}; -use wasmtime_wasi::pipe::{MemoryInputPipe, MemoryOutputPipe}; use wasmtime_wasi::preview1::WasiP1Ctx; -use wasmtime_wasi::{I32Exit, WasiCtxBuilder}; +use wasmtime_wasi::I32Exit; use crate::function_run_result::FunctionRunResult; +use crate::io::{IOHandler, OutputAndLogs}; +use crate::validated_module::ValidatedModule; use crate::{BytesContainer, BytesContainerType}; #[derive(Clone)] @@ -16,44 +16,15 @@ pub struct ProfileOpts { pub out: PathBuf, } -#[derive(RustEmbed)] -#[folder = "providers/"] -struct StandardProviders; - pub fn uses_msgpack_provider(module: &Module) -> bool { module.imports().map(|i| i.module()).any(|module| { - module.starts_with("shopify_function_v") || module == "shopify_functions_javy_v2" + module.starts_with("shopify_function_v") + || module + .strip_prefix("shopify_functions_javy_v") + .is_some_and(|v| v.parse::().is_ok_and(|v| v >= 2)) }) } -fn import_modules( - module: &Module, - engine: &Engine, - linker: &mut Linker, - mut store: &mut Store, -) { - let imported_modules: HashSet = - module.imports().map(|i| i.module().to_string()).collect(); - - imported_modules.iter().for_each(|module_name| { - let provider_path = format!("{module_name}.wasm"); - let imported_module_bytes = StandardProviders::get(&provider_path); - - if let Some(bytes) = imported_module_bytes { - let imported_module = Module::from_binary(engine, &bytes.data) - .unwrap_or_else(|_| panic!("Failed to load module {module_name}")); - - let imported_module_instance = linker - .instantiate(&mut store, &imported_module) - .expect("Failed to instantiate imported instance"); - - linker - .instance(&mut store, module_name, imported_module_instance) - .expect("Failed to import module"); - } - }); -} - pub struct FunctionRunParams<'a> { pub function_path: PathBuf, pub input: BytesContainer, @@ -68,12 +39,12 @@ const STARTING_FUEL: u64 = u64::MAX; const MAXIMUM_MEMORIES: usize = 2; // 1 for the module, 1 for Javy's provider struct FunctionContext { - wasi: WasiP1Ctx, + wasi: Option, limiter: MemoryLimiter, } impl FunctionContext { - fn new(wasi: WasiP1Ctx) -> Self { + fn new(wasi: Option) -> Self { Self { wasi, limiter: Default::default(), @@ -128,33 +99,29 @@ pub fn run(params: FunctionRunParams) -> Result { module, } = params; - let input_stream = MemoryInputPipe::new(input.raw.clone()); - let output_stream = MemoryOutputPipe::new(usize::MAX); - let error_stream = MemoryOutputPipe::new(usize::MAX); + let mut io_handler = IOHandler::new(ValidatedModule::new(module)?, input.clone()); let mut error_logs: String = String::new(); let mut linker = Linker::new(&engine); - wasmtime_wasi::preview1::add_to_linker_sync(&mut linker, |ctx: &mut FunctionContext| { - &mut ctx.wasi - })?; - deterministic_wasi_ctx::replace_scheduling_functions(&mut linker)?; - let mut wasi_builder = WasiCtxBuilder::new(); - wasi_builder.stdin(input_stream); - wasi_builder.stdout(output_stream.clone()); - wasi_builder.stderr(error_stream.clone()); - deterministic_wasi_ctx::add_determinism_to_wasi_ctx_builder(&mut wasi_builder); - let wasi = wasi_builder.build_p1(); + let wasi = io_handler.wasi(); + if wasi.is_some() { + wasmtime_wasi::preview1::add_to_linker_sync(&mut linker, |ctx: &mut FunctionContext| { + ctx.wasi.as_mut().expect("Should have WASI context") + })?; + deterministic_wasi_ctx::replace_scheduling_functions(&mut linker)?; + } + let function_context = FunctionContext::new(wasi); let mut store = Store::new(&engine, function_context); store.limiter(|s| &mut s.limiter); + + io_handler.initialize(&engine, &mut linker, &mut store)?; + store.set_fuel(STARTING_FUEL)?; store.set_epoch_deadline(1); - import_modules(&module, &engine, &mut linker, &mut store); - - linker.module(&mut store, "Function", &module)?; - let instance = linker.instantiate(&mut store, &module)?; + let instance = linker.instantiate(&mut store, io_handler.module())?; let func = instance.get_typed_func::<(), ()>(store.as_context_mut(), export)?; @@ -163,7 +130,6 @@ pub fn run(params: FunctionRunParams) -> Result { .frequency(profile_opts.interval) .weight_unit(wasmprof::WeightUnit::Fuel) .profile(|store| func.call(store.as_context_mut(), ())); - ( result, Some(profile_data.into_collapsed_stacks().to_string()), @@ -191,18 +157,14 @@ pub fn run(params: FunctionRunParams) -> Result { } } - drop(store); - - let mut logs = error_stream - .try_into_inner() - .expect("Log stream reference still exists"); + let OutputAndLogs { + output: raw_output, + mut logs, + } = io_handler.finalize(store)?; logs.extend_from_slice(error_logs.as_bytes()); let output_codec = input.codec; - let raw_output = output_stream - .try_into_inner() - .expect("Output stream reference still exists"); let output = BytesContainer::new( BytesContainerType::Output, output_codec, diff --git a/src/function_run_result.rs b/src/function_run_result.rs index 52d2cd2a..948551f0 100644 --- a/src/function_run_result.rs +++ b/src/function_run_result.rs @@ -3,7 +3,7 @@ use colored::Colorize; use serde::{Deserialize, Serialize}; use std::fmt; -const FUNCTION_LOG_LIMIT: usize = 1_000; +pub(crate) const FUNCTION_LOG_LIMIT: usize = 1_000; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct InvalidOutput { diff --git a/src/io.rs b/src/io.rs new file mode 100644 index 00000000..f18968ea --- /dev/null +++ b/src/io.rs @@ -0,0 +1,184 @@ +use anyhow::{anyhow, Result}; +use wasmtime::{AsContext, AsContextMut, Engine, Instance, Linker, Module, Store}; +use wasmtime_wasi::{ + pipe::{MemoryInputPipe, MemoryOutputPipe}, + preview1::WasiP1Ctx, + WasiCtxBuilder, +}; + +use crate::{ + function_run_result::FUNCTION_LOG_LIMIT, validated_module::ValidatedModule, BytesContainer, +}; + +pub(crate) struct OutputAndLogs { + pub output: Vec, + pub logs: Vec, +} + +struct WasiIO { + output: MemoryOutputPipe, + logs: MemoryOutputPipe, +} + +enum IOStrategy { + Wasi(WasiIO), + Memory(Option), +} + +pub(crate) struct IOHandler { + strategy: IOStrategy, + module: ValidatedModule, + input: BytesContainer, +} + +impl IOHandler { + pub(crate) fn new(module: ValidatedModule, input: BytesContainer) -> Self { + Self { + strategy: if module.uses_mem_io() { + IOStrategy::Memory(None) + } else { + IOStrategy::Wasi(WasiIO { + output: MemoryOutputPipe::new(usize::MAX), + logs: MemoryOutputPipe::new(usize::MAX), + }) + }, + module, + input, + } + } + + pub(crate) fn module(&self) -> &Module { + self.module.inner() + } + + pub(crate) fn wasi(&self) -> Option { + match &self.strategy { + IOStrategy::Wasi(WasiIO { output, logs }) => { + let input_stream = MemoryInputPipe::new(self.input.raw.clone()); + let mut wasi_builder = WasiCtxBuilder::new(); + wasi_builder.stdin(input_stream); + wasi_builder.stdout(output.clone()); + wasi_builder.stderr(logs.clone()); + deterministic_wasi_ctx::add_determinism_to_wasi_ctx_builder(&mut wasi_builder); + Some(wasi_builder.build_p1()) + } + IOStrategy::Memory(_instance) => None, + } + } + + pub(crate) fn initialize( + &mut self, + engine: &Engine, + linker: &mut Linker, + store: &mut Store, + ) -> Result<()> { + store.set_epoch_deadline(1); // Need to make sure we don't timeout during initialization. + let old_fuel = store.get_fuel()?; + store.set_fuel(u64::MAX)?; // Make sure we have fuel for initialization. + let mem_io_instance = instantiate_imports(&self.module, engine, linker, store); + if let IOStrategy::Memory(ref mut instance) = self.strategy { + *instance = mem_io_instance; + } + + if let Some(instance) = mem_io_instance { + let input_offset = instance + .get_typed_func::(store.as_context_mut(), "initialize")? + .call(store.as_context_mut(), self.input.raw.len() as _)?; + instance + .get_memory(store.as_context_mut(), "memory") + .ok_or_else(|| anyhow!("Missing memory export named memory"))? + .write(store.as_context_mut(), input_offset as _, &self.input.raw)?; + } + store.set_fuel(old_fuel)?; + Ok(()) + } + + pub(crate) fn finalize(self, mut store: Store) -> Result { + match self.strategy { + IOStrategy::Memory(instance) => { + let instance = instance.expect("Should have been defined in initialize"); + store.set_epoch_deadline(1); // Make sure we don't timeout while finalizing. + let old_fuel = store.get_fuel()?; + store.set_fuel(u64::MAX)?; // Make sure we don't run out of fuel finalizing. + let results_offset = instance + .get_typed_func::<(), i32>(store.as_context_mut(), "finalize")? + .call(store.as_context_mut(), ())? + as usize; + store.set_fuel(old_fuel)?; + + let memory = instance + .get_memory(store.as_context_mut(), "memory") + .ok_or_else(|| anyhow!("Missing memory export named memory"))?; + + let mut buf = [0; 24]; + memory.read(store.as_context(), results_offset, &mut buf)?; + + let output_offset = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize; + let output_len = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize; + let log_offset1 = u32::from_le_bytes(buf[8..12].try_into().unwrap()) as usize; + let log_len1 = u32::from_le_bytes(buf[12..16].try_into().unwrap()) as usize; + let log_offset2 = u32::from_le_bytes(buf[16..20].try_into().unwrap()) as usize; + let log_len2 = u32::from_le_bytes(buf[20..24].try_into().unwrap()) as usize; + + let mut output = vec![0; output_len]; + memory.read(store.as_context(), output_offset, &mut output)?; + + let mut logs = vec![0; log_len1]; + memory.read(store.as_context(), log_offset1, &mut logs)?; + + let mut logs2 = vec![0; log_len2]; + memory.read(store.as_context(), log_offset2, &mut logs2)?; + + logs.append(&mut logs2); + + if logs.len() > FUNCTION_LOG_LIMIT { + logs.splice(0..1, b"[TRUNCATED]...".iter().copied()); + } + + Ok(OutputAndLogs { output, logs }) + } + IOStrategy::Wasi(WasiIO { output, logs }) => { + // Need to drop store to have only one reference to output and error streams. + drop(store); + + let output = output + .try_into_inner() + .expect("Should have only one reference to output stream at this point") + .to_vec(); + let logs = logs + .try_into_inner() + .expect("Should have only one reference to error stream at this point") + .to_vec(); + Ok(OutputAndLogs { output, logs }) + } + } + } +} + +fn instantiate_imports( + module: &ValidatedModule, + engine: &Engine, + linker: &mut Linker, + mut store: &mut Store, +) -> Option { + let mut mem_io_instance = None; + + if let Some(std_import) = module.std_import() { + let imported_module = Module::from_binary(engine, &std_import.bytes) + .unwrap_or_else(|_| panic!("Failed to load module {}", std_import.name)); + + let imported_module_instance = linker + .instantiate(&mut store, &imported_module) + .expect("Failed to instantiate imported instance"); + + if std_import.is_mem_io_provider() { + mem_io_instance = Some(imported_module_instance); + } + + linker + .instance(&mut store, &std_import.name, imported_module_instance) + .expect("Failed to import module"); + } + + mem_io_instance +} diff --git a/src/lib.rs b/src/lib.rs index ecde0ba7..19e50075 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,9 @@ pub mod bluejay_schema_analyzer; pub mod container; pub mod engine; pub mod function_run_result; +mod io; pub mod scale_limits_analyzer; +mod validated_module; use clap::ValueEnum; pub use container::*; diff --git a/src/validated_module.rs b/src/validated_module.rs new file mode 100644 index 00000000..2d4b40e9 --- /dev/null +++ b/src/validated_module.rs @@ -0,0 +1,150 @@ +use std::borrow::Cow; + +use anyhow::{bail, Result}; +use rust_embed::RustEmbed; +use wasmtime::Module; + +#[derive(RustEmbed)] +#[folder = "providers/"] +struct StandardProviders; + +#[derive(Debug)] +pub(crate) struct Provider { + pub(crate) bytes: Cow<'static, [u8]>, + pub(crate) name: String, +} + +impl Provider { + pub(crate) fn is_mem_io_provider(&self) -> bool { + let javy_plugin_version = self + .name + .strip_prefix("shopify_functions_javy_v") + .map(|s| s.parse::()) + .and_then(|result| result.ok()); + if javy_plugin_version.is_some_and(|version| version >= 3) { + return true; + } + + let functions_provider_version = self + .name + .strip_prefix("shopify_function_v") + .map(|s| s.parse::()) + .and_then(|result| result.ok()); + if functions_provider_version.is_some_and(|version| version >= 2) { + return true; + } + + false + } +} + +#[derive(Debug)] +pub(crate) struct ValidatedModule { + module: Module, + std_import: Option, +} + +impl ValidatedModule { + pub(crate) fn new(module: Module) -> Result { + // Need to track with deterministic order so don't use a hash + let mut imports = vec![]; + for import in module.imports().map(|i| i.module().to_string()) { + if !imports.contains(&import) { + imports.push(import); + } + } + + let uses_wasi = imports.contains(&"wasi_snapshot_preview1".to_string()); + + let std_import = imports.iter().find_map(|import| { + StandardProviders::get(&format!("{import}.wasm")).map(|file| Provider { + bytes: file.data, + name: import.into(), + }) + }); + + // If there are multiple standard imports or more than zero unknown imports, + // the module will fail to instantiate because we only link the one + // standard provider so the other imports will be unsatisfied. + + if let Some(import) = &std_import { + if import.is_mem_io_provider() && uses_wasi { + bail!("Invalid Function, cannot use `{}` and import WASI. If using Rust, change the build target to `wasm32-unknown-unknown`.", import.name); + } + } + + Ok(ValidatedModule { module, std_import }) + } + + pub(crate) fn inner(&self) -> &Module { + &self.module + } + + pub(crate) fn std_import(&self) -> Option<&Provider> { + self.std_import.as_ref() + } + + pub(crate) fn uses_mem_io(&self) -> bool { + self.std_import + .as_ref() + .is_some_and(|i| i.is_mem_io_provider()) + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use wasmtime::{Engine, Module}; + + use crate::validated_module::ValidatedModule; + + #[test] + fn test_module_with_just_wasi() -> Result<()> { + let wat = r#" + (module + (import "wasi_snapshot_preview1" "fd_read" (func)) + ) + "#; + let module = Module::new(&Engine::default(), &wat)?; + ValidatedModule::new(module)?; + Ok(()) + } + + #[test] + fn test_module_with_wasi_and_old_provider() -> Result<()> { + let wat = r#" + (module + (import "wasi_snapshot_preview1" "fd_read" (func)) + (import "shopify_function_v1" "shopify_function_input_get" (func)) + ) + "#; + let module = Module::new(&Engine::default(), &wat)?; + ValidatedModule::new(module)?; + Ok(()) + } + + #[test] + fn test_module_without_wasi_and_with_new_provider() -> Result<()> { + let wat = r#" + (module + (import "shopify_function_v2" "shopify_function_input_get" (func)) + ) + "#; + let module = Module::new(&Engine::default(), &wat)?; + ValidatedModule::new(module)?; + Ok(()) + } + + #[test] + fn test_module_with_wasi_and_new_provider() -> Result<()> { + let wat = r#" + (module + (import "wasi_snapshot_preview1" "fd_read" (func)) + (import "shopify_function_v2" "shopify_function_input_get" (func)) + ) + "#; + let module = Module::new(&Engine::default(), &wat)?; + ValidatedModule::new(module).unwrap_err(); + Ok(()) + } +} diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index f9dd2915..7e04a1c7 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -9,7 +9,22 @@ pub fn process_with_v1_trampoline, Q: AsRef>( wasm_path: P, trampolined_path: Q, ) -> Result<()> { - let trampoline_path = TRAMPOLINE_1_0_PATH + process_with_trampoline(&TRAMPOLINE_1_0_PATH, wasm_path, trampolined_path) +} + +pub fn process_with_v2_trampoline, Q: AsRef>( + wasm_path: P, + trampolined_path: Q, +) -> Result<()> { + process_with_trampoline(&TRAMPOLINE_2_0_PATH, wasm_path, trampolined_path) +} + +fn process_with_trampoline, Q: AsRef>( + trampoline_path: &LazyLock>, + wasm_path: P, + trampolined_path: Q, +) -> Result<()> { + let trampoline_path = trampoline_path .as_ref() .map_err(|e| anyhow!("Failed to download trampoline: {e}"))?; let status = Command::new(trampoline_path) @@ -24,15 +39,19 @@ pub fn process_with_v1_trampoline, Q: AsRef>( Ok(()) } -static TRAMPOLINE_1_0_PATH: LazyLock> = LazyLock::new(|| { +static TRAMPOLINE_1_0_PATH: LazyLock> = LazyLock::new(|| trampoline_path("1.0.2")); + +static TRAMPOLINE_2_0_PATH: LazyLock> = LazyLock::new(|| trampoline_path("2.0.0")); + +fn trampoline_path(version: &str) -> Result { let binaries_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tmp"); - let path = binaries_path.join("trampoline-1.0.2"); + let path = binaries_path.join(format!("trampoline-{version}")); if !path.exists() { std::fs::create_dir_all(binaries_path)?; - download_trampoline(&path, "1.0.2")?; + download_trampoline(&path, version)?; } Ok(path) -}); +} fn download_trampoline(destination: &Path, version: &str) -> Result<()> { let target_os = if cfg!(target_os = "macos") { diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index 6b177745..91c4f45c 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -13,6 +13,8 @@ Example Functions used as test fixtures. ``` cargo build --target wasm32-wasip1 --profile=wasm -p exit_code -p exports -p log_truncation_function -p noop -p wasm_api_v1 && find target/wasm32-wasip1/wasm/{exit_code.wasm,exports.wasm,log_truncation_function.wasm,noop.wasm,wasm_api_v1.wasm} | xargs -I {} sh -c 'name=$(basename {}); wasm-opt {} -Oz --enable-bulk-memory --strip-debug -o "tests/fixtures/build/$name"' +cargo build --target wasm32-unknown-unknown --profile=wasm -p wasm_api_v2 && + find target/wasm32-unknown-unknown/wasm/wasm_api_v2.wasm | xargs -I {} sh -c 'name=$(basename {}); wasm-opt {} -Oz --enable-bulk-memory --strip-debug -o "tests/fixtures/build/$name"' ``` **JS examples:** @@ -36,6 +38,11 @@ js_function_that_throws.wasm: javy build -C dynamic -C plugin=providers/javy_quickjs_provider_v3.wasm -o tests/fixtures/build/js_function_that_throws.wasm tests/fixtures/js_function_that_throws/src/functions.js ``` +js_function_javy_plugin_v3.wasm: +``` +javy build -C dynamic -C plugin=providers/shopify_functions_javy_v3.wasm -o tests/fixtures/build/js_function_javy_plugin_v3.wasm tests/fixtures/js_function_javy_plugin_v3/function.js +``` + **`*.wat` examples:** ``` find tests/fixtures -maxdepth 1 -type f -name "*.wat" \ diff --git a/tests/fixtures/build/invalid_import_combination.wasm b/tests/fixtures/build/invalid_import_combination.wasm new file mode 100644 index 00000000..b3cc8ad0 Binary files /dev/null and b/tests/fixtures/build/invalid_import_combination.wasm differ diff --git a/tests/fixtures/build/js_function_javy_plugin_v3.wasm b/tests/fixtures/build/js_function_javy_plugin_v3.wasm new file mode 100644 index 00000000..3d420725 Binary files /dev/null and b/tests/fixtures/build/js_function_javy_plugin_v3.wasm differ diff --git a/tests/fixtures/build/wasm_api_v2.wasm b/tests/fixtures/build/wasm_api_v2.wasm new file mode 100644 index 00000000..5bebca17 Binary files /dev/null and b/tests/fixtures/build/wasm_api_v2.wasm differ diff --git a/tests/fixtures/invalid_import_combination.wat b/tests/fixtures/invalid_import_combination.wat new file mode 100644 index 00000000..bcbb5700 --- /dev/null +++ b/tests/fixtures/invalid_import_combination.wat @@ -0,0 +1,6 @@ +(module + (import "shopify_function_v2" "_shopify_function_input_get" (func (result i64))) + (import "wasi_snapshot_preview1" "fd_write" (func (param i32 i32 i32 i32) (result i32))) + (func $start) + (export "_start" (func $start)) +) \ No newline at end of file diff --git a/tests/fixtures/js_function_javy_plugin_v3/function.js b/tests/fixtures/js_function_javy_plugin_v3/function.js new file mode 100644 index 00000000..3cf088f6 --- /dev/null +++ b/tests/fixtures/js_function_javy_plugin_v3/function.js @@ -0,0 +1,4 @@ +const inputObj = ShopifyFunction.readInput(); +console.error("this is an error message"); +const outputObj = { hello: inputObj.hello + " output" }; +ShopifyFunction.writeOutput(outputObj); diff --git a/tests/fixtures/wasm_api_v2/Cargo.toml b/tests/fixtures/wasm_api_v2/Cargo.toml new file mode 100644 index 00000000..a52903b7 --- /dev/null +++ b/tests/fixtures/wasm_api_v2/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "wasm_api_v2" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1.0" +shopify_function_wasm_api = "0.3.0" diff --git a/tests/fixtures/wasm_api_v2/src/lib.rs b/tests/fixtures/wasm_api_v2/src/lib.rs new file mode 100644 index 00000000..88ba28c5 --- /dev/null +++ b/tests/fixtures/wasm_api_v2/src/lib.rs @@ -0,0 +1,27 @@ +use anyhow::{anyhow, Result}; + +#[export_name = "_start"] +fn start() { + main().unwrap() +} + +fn main() -> Result<()> { + let mut ctx = shopify_function_wasm_api::Context::new(); + let input = ctx.input_get()?; + let str = input + .get_obj_prop("hello") + .as_string() + .ok_or_else(|| anyhow!("Should be string"))?; + ctx.write_object( + |ctx| { + ctx.write_utf8_str("bye")?; + ctx.write_utf8_str(&str)?; + Ok(()) + }, + 1, + )?; + // Test log wrap-around by writing 1011 entries (capacity is 1001). + ctx.log(&"a".repeat(1001)); + ctx.log(&"b".repeat(10)); + Ok(()) +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index a12e95bc..12e3c9e2 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -403,4 +403,93 @@ mod tests { Ok(()) } + + #[test] + fn run_wasm_api_v2_function() -> Result<()> { + let trampolined_module = assert_fs::NamedTempFile::new("wasm_api_v2.trampolined.wasm")?; + test_utils::process_with_v2_trampoline( + "tests/fixtures/build/wasm_api_v2.wasm", + &trampolined_module, + )?; + + let mut cmd = Command::cargo_bin("function-runner")?; + let input_file = temp_input(json!({"hello": "world"}))?; + + cmd.arg("--function") + .arg(trampolined_module.as_os_str()) + .arg("--json") + .arg("--input") + .arg(input_file.as_os_str()) + .stdout(Stdio::piped()) + .spawn()? + .wait_with_output()?; + + cmd.assert().success(); + cmd.assert().stdout(contains("world")); + cmd.assert().stdout(contains(format!( + "[TRUNCATED]...{}{}", + "a".repeat(990), + "b".repeat(10) + ))); + + Ok(()) + } + + #[test] + fn run_javy_plugin_v3() -> Result<()> { + let mut cmd = Command::cargo_bin("function-runner")?; + let input = temp_input(json!({"hello": "world"}))?; + + cmd.args([ + "--function", + "tests/fixtures/build/js_function_javy_plugin_v3.wasm", + ]) + .arg("--json") + .arg("--input") + .arg(input.as_os_str()) + .stdout(Stdio::piped()) + .spawn() + .expect("Failed to spawn child process") + .wait_with_output() + .expect("Failed waiting for output"); + + // Command should succeed + cmd.assert().success(); + + // Logs should be returned + cmd.assert().stdout(contains("this is an error message")); + + // Input and module output should be returned + cmd.assert().stdout(contains("\"hello\": \"world output\"")); + Ok(()) + } + + #[test] + fn invalid_import_combination() -> Result<()> { + let mut cmd = Command::cargo_bin("function-runner")?; + let input = temp_input(json!({"hello": "world"}))?; + + cmd.args([ + "--function", + "tests/fixtures/build/invalid_import_combination.wasm", + ]) + .arg("--json") + .arg("--input") + .arg(input.as_os_str()) + .stdout(Stdio::piped()) + .spawn() + .expect("Failed to spawn child process") + .wait_with_output() + .expect("Failed waiting for output"); + + // Command should fail + cmd.assert().failure(); + + // Error should mention not mixing WASI and the import + cmd.assert().stderr(contains( + "Error: Invalid Function, cannot use `shopify_function_v2` and import WASI. If using Rust, change the build target to `wasm32-unknown-unknown`.", + )); + + Ok(()) + } }