diff --git a/Cargo.lock b/Cargo.lock index 6a9b6855..788826ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,7 +319,7 @@ checksum = "4ac68674a6042af2bcee1adad9f6abd432642cf03444ce3a5b36c3f39f23baf8" dependencies = [ "cap-primitives", "cap-std", - "rustix", + "rustix 0.38.42", "smallvec", ] @@ -335,7 +335,7 @@ dependencies = [ "io-lifetimes", "ipnet", "maybe-owned", - "rustix", + "rustix 0.38.42", "windows-sys 0.59.0", "winx", ] @@ -359,7 +359,7 @@ dependencies = [ "cap-primitives", "io-extras", "io-lifetimes", - "rustix", + "rustix 0.38.42", ] [[package]] @@ -372,7 +372,7 @@ dependencies = [ "cap-primitives", "iana-time-zone", "once_cell", - "rustix", + "rustix 0.38.42", "winx", ] @@ -751,11 +751,15 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "echo" +version = "0.1.0" + [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "embedded-io" @@ -792,18 +796,18 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -831,12 +835,12 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fd-lock" -version = "4.0.2" +version = "4.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix", + "rustix 1.0.5", "windows-sys 0.52.0", ] @@ -857,9 +861,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" @@ -872,13 +876,13 @@ dependencies = [ [[package]] name = "fs-set-times" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2e6123af26f0f2c51cc66869137080199406754903cc926a7690401ce09cb4" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" dependencies = [ "io-lifetimes", - "rustix", - "windows-sys 0.59.0", + "rustix 1.0.5", + "windows-sys 0.52.0", ] [[package]] @@ -1412,6 +1416,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "litemap" version = "0.7.4" @@ -1492,7 +1502,21 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" dependencies = [ - "rustix", + "rustix 0.38.42", +] + +[[package]] +name = "messagepack-invalid" +version = "0.1.0" +dependencies = [ + "rmpv", +] + +[[package]] +name = "messagepack-valid" +version = "0.1.0" +dependencies = [ + "rmpv", ] [[package]] @@ -1847,6 +1871,16 @@ dependencies = [ "serde", ] +[[package]] +name = "rmpv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58450723cd9ee93273ce44a20b6ec4efe17f8ed2e3631474387bfdecf18bb2a9" +dependencies = [ + "num-traits", + "rmp", +] + [[package]] name = "rust-embed" version = "8.6.0" @@ -1903,11 +1937,24 @@ dependencies = [ "errno", "itoa", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.14", "once_cell", "windows-sys 0.59.0", ] +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.52.0", +] + [[package]] name = "rustversion" version = "1.0.18" @@ -2112,7 +2159,7 @@ dependencies = [ "cap-std", "fd-lock", "io-lifetimes", - "rustix", + "rustix 0.38.42", "windows-sys 0.59.0", "winx", ] @@ -2138,7 +2185,7 @@ dependencies = [ "cfg-if", "fastrand", "once_cell", - "rustix", + "rustix 0.38.42", "windows-sys 0.59.0", ] @@ -2380,7 +2427,7 @@ dependencies = [ "io-extras", "io-lifetimes", "log", - "rustix", + "rustix 0.38.42", "system-interface", "thiserror", "tracing", @@ -2455,9 +2502,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.221.2" +version = "0.221.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9845c470a2e10b61dd42c385839cdd6496363ed63b5c9e420b5488b77bd22083" +checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" dependencies = [ "bitflags 2.6.0", "hashbrown 0.15.2", @@ -2522,7 +2569,7 @@ dependencies = [ "psm", "pulley-interpreter 28.0.1", "rayon", - "rustix", + "rustix 0.38.42", "semver", "serde", "serde_derive", @@ -2570,7 +2617,7 @@ dependencies = [ "postcard", "psm", "pulley-interpreter 29.0.1", - "rustix", + "rustix 0.38.42", "serde", "serde_derive", "smallvec", @@ -2614,7 +2661,7 @@ dependencies = [ "directories-next", "log", "postcard", - "rustix", + "rustix 0.38.42", "serde", "serde_derive", "sha2", @@ -2727,7 +2774,7 @@ dependencies = [ "anyhow", "cc", "cfg-if", - "rustix", + "rustix 0.38.42", "wasmtime-asm-macros 28.0.1", "wasmtime-versioned-export-macros 28.0.1", "windows-sys 0.59.0", @@ -2740,7 +2787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cec0a8e5620ae71bfcaaec78e3076be5b6ebf869f4e6191925d73242224a915" dependencies = [ "object", - "rustix", + "rustix 0.38.42", "wasmtime-versioned-export-macros 28.0.1", ] @@ -2818,7 +2865,7 @@ dependencies = [ "futures", "io-extras", "io-lifetimes", - "rustix", + "rustix 0.38.42", "system-interface", "thiserror", "tokio", @@ -3133,9 +3180,9 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.221.2" +version = "0.221.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe1538eea6ea5ddbe5defd0dc82539ad7ba751e1631e9185d24a931f0a5adc8" +checksum = "896112579ed56b4a538b07a3d16e562d101ff6265c46b515ce0c701eef16b2ac" dependencies = [ "anyhow", "id-arena", diff --git a/Cargo.toml b/Cargo.toml index 498858c3..6d14ce6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,9 @@ members = [ "tests/fixtures/log_truncation_function", "tests/fixtures/exports", "tests/fixtures/noop", + "tests/fixtures/messagepack-valid", + "tests/fixtures/messagepack-invalid", + "tests/fixtures/echo", ] [package] diff --git a/src/container.rs b/src/container.rs new file mode 100644 index 00000000..3f79e6d0 --- /dev/null +++ b/src/container.rs @@ -0,0 +1,137 @@ +use crate::Codec; +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default)] +pub enum BytesContainerType { + /// Input bytes. + #[default] + Input, + /// Output bytes. + Output, +} + +/// A container of bytes to hold either the input or output bytes. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BytesContainer { + /// The raw bytes. + #[serde(skip)] + pub raw: Vec, + /// Bytes encoding. + #[serde(skip)] + pub codec: Codec, + /// The JSON represantation of the bytes. + #[serde(skip)] + pub json_value: Option, + /// The human readable representation of the bytes. + pub humanized: String, + /// Context for encoding errors. + #[serde(skip)] + pub encoding_error: Option, +} + +impl Default for BytesContainer { + fn default() -> Self { + Self { + codec: Codec::Raw, + humanized: "".into(), + json_value: None, + raw: Default::default(), + encoding_error: None, + } + } +} + +impl BytesContainer { + pub fn new(ty: BytesContainerType, codec: Codec, raw: Vec) -> Result { + match codec { + Codec::Raw => { + let humanized = raw + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(" "); + + Ok(Self { + raw, + codec, + humanized, + ..Default::default() + }) + } + Codec::Json => match ty { + BytesContainerType::Input => { + let json = serde_json::from_slice::(&raw) + .map_err(|e| anyhow!("Invalid input JSON: {}", e))?; + let minified_buffer = serde_json::to_vec(&json) + .map_err(|e| anyhow!("Couldn't serialize JSON: {}", e))?; + + Ok(Self { + codec, + raw: minified_buffer, + json_value: Some(json.clone()), + humanized: serde_json::to_string_pretty(&json)?, + encoding_error: None, + }) + } + BytesContainerType::Output => { + let mut this = Self { + codec, + ..Default::default() + }; + + match serde_json::from_slice::(&raw) { + Ok(json) => { + this.json_value = Some(json.clone()); + this.humanized = serde_json::to_string_pretty(&json)?; + this.raw = serde_json::to_vec(&json)?; + } + Err(e) => { + this.humanized = String::from_utf8_lossy(&raw).into(); + this.encoding_error = Some(e.to_string()); + } + }; + + Ok(this) + } + }, + Codec::Messagepack => match ty { + BytesContainerType::Input => { + let json: serde_json::Value = serde_json::from_slice(&raw) + .map_err(|e| anyhow!("Invalid input JSON: {}", e))?; + let bytes = rmp_serde::to_vec(&json) + .map_err(|e| anyhow!("Couldn't convert JSON to MessagePack: {}", e))?; + + Ok(Self { + raw: bytes, + codec, + json_value: Some(json.clone()), + humanized: serde_json::to_string_pretty(&json)?, + encoding_error: None, + }) + } + BytesContainerType::Output => { + let mut this = Self { + codec, + ..Default::default() + }; + + let value: Result = rmp_serde::decode::from_slice(&raw); + match value { + Ok(json) => { + this.json_value = Some(json.clone()); + this.humanized = serde_json::to_string_pretty(&json)?; + this.raw = raw; + } + Err(e) => { + this.humanized = String::from_utf8_lossy(&raw).into(); + this.encoding_error = Some(e.to_string()); + } + }; + + Ok(this) + } + }, + } + } +} diff --git a/src/engine.rs b/src/engine.rs index 61dd8bdd..45e777e3 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -7,10 +7,8 @@ use wasmtime_wasi::pipe::{MemoryInputPipe, MemoryOutputPipe}; use wasmtime_wasi::preview1::WasiP1Ctx; use wasmtime_wasi::{I32Exit, WasiCtxBuilder}; -use crate::function_run_result::{ - FunctionOutput::{self, InvalidJsonOutput, JsonOutput}, - FunctionRunResult, InvalidOutput, -}; +use crate::function_run_result::FunctionRunResult; +use crate::{BytesContainer, BytesContainerType}; #[derive(Clone)] pub struct ProfileOpts { @@ -50,7 +48,7 @@ fn import_modules( #[derive(Default)] pub struct FunctionRunParams<'a> { pub function_path: PathBuf, - pub input: Vec, + pub input: BytesContainer, pub export: &'a str, pub profile_opts: Option<&'a ProfileOpts>, pub scale_factor: f64, @@ -128,7 +126,7 @@ pub fn run(params: FunctionRunParams) -> Result { let module = Module::from_file(&engine, &function_path) .map_err(|e| anyhow!("Couldn't load the Function {:?}: {}", &function_path, e))?; - let input_stream = MemoryInputPipe::new(input.clone()); + let input_stream = MemoryInputPipe::new(input.raw.clone()); let output_stream = MemoryOutputPipe::new(usize::MAX); let error_stream = MemoryOutputPipe::new(usize::MAX); @@ -207,32 +205,18 @@ pub fn run(params: FunctionRunParams) -> Result { .try_into_inner() .expect("Output stream reference still exists"); - let output: FunctionOutput = match serde_json::from_slice(&raw_output) { - Ok(json_output) => JsonOutput(json_output), - Err(error) => InvalidJsonOutput(InvalidOutput { - stdout: std::str::from_utf8(&raw_output) - .map_err(|e| anyhow!("Couldn't print Function Output: {}", e)) - .unwrap() - .to_owned(), - error: error.to_string(), - }), - }; + let output = BytesContainer::new(BytesContainerType::Output, input.codec, raw_output.to_vec())?; let name = function_path.file_name().unwrap().to_str().unwrap(); let size = function_path.metadata()?.len() / 1024; - let parsed_input = - String::from_utf8(input).map_err(|e| anyhow!("Couldn't parse input: {}", e))?; - - let function_run_input = serde_json::from_str(&parsed_input)?; - let function_run_result = FunctionRunResult { name: name.to_string(), size, memory_usage, instructions, logs: String::from_utf8_lossy(&logs).into(), - input: function_run_input, + input, output, profile: profile_data, scale_factor, @@ -248,117 +232,133 @@ mod tests { use serde_json::json; use super::*; + use crate::Codec; + use anyhow::Result; use std::path::Path; const DEFAULT_EXPORT: &str = "_start"; + fn json_input(raw: &[u8]) -> Result { + BytesContainer::new(BytesContainerType::Input, Codec::Json, raw.to_vec()) + } + #[test] - fn test_js_function() { - let input = include_bytes!("../tests/fixtures/input/js_function_input.json").to_vec(); + fn test_js_function() -> Result<()> { + let input = json_input(include_bytes!( + "../tests/fixtures/input/js_function_input.json" + ))?; + let function_run_result = run(FunctionRunParams { function_path: Path::new("tests/fixtures/build/js_function.wasm").to_path_buf(), input, export: DEFAULT_EXPORT, ..Default::default() - }); + })?; - assert!(function_run_result.is_ok()); - assert_eq!(function_run_result.unwrap().memory_usage, 1280); + assert_eq!(function_run_result.memory_usage, 1280); + + Ok(()) } #[test] - fn test_js_v2_function() { - let input = include_bytes!("../tests/fixtures/input/js_function_input.json").to_vec(); + fn test_js_v2_function() -> Result<()> { + let input = json_input(include_bytes!( + "../tests/fixtures/input/js_function_input.json" + ))?; let function_run_result = run(FunctionRunParams { function_path: Path::new("tests/fixtures/build/js_function_v2.wasm").to_path_buf(), input, export: DEFAULT_EXPORT, ..Default::default() - }); + })?; - assert!(function_run_result.is_ok()); - assert_eq!(function_run_result.unwrap().memory_usage, 1344); + assert_eq!(function_run_result.memory_usage, 1344); + Ok(()) } #[test] - fn test_js_v3_function() { - let input = include_bytes!("../tests/fixtures/input/js_function_input.json").to_vec(); + fn test_js_v3_function() -> Result<()> { + let input = json_input(include_bytes!( + "../tests/fixtures/input/js_function_input.json" + ))?; + let function_run_result = run(FunctionRunParams { function_path: Path::new("tests/fixtures/build/js_function_v3.wasm").to_path_buf(), input, export: DEFAULT_EXPORT, ..Default::default() - }); + })?; - assert!(function_run_result.is_ok()); - assert_eq!(function_run_result.unwrap().memory_usage, 1344); + assert_eq!(function_run_result.memory_usage, 1344); + Ok(()) } #[test] - fn test_js_functions_javy_v1() { - let input = include_bytes!("../tests/fixtures/input/js_function_input.json").to_vec(); + fn test_js_functions_javy_v1() -> Result<()> { + let input = json_input(include_bytes!( + "../tests/fixtures/input/js_function_input.json" + ))?; + let function_run_result = run(FunctionRunParams { function_path: Path::new("tests/fixtures/build/js_functions_javy_v1.wasm") .to_path_buf(), input, export: DEFAULT_EXPORT, ..Default::default() - }); + })?; - assert!(function_run_result.is_ok()); - assert_eq!(function_run_result.unwrap().memory_usage, 1344); + assert_eq!(function_run_result.memory_usage, 1344); + Ok(()) } #[test] - fn test_exit_code_zero() { + fn test_exit_code_zero() -> Result<()> { let function_run_result = run(FunctionRunParams { function_path: Path::new("tests/fixtures/build/exit_code.wasm").to_path_buf(), - input: json!({ "code": 0 }).to_string().into(), + input: json_input(&serde_json::to_vec(&json!({ "code": 0 }))?)?, export: DEFAULT_EXPORT, ..Default::default() - }) - .unwrap(); + })?; assert_eq!(function_run_result.logs, ""); + Ok(()) } #[test] - fn test_exit_code_one() { + fn test_exit_code_one() -> Result<()> { let function_run_result = run(FunctionRunParams { function_path: Path::new("tests/fixtures/build/exit_code.wasm").to_path_buf(), - input: json!({ "code": 1 }).to_string().into(), + input: json_input(&serde_json::to_vec(&json!({ "code": 1 }))?)?, export: DEFAULT_EXPORT, ..Default::default() - }) - .unwrap(); + })?; assert_eq!(function_run_result.logs, "module exited with code: 1"); + Ok(()) } #[test] - fn test_linear_memory_usage_in_kb() { + fn test_linear_memory_usage_in_kb() -> Result<()> { let function_run_result = run(FunctionRunParams { function_path: Path::new("tests/fixtures/build/linear_memory.wasm").to_path_buf(), - input: "{}".as_bytes().to_vec(), + input: json_input(&serde_json::to_vec(&json!({}))?)?, export: DEFAULT_EXPORT, ..Default::default() - }) - .unwrap(); + })?; assert_eq!(function_run_result.memory_usage, 12800); // 200 * 64KiB pages + Ok(()) } #[test] - fn test_logs_truncation() { - let input = "{}".as_bytes().to_vec(); + fn test_logs_truncation() -> Result<()> { let function_run_result = run(FunctionRunParams { + input: json_input("{}".as_bytes())?, function_path: Path::new("tests/fixtures/build/log_truncation_function.wasm") .to_path_buf(), - input, export: DEFAULT_EXPORT, ..Default::default() - }) - .unwrap(); + })?; assert!( function_run_result.to_string().contains( @@ -368,23 +368,24 @@ mod tests { ), "Expected logs to be truncated, but were: {function_run_result}" ); + Ok(()) } #[test] - fn test_file_size_in_kb() { + fn test_file_size_in_kb() -> Result<()> { let file_path = Path::new("tests/fixtures/build/exit_code.wasm"); let function_run_result = run(FunctionRunParams { function_path: file_path.to_path_buf(), - input: json!({ "code": 0 }).to_string().into(), + input: json_input(&serde_json::to_vec(&json!({ "code": 0 }))?)?, export: DEFAULT_EXPORT, ..Default::default() - }) - .unwrap(); + })?; assert_eq!( function_run_result.size, file_path.metadata().unwrap().len() / 1024 ); + Ok(()) } } diff --git a/src/function_run_result.rs b/src/function_run_result.rs index 89e9db83..52d2cd2a 100644 --- a/src/function_run_result.rs +++ b/src/function_run_result.rs @@ -1,3 +1,4 @@ +use crate::BytesContainer; use colored::Colorize; use serde::{Deserialize, Serialize}; use std::fmt; @@ -10,13 +11,6 @@ pub struct InvalidOutput { pub stdout: String, } -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(untagged)] -pub enum FunctionOutput { - JsonOutput(serde_json::Value), - InvalidJsonOutput(InvalidOutput), -} - #[derive(Serialize, Deserialize, Clone, Debug)] pub struct FunctionRunResult { pub name: String, @@ -24,8 +18,8 @@ pub struct FunctionRunResult { pub memory_usage: u64, pub instructions: u64, pub logs: String, - pub input: serde_json::Value, - pub output: FunctionOutput, + pub input: BytesContainer, + pub output: BytesContainer, #[serde(skip)] pub profile: Option, #[serde(skip)] @@ -37,24 +31,17 @@ const DEFAULT_INSTRUCTIONS_LIMIT: u64 = 11_000_000; const DEFAULT_INPUT_SIZE_LIMIT: u64 = 128_000; const DEFAULT_OUTPUT_SIZE_LIMIT: u64 = 20_000; -pub fn get_json_size_as_bytes(value: &serde_json::Value) -> usize { - serde_json::to_vec(value).map(|v| v.len()).unwrap_or(0) -} - impl FunctionRunResult { pub fn to_json(&self) -> String { serde_json::to_string_pretty(&self).unwrap_or_else(|error| error.to_string()) } pub fn input_size(&self) -> usize { - get_json_size_as_bytes(&self.input) + self.input.raw.len() } pub fn output_size(&self) -> usize { - match &self.output { - FunctionOutput::JsonOutput(value) => get_json_size_as_bytes(value), - FunctionOutput::InvalidJsonOutput(_value) => 0, - } + self.output.raw.len() } } @@ -98,8 +85,7 @@ impl fmt::Display for FunctionRunResult { formatter, "{}\n\n{}", " Input ".black().on_bright_yellow(), - serde_json::to_string_pretty(&self.input) - .expect("Input should be serializable to a string") + self.input.humanized, )?; writeln!( @@ -120,31 +106,27 @@ impl fmt::Display for FunctionRunResult { )?; } - match &self.output { - FunctionOutput::JsonOutput(json_output) => { - writeln!( - formatter, - "{}\n\n{}", - " Output ".black().on_bright_green(), - serde_json::to_string_pretty(&json_output) - .expect("Output should be serializable to a string") - )?; - } - FunctionOutput::InvalidJsonOutput(invalid_output) => { - writeln!( - formatter, - "{}\n\n{}", - " Invalid Output ".black().on_bright_red(), - invalid_output.stdout - )?; - - writeln!( - formatter, - "{}\n\n{}", - " JSON Error ".black().on_bright_red(), - invalid_output.error - )?; - } + if let Some(e) = &self.output.encoding_error { + writeln!( + formatter, + "{}\n\n{}", + " Invalid Output ".black().on_bright_red(), + self.output.humanized, + )?; + + writeln!( + formatter, + "{}\n\n{}", + " JSON Error ".black().on_bright_red(), + e + )?; + } else { + writeln!( + formatter, + "{}\n\n{}", + " Output ".black().on_bright_green(), + self.output.humanized, + )?; } let input_size_limit = self.scale_factor * DEFAULT_INPUT_SIZE_LIMIT as f64; @@ -234,13 +216,22 @@ mod tests { use anyhow::Result; use predicates::prelude::*; + use crate::*; + use super::*; + fn json_output(raw: &[u8]) -> Result { + BytesContainer::new(BytesContainerType::Output, Codec::Json, raw.to_vec()) + } + + fn mock_json_input() -> Result { + let bytes = "{\"input_test\": \"input_value\"}".as_bytes(); + BytesContainer::new(BytesContainerType::Input, Codec::Json, bytes.to_vec()) + } + #[test] fn test_js_output() -> Result<()> { - let mock_input_string = "{\"input_test\": \"input_value\"}".to_string(); - let mock_function_input = serde_json::from_str(&mock_input_string)?; - let expected_input_display = serde_json::to_string_pretty(&mock_function_input)?; + let input = mock_json_input()?; let function_run_result = FunctionRunResult { name: "test".to_string(), @@ -248,10 +239,10 @@ mod tests { memory_usage: 1000, instructions: 1001, logs: "test".to_string(), - input: mock_function_input, - output: FunctionOutput::JsonOutput(serde_json::json!({ + input: input.clone(), + output: json_output(&serde_json::to_vec(&serde_json::json!({ "test": "test" - })), + }))?)?, profile: None, scale_factor: 1.0, success: true, @@ -259,7 +250,7 @@ mod tests { let predicate = predicates::str::contains("Instructions: 1.001K") .and(predicates::str::contains("Linear Memory Usage: 1000KB")) - .and(predicates::str::contains(expected_input_display)) + .and(predicates::str::contains(input.humanized)) .and(predicates::str::contains("Input Size: 28B")) .and(predicates::str::contains("Output Size: 15B")); assert!(predicate.eval(&function_run_result.to_string())); @@ -270,9 +261,7 @@ mod tests { #[test] fn test_js_output_1000() -> Result<()> { - let mock_input_string = "{\"input_test\": \"input_value\"}".to_string(); - let mock_function_input = serde_json::from_str(&mock_input_string)?; - let expected_input_display = serde_json::to_string_pretty(&mock_function_input)?; + let input = mock_json_input()?; let function_run_result = FunctionRunResult { name: "test".to_string(), @@ -280,10 +269,10 @@ mod tests { memory_usage: 1000, instructions: 1000, logs: "test".to_string(), - input: mock_function_input, - output: FunctionOutput::JsonOutput(serde_json::json!({ + input: input.clone(), + output: json_output(&serde_json::to_vec(&serde_json::json!({ "test": "test" - })), + }))?)?, profile: None, scale_factor: 1.0, success: true, @@ -291,16 +280,14 @@ mod tests { let predicate = predicates::str::contains("Instructions: 1") .and(predicates::str::contains("Linear Memory Usage: 1000KB")) - .and(predicates::str::contains(expected_input_display)); + .and(predicates::str::contains(input.humanized)); assert!(predicate.eval(&function_run_result.to_string())); Ok(()) } #[test] fn test_instructions_less_than_1000() -> Result<()> { - let mock_input_string = "{\"input_test\": \"input_value\"}".to_string(); - let mock_function_input = serde_json::from_str(&mock_input_string)?; - let expected_input_display = serde_json::to_string_pretty(&mock_function_input)?; + let input = mock_json_input()?; let function_run_result = FunctionRunResult { name: "test".to_string(), @@ -308,10 +295,10 @@ mod tests { memory_usage: 1000, instructions: 999, logs: "test".to_string(), - input: mock_function_input, - output: FunctionOutput::JsonOutput(serde_json::json!({ + input: input.clone(), + output: json_output(&serde_json::to_vec(&serde_json::json!({ "test": "test" - })), + }))?)?, profile: None, scale_factor: 1.0, success: true, @@ -319,7 +306,7 @@ mod tests { let predicate = predicates::str::contains("Instructions: 999") .and(predicates::str::contains("Linear Memory Usage: 1000KB")) - .and(predicates::str::contains(expected_input_display)); + .and(predicates::str::contains(input.humanized)); assert!(predicate.eval(&function_run_result.to_string())); Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index a7f46265..ecde0ba7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,20 @@ pub mod bluejay_schema_analyzer; +pub mod container; pub mod engine; pub mod function_run_result; pub mod scale_limits_analyzer; +use clap::ValueEnum; + +pub use container::*; + +/// Supported input encoding. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)] +pub enum Codec { + #[default] + /// JSON input. + Json, + /// Raw input, no validation, passed as-is. + Raw, + /// JSON input encoded as Messagepack. + Messagepack, +} diff --git a/src/main.rs b/src/main.rs index 383b2284..b176dec5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +use function_runner::{BytesContainer, BytesContainerType, Codec}; + use std::{ fs::File, io::{stdin, BufReader, Read}, @@ -5,7 +7,7 @@ use std::{ }; use anyhow::{anyhow, Result}; -use clap::{Parser, ValueEnum}; +use clap::Parser; use function_runner::{ bluejay_schema_analyzer::BluejaySchemaAnalyzer, engine::{run, FunctionRunParams, ProfileOpts}, @@ -16,17 +18,6 @@ use is_terminal::IsTerminal; const PROFILE_DEFAULT_INTERVAL: u32 = 500_000; // every 5us const DEFAULT_SCALE_FACTOR: f64 = 1.0; -/// Supported input flavors -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -enum Codec { - /// JSON input, must be valid JSON - Json, - /// Raw input, no validation, passed as-is - Raw, - /// JSON input, will be converted to MessagePack, must be valid JSON - JsonToMessagepack, -} - /// Simple Function runner which takes JSON as a convenience. #[derive(Parser, Debug)] #[clap(version)] @@ -144,26 +135,9 @@ fn main() -> Result<()> { let query_string = opts.read_query_to_string().transpose()?; - let (json_value, buffer) = match opts.codec { - Codec::Json => { - let json = serde_json::from_slice::(&buffer) - .map_err(|e| anyhow!("Invalid input JSON: {}", e))?; - let minified_buffer = - serde_json::to_vec(&json).map_err(|e| anyhow!("Couldn't serialize JSON: {}", e))?; - (Some(json), minified_buffer) - } - Codec::Raw => (None, buffer), - Codec::JsonToMessagepack => { - let json: serde_json::Value = serde_json::from_slice(&buffer) - .map_err(|e| anyhow!("Invalid input JSON: {}", e))?; - let bytes = rmp_serde::to_vec(&json) - .map_err(|e| anyhow!("Couldn't convert JSON to MessagePack: {}", e))?; - (Some(json), bytes) - } - }; - + let input = BytesContainer::new(BytesContainerType::Input, opts.codec, buffer)?; let scale_factor = if let (Some(schema_string), Some(query_string), Some(json_value)) = - (schema_string, query_string, json_value) + (schema_string, query_string, input.json_value.clone()) { BluejaySchemaAnalyzer::analyze_schema_definition( &schema_string, @@ -180,7 +154,7 @@ fn main() -> Result<()> { let function_run_result = run(FunctionRunParams { function_path: opts.function, - input: buffer, + input, export: opts.export.as_ref(), profile_opts: profile_opts.as_ref(), scale_factor, diff --git a/tests/fixtures/build/echo.wasm b/tests/fixtures/build/echo.wasm new file mode 100644 index 00000000..d2f92ba7 Binary files /dev/null and b/tests/fixtures/build/echo.wasm differ diff --git a/tests/fixtures/build/messagepack-invalid.wasm b/tests/fixtures/build/messagepack-invalid.wasm new file mode 100644 index 00000000..68c0033e Binary files /dev/null and b/tests/fixtures/build/messagepack-invalid.wasm differ diff --git a/tests/fixtures/build/messagepack-valid.wasm b/tests/fixtures/build/messagepack-valid.wasm new file mode 100644 index 00000000..79cfd966 Binary files /dev/null and b/tests/fixtures/build/messagepack-valid.wasm differ diff --git a/tests/fixtures/echo/Cargo.toml b/tests/fixtures/echo/Cargo.toml new file mode 100644 index 00000000..51d5eb6c --- /dev/null +++ b/tests/fixtures/echo/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "echo" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/tests/fixtures/echo/src/main.rs b/tests/fixtures/echo/src/main.rs new file mode 100644 index 00000000..ee274407 --- /dev/null +++ b/tests/fixtures/echo/src/main.rs @@ -0,0 +1,7 @@ +use std::io::{Read, Write}; + +fn main() { + let mut buf: Vec = vec![]; + std::io::stdin().read_to_end(&mut buf).unwrap(); + std::io::stdout().write_all(&buf).unwrap(); +} diff --git a/tests/fixtures/messagepack-invalid/Cargo.toml b/tests/fixtures/messagepack-invalid/Cargo.toml new file mode 100644 index 00000000..81dc9124 --- /dev/null +++ b/tests/fixtures/messagepack-invalid/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "messagepack-invalid" +version = "0.1.0" +edition = "2021" + +[dependencies] +rmpv = "1.3.0" diff --git a/tests/fixtures/messagepack-invalid/src/main.rs b/tests/fixtures/messagepack-invalid/src/main.rs new file mode 100644 index 00000000..515c86c2 --- /dev/null +++ b/tests/fixtures/messagepack-invalid/src/main.rs @@ -0,0 +1,10 @@ +use std::io::{Read, Write}; + +fn main() { + let mut buf: Vec = vec![]; + std::io::stdin().read_to_end(&mut buf).unwrap(); + std::io::stdout() + // Invalid messagepack. + .write_all(&[192, 193, 194, 195, 196, 197, 198, 199, 200, 201]) + .unwrap(); +} diff --git a/tests/fixtures/messagepack-valid/Cargo.toml b/tests/fixtures/messagepack-valid/Cargo.toml new file mode 100644 index 00000000..0dbd8fcc --- /dev/null +++ b/tests/fixtures/messagepack-valid/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "messagepack-valid" +version = "0.1.0" +edition = "2021" + +[dependencies] +rmpv = "1.3.0" diff --git a/tests/fixtures/messagepack-valid/src/main.rs b/tests/fixtures/messagepack-valid/src/main.rs new file mode 100644 index 00000000..5e3dd736 --- /dev/null +++ b/tests/fixtures/messagepack-valid/src/main.rs @@ -0,0 +1,9 @@ +use std::io::{Read, Write}; + +fn main() { + let mut buf: Vec = vec![]; + std::io::stdin().read_to_end(&mut buf).unwrap(); + let mut cursor = std::io::Cursor::new(&buf); + rmpv::decode::read_value(&mut cursor).expect("Valid messagepack"); + std::io::stdout().write_all(&buf).unwrap(); +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 5b47f54f..b1ccec0a 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,19 +1,21 @@ #[cfg(test)] mod tests { + use anyhow::Result; use assert_cmd::prelude::*; use assert_fs::prelude::*; use function_runner::function_run_result::FunctionRunResult; use predicates::prelude::*; use predicates::{prelude::predicate, str::contains}; use serde_json::json; + use std::io::Write; use std::{ fs::File, process::{Command, Stdio}, }; #[test] - fn run() -> Result<(), Box> { + fn run() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; let input_file = temp_input(json!({"count": 0}))?; @@ -26,7 +28,7 @@ mod tests { } #[test] - fn invalid_json_input() -> Result<(), Box> { + fn invalid_json_input() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; cmd.args(["--function", "tests/fixtures/build/exit_code.wasm"]) @@ -40,7 +42,7 @@ mod tests { } #[test] - fn run_stdin() -> Result<(), Box> { + fn run_stdin() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; let input_file = temp_input(json!({"exit_code": 0}))?; @@ -63,7 +65,7 @@ mod tests { } #[test] - fn run_no_opts() -> Result<(), Box> { + fn run_no_opts() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; let output = cmd .stdout(Stdio::piped()) @@ -86,7 +88,7 @@ mod tests { #[test] #[ignore = "This test hangs on CI but runs locally, is_terminal is likely returning false in CI"] - fn run_function_no_input() -> Result<(), Box> { + fn run_function_no_input() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; cmd.args(["--function", "tests/fixtures/build/exit_code.wasm"]); @@ -98,7 +100,7 @@ mod tests { } #[test] - fn run_json() -> Result<(), Box> { + fn run_json() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; let input_file = temp_input(json!({"count": 0}))?; @@ -115,7 +117,7 @@ mod tests { } #[test] - fn wasm_file_doesnt_exist() -> Result<(), Box> { + fn wasm_file_doesnt_exist() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; let input_file = temp_input(json!({"exit_code": 0}))?; @@ -130,7 +132,7 @@ mod tests { } #[test] - fn input_file_doesnt_exist() -> Result<(), Box> { + fn input_file_doesnt_exist() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; cmd.args(["--function", "tests/fixtures/build/exit_code.wasm"]) @@ -143,7 +145,7 @@ mod tests { } #[test] - fn profile_writes_file() -> Result<(), Box> { + fn profile_writes_file() -> Result<()> { let (mut cmd, temp) = profile_base_cmd_in_temp_dir()?; cmd.arg("--profile").assert().success(); temp.child("noop.perf").assert(predicate::path::exists()); @@ -152,7 +154,7 @@ mod tests { } #[test] - fn profile_writes_specified_file_name() -> Result<(), Box> { + fn profile_writes_specified_file_name() -> Result<()> { let (mut cmd, temp) = profile_base_cmd_in_temp_dir()?; cmd.args(["--profile-out", "foo.perf"]).assert().success(); temp.child("foo.perf").assert(predicate::path::exists()); @@ -161,7 +163,7 @@ mod tests { } #[test] - fn profile_frequency_triggers_profiling() -> Result<(), Box> { + fn profile_frequency_triggers_profiling() -> Result<()> { let (mut cmd, temp) = profile_base_cmd_in_temp_dir()?; cmd.args(["--profile-frequency", "80000"]) .assert() @@ -172,7 +174,7 @@ mod tests { } #[test] - fn incorrect_input() -> Result<(), Box> { + fn incorrect_input() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; let input_file = temp_input(json!({}))?; @@ -194,7 +196,7 @@ mod tests { } #[test] - fn exports() -> Result<(), Box> { + fn exports() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; let input_file = temp_input(json!({}))?; cmd.args(["--function", "tests/fixtures/build/exports.wasm"]) @@ -208,7 +210,7 @@ mod tests { } #[test] - fn missing_export() -> Result<(), Box> { + fn missing_export() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; let input_file = temp_input(json!({}))?; cmd.args(["--function", "tests/fixtures/build/exports.wasm"]) @@ -223,8 +225,7 @@ mod tests { } #[test] - fn failing_function_returns_non_zero_exit_code_for_module_errors( - ) -> Result<(), Box> { + fn failing_function_returns_non_zero_exit_code_for_module_errors() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; let input_file = temp_input(json!({}))?; cmd.args([ @@ -241,8 +242,7 @@ mod tests { Ok(()) } - fn profile_base_cmd_in_temp_dir( - ) -> Result<(Command, assert_fs::TempDir), Box> { + fn profile_base_cmd_in_temp_dir() -> Result<(Command, assert_fs::TempDir)> { let mut cmd = Command::cargo_bin("function-runner")?; let cwd = std::env::current_dir()?; let temp = assert_fs::TempDir::new()?; @@ -258,9 +258,7 @@ mod tests { Ok((cmd, temp)) } - fn temp_input( - json: serde_json::Value, - ) -> Result> { + fn temp_input(json: serde_json::Value) -> Result { let file = assert_fs::NamedTempFile::new("input.json")?; file.write_str(json.to_string().as_str())?; @@ -268,8 +266,7 @@ mod tests { } #[test] - fn test_scale_limits_analyzer_use_defaults_when_query_and_schema_not_provided( - ) -> Result<(), Box> { + fn test_scale_limits_analyzer_use_defaults_when_query_and_schema_not_provided() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; let input_file = temp_input(json!({"cart": { "lines": [ @@ -292,8 +289,7 @@ mod tests { } #[test] - fn test_scale_limits_analyzer_use_defaults_when_query_or_schema_not_provided( - ) -> Result<(), Box> { + fn test_scale_limits_analyzer_use_defaults_when_query_or_schema_not_provided() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; let input_file = temp_input(json!({"cart": { "lines": [ @@ -318,7 +314,7 @@ mod tests { } #[test] - fn test_scale_limits_analyzer_with_scaled_limits() -> Result<(), Box> { + fn test_scale_limits_analyzer_with_scaled_limits() -> Result<()> { let mut cmd = Command::cargo_bin("function-runner")?; let input_data = vec![json!({"quantity": 2}); 400]; @@ -351,4 +347,80 @@ mod tests { Ok(()) } + + #[test] + fn messagepack_roundtrip() -> Result<()> { + let mut cmd = Command::cargo_bin("function-runner")?; + let input = temp_input(json!({"hello": "world"}))?; + + cmd.args(["--function", "tests/fixtures/build/messagepack-valid.wasm"]) + .arg("--codec") + .arg("messagepack") + .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"); + + cmd.assert().success(); + cmd.assert().stdout(contains("hello")); + cmd.assert().stdout(contains("world")); + + Ok(()) + } + + #[test] + fn messagepack_failure() -> Result<()> { + let mut cmd = Command::cargo_bin("function-runner")?; + let input = temp_input(json!({}))?; + + cmd.args([ + "--function", + "tests/fixtures/build/messagepack-invalid.wasm", + ]) + .arg("--codec") + .arg("messagepack") + .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"); + + cmd.assert().success(); + cmd.assert().stdout(contains("null")); + + Ok(()) + } + + #[test] + fn raw_roundtrip() -> Result<()> { + let mut cmd = Command::cargo_bin("function-runner")?; + let input = temp_input(json!({}))?; + + let mut child = cmd + .args(["--function", "tests/fixtures/build/echo.wasm"]) + .arg("--codec") + .arg("raw") + .arg("--json") + .arg("--input") + .arg(input.as_os_str()) + .stdout(Stdio::piped()) + .stdin(Stdio::piped()) + .spawn()?; + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(&[1, 2])?; + } + + child.wait_with_output()?; + cmd.assert().success(); + cmd.assert().stdout(contains("7b 7d")); + + Ok(()) + } }