diff --git a/Cargo.lock b/Cargo.lock index df38b48c..41faa2ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -991,7 +991,7 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "function-runner" -version = "9.2.2" +version = "10.0.0" dependencies = [ "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index e9770735..5fc278a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = [ [package] name = "function-runner" -version = "9.2.2" +version = "10.0.0" edition = "2021" diff --git a/README.md b/README.md index 0d5e953f..74546111 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,26 @@ Profiling is not available in batch mode. ## Library usage +The `views` module turns run data into output. Each view borrows the data and +builds its text only when you display it: + +- `ReadableBytes::from(&container)`: readable text for an input or an output. + It is pretty-printed JSON when the bytes decode to JSON or MessagePack, a hex + dump for the `Raw` codec, or the bytes as lossy UTF-8 when an output does not + decode. `encoding_error` gives the reason, and `raw` keeps the bytes. +- `TextReport::from(&result)`: the report that the CLI prints by default. +- `JsonReport::from(&result)`: the JSON that `--json` prints. It uses the + result's serde serialization, so `serde_json::to_value(&result)` has the same + fields. + +```rust +use function_runner::views::{JsonReport, ReadableBytes, TextReport}; + +println!("{}", TextReport::from(&result)); +println!("{}", JsonReport::from(&result)); +println!("{}", ReadableBytes::from(&result.output)); +``` + To compute scale factors for many inputs, use `bluejay_schema_analyzer::BluejaySchemaAnalyzer::with_analyzer`. It parses the schema and query once, then calls your closure with an `analyze` function that @@ -97,6 +117,24 @@ the schema, so a query with an unknown field still parses. The `test_with_analyzer_analyzes_many_inputs` test in `src/bluejay_schema_analyzer.rs` runs this pattern. +### Upgrading to 10.0 + +10.0 moves display code out of `BytesContainer` and `FunctionRunResult` and +into the `views` module: + +| 9.x | 10.0 | +|---|---| +| `container.humanized` | `ReadableBytes::from(&container).to_string()` | +| `result.to_string()` or `format!("{result}")` | `TextReport::from(&result).to_string()` | +| `result.to_json()` | `JsonReport::from(&result).to_string()` | + +Other changes: + +- `BytesContainer::raw` keeps the bytes of an output that does not decode. In + 9.x it was empty. +- `Codec` no longer implements `clap::ValueEnum`. +- The unused `InvalidOutput` type is removed. + ## Development Building requires a rust toolchain of `1.66.0` to `1.67.0`. `cargo install --path . --locked` will build diff --git a/src/container.rs b/src/container.rs index 1e010593..dd1eec3d 100644 --- a/src/container.rs +++ b/src/container.rs @@ -109,9 +109,6 @@ pub struct BytesContainer { /// The JSON represantation of the bytes. #[serde(flatten)] pub json_value: Option, - /// The human readable representation of the bytes. - #[serde(skip)] - pub humanized: String, /// Context for encoding errors. #[serde(skip)] pub encoding_error: Option, @@ -121,7 +118,6 @@ impl Default for BytesContainer { fn default() -> Self { Self { codec: Codec::Raw, - humanized: "".into(), json_value: None, raw: Default::default(), encoding_error: None, @@ -132,20 +128,11 @@ impl Default for BytesContainer { 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::Raw => Ok(Self { + raw, + codec, + ..Default::default() + }), Codec::Json => match ty { BytesContainerType::Input => { let json = serde_json::from_slice::(&raw) @@ -155,30 +142,25 @@ impl BytesContainer { Ok(Self { codec, raw: minified_buffer, - json_value: Some(json.clone()), - humanized: serde_json::to_string_pretty(&json)?, + json_value: Some(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) + Ok(json) => Ok(Self { + codec, + raw: serde_json::to_vec(&json)?, + json_value: Some(json), + encoding_error: None, + }), + Err(e) => Ok(Self { + codec, + raw, + json_value: None, + encoding_error: Some(e.to_string()), + }), + } } }, Codec::Messagepack => match ty { @@ -191,31 +173,26 @@ impl BytesContainer { Ok(Self { raw: bytes, codec, - json_value: Some(json.clone()), - humanized: serde_json::to_string_pretty(&json)?, + json_value: Some(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) + Ok(json) => Ok(Self { + codec, + raw, + json_value: Some(json), + encoding_error: None, + }), + Err(e) => Ok(Self { + codec, + raw, + json_value: None, + encoding_error: Some(e.to_string()), + }), + } } }, } diff --git a/src/engine.rs b/src/engine.rs index 6f120a93..cecc715a 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -99,7 +99,7 @@ pub fn run(params: FunctionRunParams) -> Result { module, } = params; - let mut io_handler = IOHandler::new(ValidatedModule::new(module)?, input.clone()); + let mut io_handler = IOHandler::new(ValidatedModule::new(module)?, &input); let mut error_logs: String = String::new(); @@ -165,11 +165,7 @@ pub fn run(params: FunctionRunParams) -> Result { logs.extend_from_slice(error_logs.as_bytes()); let output_codec = input.codec; - let output = BytesContainer::new( - BytesContainerType::Output, - output_codec, - raw_output.to_vec(), - )?; + let output = BytesContainer::new(BytesContainerType::Output, output_codec, raw_output)?; let name = function_path.file_name().unwrap().to_str().unwrap(); let size = function_path.metadata()?.len() / 1024; @@ -439,13 +435,14 @@ mod tests { profile_opts: None, })?; + let report = crate::views::TextReport::from(&function_run_result).to_string(); assert!( - function_run_result.to_string().contains( + report.contains( &"Logs would be truncated in production, length 6000 > 1000 limit" .red() .to_string() ), - "Expected logs to be truncated, but were: {function_run_result}" + "Expected logs to be truncated, but were: {report}" ); Ok(()) } diff --git a/src/function_run_result.rs b/src/function_run_result.rs index 948551f0..a8a98812 100644 --- a/src/function_run_result.rs +++ b/src/function_run_result.rs @@ -1,16 +1,8 @@ use crate::BytesContainer; -use colored::Colorize; use serde::{Deserialize, Serialize}; -use std::fmt; pub(crate) const FUNCTION_LOG_LIMIT: usize = 1_000; -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct InvalidOutput { - pub error: String, - pub stdout: String, -} - #[derive(Serialize, Deserialize, Clone, Debug)] pub struct FunctionRunResult { pub name: String, @@ -27,15 +19,7 @@ pub struct FunctionRunResult { pub success: bool, } -const DEFAULT_INSTRUCTIONS_LIMIT: u64 = 11_000_000; -const DEFAULT_INPUT_SIZE_LIMIT: u64 = 128_000; -const DEFAULT_OUTPUT_SIZE_LIMIT: u64 = 20_000; - 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 { self.input.raw.len() } @@ -44,270 +28,3 @@ impl FunctionRunResult { self.output.raw.len() } } - -fn humanize_size(title: &str, size_bytes: u64, size_limit: u64) -> String { - let size_humanized = match size_bytes { - 0..=1023 => format!("{}B", size_bytes), - 1024..=1_048_575 => format!("{:.2}KB", size_bytes as f64 / 1024.0), - 1_048_576..=1_073_741_823 => format!("{:.2}MB", size_bytes as f64 / 1_048_576.0), - _ => { - format!("{:.2}GB", size_bytes as f64 / 1_073_741_824.0) - } - }; - - if size_bytes > size_limit { - format!("{}: {}", title, size_humanized).red().to_string() - } else { - format!("{}: {}", title, size_humanized) - } -} - -fn humanize_instructions(title: &str, instructions: u64, instructions_limit: u64) -> String { - let instructions_humanized = match instructions { - 0..=999 => instructions.to_string(), - 1000..=999_999 => format!("{}K", instructions as f64 / 1000.0), - 1_000_000..=999_999_999 => format!("{}M", instructions as f64 / 1_000_000.0), - 1_000_000_000..=u64::MAX => format!("{}B", instructions as f64 / 1_000_000_000.0), - }; - - if instructions > instructions_limit { - format!("{}: {}", title, instructions_humanized) - .red() - .to_string() - } else { - format!("{}: {}", title, instructions_humanized) - } -} - -impl fmt::Display for FunctionRunResult { - fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - writeln!( - formatter, - "{}\n\n{}", - " Input ".black().on_bright_yellow(), - self.input.humanized, - )?; - - writeln!( - formatter, - "{}\n\n{}\n", - " Logs ".black().on_bright_blue(), - self.logs - )?; - - let logs_length = self.logs.len(); - if logs_length > FUNCTION_LOG_LIMIT { - writeln!( - formatter, - "{}\n\n", - &format!( - "Logs would be truncated in production, length {logs_length} > {FUNCTION_LOG_LIMIT} limit", - ).red() - )?; - } - - 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; - let output_size_limit = self.scale_factor * DEFAULT_OUTPUT_SIZE_LIMIT as f64; - let instructions_size_limit = self.scale_factor * DEFAULT_INSTRUCTIONS_LIMIT as f64; - - writeln!( - formatter, - "\n{}\n\n", - " Resource Limits " - .black() - .on_bright_magenta() - )?; - - writeln!( - formatter, - "{}", - humanize_size( - "Input Size", - input_size_limit as u64, - input_size_limit as u64 - ) - )?; - - writeln!( - formatter, - "{}", - humanize_size( - "Output Size", - output_size_limit as u64, - output_size_limit as u64 - ) - )?; - writeln!( - formatter, - "{}", - humanize_instructions( - "Instructions", - instructions_size_limit as u64, - instructions_size_limit as u64 - ) - )?; - - let title = " Benchmark Results " - .black() - .on_truecolor(150, 191, 72); - - write!(formatter, "\n\n{title}\n\n")?; - writeln!(formatter, "Name: {}", self.name)?; - writeln!(formatter, "Linear Memory Usage: {}KB", self.memory_usage)?; - writeln!( - formatter, - "{}", - humanize_instructions( - "Instructions", - self.instructions, - instructions_size_limit as u64 - ) - )?; - writeln!( - formatter, - "{}", - humanize_size( - "Input Size", - self.input_size() as u64, - input_size_limit as u64, - ) - )?; - writeln!( - formatter, - "{}", - humanize_size( - "Output Size", - self.output_size() as u64, - output_size_limit as u64, - ) - )?; - - writeln!(formatter, "Module Size: {}KB\n", self.size)?; - - Ok(()) - } -} - -#[cfg(test)] -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 input = mock_json_input()?; - - let function_run_result = FunctionRunResult { - name: "test".to_string(), - size: 100, - memory_usage: 1000, - instructions: 1001, - logs: "test".to_string(), - input: input.clone(), - output: json_output(&serde_json::to_vec(&serde_json::json!({ - "test": "test" - }))?)?, - profile: None, - scale_factor: 1.0, - success: true, - }; - - let predicate = predicates::str::contains("Instructions: 1.001K") - .and(predicates::str::contains("Linear Memory Usage: 1000KB")) - .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())); - - assert!(predicate.eval(&function_run_result.to_string())); - Ok(()) - } - - #[test] - fn test_js_output_1000() -> Result<()> { - let input = mock_json_input()?; - - let function_run_result = FunctionRunResult { - name: "test".to_string(), - size: 100, - memory_usage: 1000, - instructions: 1000, - logs: "test".to_string(), - input: input.clone(), - output: json_output(&serde_json::to_vec(&serde_json::json!({ - "test": "test" - }))?)?, - profile: None, - scale_factor: 1.0, - success: true, - }; - - let predicate = predicates::str::contains("Instructions: 1") - .and(predicates::str::contains("Linear Memory Usage: 1000KB")) - .and(predicates::str::contains(input.humanized)); - assert!(predicate.eval(&function_run_result.to_string())); - Ok(()) - } - - #[test] - fn test_instructions_less_than_1000() -> Result<()> { - let input = mock_json_input()?; - - let function_run_result = FunctionRunResult { - name: "test".to_string(), - size: 100, - memory_usage: 1000, - instructions: 999, - logs: "test".to_string(), - input: input.clone(), - output: json_output(&serde_json::to_vec(&serde_json::json!({ - "test": "test" - }))?)?, - profile: None, - scale_factor: 1.0, - success: true, - }; - - let predicate = predicates::str::contains("Instructions: 999") - .and(predicates::str::contains("Linear Memory Usage: 1000KB")) - .and(predicates::str::contains(input.humanized)); - assert!(predicate.eval(&function_run_result.to_string())); - Ok(()) - } -} diff --git a/src/io.rs b/src/io.rs index ce2ea671..fddbcf9a 100644 --- a/src/io.rs +++ b/src/io.rs @@ -25,14 +25,14 @@ enum IOStrategy { Memory(Option), } -pub(crate) struct IOHandler { +pub(crate) struct IOHandler<'a> { strategy: IOStrategy, module: ValidatedModule, - input: BytesContainer, + input: &'a BytesContainer, } -impl IOHandler { - pub(crate) fn new(module: ValidatedModule, input: BytesContainer) -> Self { +impl<'a> IOHandler<'a> { + pub(crate) fn new(module: ValidatedModule, input: &'a BytesContainer) -> Self { Self { strategy: if module.uses_mem_io() { IOStrategy::Memory(None) diff --git a/src/lib.rs b/src/lib.rs index 19e50075..f54bdad4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,12 +5,12 @@ pub mod function_run_result; mod io; pub mod scale_limits_analyzer; mod validated_module; -use clap::ValueEnum; +pub mod views; pub use container::*; /// Supported input encoding. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum Codec { #[default] /// JSON input. diff --git a/src/main.rs b/src/main.rs index 97fa0775..865e4b44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,9 @@ mod batch; -use function_runner::{BytesContainer, BytesContainerType, Codec}; +use function_runner::{ + views::{JsonReport, TextReport}, + BytesContainer, BytesContainerType, Codec, +}; use wasmtime::Module; use std::{ @@ -224,9 +227,9 @@ fn main() -> Result<()> { })?; if opts.json { - println!("{}", function_run_result.to_json()); + println!("{}", JsonReport::from(&function_run_result)); } else { - println!("{function_run_result}"); + println!("{}", TextReport::from(&function_run_result)); } if let Some(profile) = function_run_result.profile.as_ref() { diff --git a/src/views.rs b/src/views.rs new file mode 100644 index 00000000..feafe90a --- /dev/null +++ b/src/views.rs @@ -0,0 +1,401 @@ +use std::fmt; + +use colored::Colorize; +use serde::Serialize; + +use crate::{ + function_run_result::{FunctionRunResult, FUNCTION_LOG_LIMIT}, + BytesContainer, Codec, +}; + +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 struct ReadableBytes<'a>(&'a BytesContainer); + +impl<'a> From<&'a BytesContainer> for ReadableBytes<'a> { + fn from(bytes: &'a BytesContainer) -> Self { + Self(bytes) + } +} + +impl fmt::Display for ReadableBytes<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let bytes = self.0; + + if let Codec::Raw = bytes.codec { + for (index, byte) in bytes.raw.iter().enumerate() { + if index > 0 { + f.write_str(" ")?; + } + write!(f, "{byte:02x}")?; + } + return Ok(()); + } + + match &bytes.json_value { + Some(json) => { + let pretty = serde_json::to_string_pretty(json).map_err(|_| fmt::Error)?; + f.write_str(&pretty) + } + None => f.write_str(&String::from_utf8_lossy(&bytes.raw)), + } + } +} + +pub struct TextReport<'a>(&'a FunctionRunResult); + +impl<'a> From<&'a FunctionRunResult> for TextReport<'a> { + fn from(result: &'a FunctionRunResult) -> Self { + Self(result) + } +} + +impl fmt::Display for TextReport<'_> { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + let result = self.0; + + writeln!( + formatter, + "{}\n\n{}", + " Input ".black().on_bright_yellow(), + ReadableBytes::from(&result.input), + )?; + + writeln!( + formatter, + "{}\n\n{}\n", + " Logs ".black().on_bright_blue(), + result.logs + )?; + + let logs_length = result.logs.len(); + if logs_length > FUNCTION_LOG_LIMIT { + writeln!( + formatter, + "{}\n\n", + &format!( + "Logs would be truncated in production, length {logs_length} > {FUNCTION_LOG_LIMIT} limit", + ).red() + )?; + } + + if let Some(e) = &result.output.encoding_error { + writeln!( + formatter, + "{}\n\n{}", + " Invalid Output ".black().on_bright_red(), + ReadableBytes::from(&result.output), + )?; + + writeln!( + formatter, + "{}\n\n{}", + " JSON Error ".black().on_bright_red(), + e + )?; + } else { + writeln!( + formatter, + "{}\n\n{}", + " Output ".black().on_bright_green(), + ReadableBytes::from(&result.output), + )?; + } + + let input_size_limit = result.scale_factor * DEFAULT_INPUT_SIZE_LIMIT as f64; + let output_size_limit = result.scale_factor * DEFAULT_OUTPUT_SIZE_LIMIT as f64; + let instructions_size_limit = result.scale_factor * DEFAULT_INSTRUCTIONS_LIMIT as f64; + + writeln!( + formatter, + "\n{}\n\n", + " Resource Limits " + .black() + .on_bright_magenta() + )?; + + writeln!( + formatter, + "{}", + humanize_size( + "Input Size", + input_size_limit as u64, + input_size_limit as u64 + ) + )?; + + writeln!( + formatter, + "{}", + humanize_size( + "Output Size", + output_size_limit as u64, + output_size_limit as u64 + ) + )?; + writeln!( + formatter, + "{}", + humanize_instructions( + "Instructions", + instructions_size_limit as u64, + instructions_size_limit as u64 + ) + )?; + + let title = " Benchmark Results " + .black() + .on_truecolor(150, 191, 72); + + write!(formatter, "\n\n{title}\n\n")?; + writeln!(formatter, "Name: {}", result.name)?; + writeln!(formatter, "Linear Memory Usage: {}KB", result.memory_usage)?; + writeln!( + formatter, + "{}", + humanize_instructions( + "Instructions", + result.instructions, + instructions_size_limit as u64 + ) + )?; + writeln!( + formatter, + "{}", + humanize_size( + "Input Size", + result.input_size() as u64, + input_size_limit as u64, + ) + )?; + writeln!( + formatter, + "{}", + humanize_size( + "Output Size", + result.output_size() as u64, + output_size_limit as u64, + ) + )?; + + writeln!(formatter, "Module Size: {}KB\n", result.size)?; + + Ok(()) + } +} + +#[derive(Serialize)] +#[serde(transparent)] +pub struct JsonReport<'a>(&'a FunctionRunResult); + +impl<'a> From<&'a FunctionRunResult> for JsonReport<'a> { + fn from(result: &'a FunctionRunResult) -> Self { + Self(result) + } +} + +impl fmt::Display for JsonReport<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match serde_json::to_string_pretty(self) { + Ok(json) => f.write_str(&json), + Err(error) => write!(f, "{error}"), + } + } +} + +fn humanize_size(title: &str, size_bytes: u64, size_limit: u64) -> String { + let size_humanized = match size_bytes { + 0..=1023 => format!("{}B", size_bytes), + 1024..=1_048_575 => format!("{:.2}KB", size_bytes as f64 / 1024.0), + 1_048_576..=1_073_741_823 => format!("{:.2}MB", size_bytes as f64 / 1_048_576.0), + _ => { + format!("{:.2}GB", size_bytes as f64 / 1_073_741_824.0) + } + }; + + if size_bytes > size_limit { + format!("{}: {}", title, size_humanized).red().to_string() + } else { + format!("{}: {}", title, size_humanized) + } +} + +fn humanize_instructions(title: &str, instructions: u64, instructions_limit: u64) -> String { + let instructions_humanized = match instructions { + 0..=999 => instructions.to_string(), + 1000..=999_999 => format!("{}K", instructions as f64 / 1000.0), + 1_000_000..=999_999_999 => format!("{}M", instructions as f64 / 1_000_000.0), + 1_000_000_000..=u64::MAX => format!("{}B", instructions as f64 / 1_000_000_000.0), + }; + + if instructions > instructions_limit { + format!("{}: {}", title, instructions_humanized) + .red() + .to_string() + } else { + format!("{}: {}", title, instructions_humanized) + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use predicates::prelude::*; + + use crate::{ + function_run_result::FunctionRunResult, BytesContainer, BytesContainerType, Codec, + }; + + 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 input = mock_json_input()?; + + let function_run_result = FunctionRunResult { + name: "test".to_string(), + size: 100, + memory_usage: 1000, + instructions: 1001, + logs: "test".to_string(), + input: input.clone(), + output: json_output(&serde_json::to_vec(&serde_json::json!({ + "test": "test" + }))?)?, + profile: None, + scale_factor: 1.0, + success: true, + }; + + let predicate = predicates::str::contains("Instructions: 1.001K") + .and(predicates::str::contains("Linear Memory Usage: 1000KB")) + .and(predicates::str::contains( + ReadableBytes::from(&input).to_string(), + )) + .and(predicates::str::contains("Input Size: 28B")) + .and(predicates::str::contains("Output Size: 15B")); + assert!(predicate.eval(&TextReport::from(&function_run_result).to_string())); + Ok(()) + } + + #[test] + fn test_js_output_1000() -> Result<()> { + let input = mock_json_input()?; + + let function_run_result = FunctionRunResult { + name: "test".to_string(), + size: 100, + memory_usage: 1000, + instructions: 1000, + logs: "test".to_string(), + input: input.clone(), + output: json_output(&serde_json::to_vec(&serde_json::json!({ + "test": "test" + }))?)?, + profile: None, + scale_factor: 1.0, + success: true, + }; + + let predicate = predicates::str::contains("Instructions: 1") + .and(predicates::str::contains("Linear Memory Usage: 1000KB")) + .and(predicates::str::contains( + ReadableBytes::from(&input).to_string(), + )); + assert!(predicate.eval(&TextReport::from(&function_run_result).to_string())); + Ok(()) + } + + #[test] + fn test_instructions_less_than_1000() -> Result<()> { + let input = mock_json_input()?; + + let function_run_result = FunctionRunResult { + name: "test".to_string(), + size: 100, + memory_usage: 1000, + instructions: 999, + logs: "test".to_string(), + input: input.clone(), + output: json_output(&serde_json::to_vec(&serde_json::json!({ + "test": "test" + }))?)?, + profile: None, + scale_factor: 1.0, + success: true, + }; + + let predicate = predicates::str::contains("Instructions: 999") + .and(predicates::str::contains("Linear Memory Usage: 1000KB")) + .and(predicates::str::contains( + ReadableBytes::from(&input).to_string(), + )); + assert!(predicate.eval(&TextReport::from(&function_run_result).to_string())); + Ok(()) + } + + #[test] + fn readable_bytes_pretty_prints_json() -> Result<()> { + let output = json_output(br#"{"a":{"b":1}}"#)?; + + assert_eq!( + ReadableBytes::from(&output).to_string(), + "{\n \"a\": {\n \"b\": 1\n }\n}" + ); + Ok(()) + } + + #[test] + fn readable_bytes_shows_raw_codec_as_hex() -> Result<()> { + let input = BytesContainer::new( + BytesContainerType::Input, + Codec::Raw, + vec![0x0a, 0xff, 0x00], + )?; + + assert_eq!(ReadableBytes::from(&input).to_string(), "0a ff 00"); + Ok(()) + } + + #[test] + fn readable_bytes_shows_undecodable_output_as_text() -> Result<()> { + let output = json_output(b"not json")?; + + assert!(output.encoding_error.is_some()); + assert_eq!(ReadableBytes::from(&output).to_string(), "not json"); + Ok(()) + } + + #[test] + fn json_report_matches_serde_output() -> Result<()> { + let function_run_result = FunctionRunResult { + name: "test".to_string(), + size: 100, + memory_usage: 1000, + instructions: 1001, + logs: "test".to_string(), + input: mock_json_input()?, + output: json_output(br#"{"test":"test"}"#)?, + profile: None, + scale_factor: 1.0, + success: true, + }; + + assert_eq!( + JsonReport::from(&function_run_result).to_string(), + serde_json::to_string_pretty(&function_run_result)? + ); + Ok(()) + } +}