Skip to content

Commit

Permalink
Add input flavors (raw, json-to-messagepack, and json) (#252)
Browse files Browse the repository at this point in the history
* Add input codecs (raw, json-to-messagepack, and json)
* Don't truncate the log, only warn when the log would be truncated.
  • Loading branch information
makrisoft authored Mar 26, 2024
1 parent 801ceae commit 4cbb9f3
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 140 deletions.
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ serde_json = "1.0"
colored = "2.1"
serde = "1.0"
rust-embed = "8.3.0"
rmp-serde = "1.1"
is-terminal = "0.4.12"
wasmprof = "0.3.0"

Expand Down
105 changes: 60 additions & 45 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,22 @@ fn import_modules(
});
}

pub fn run(
function_path: PathBuf,
input: Vec<u8>,
export: &str,
profile_opts: Option<&ProfileOpts>,
) -> Result<FunctionRunResult> {
#[derive(Default)]
pub struct FunctionRunParams<'a> {
pub function_path: PathBuf,
pub input: Vec<u8>,
pub export: &'a str,
pub profile_opts: Option<&'a ProfileOpts>,
}

pub fn run(params: FunctionRunParams) -> Result<FunctionRunResult> {
let FunctionRunParams {
function_path,
input,
export,
profile_opts,
} = params;

let engine = Engine::new(
Config::new()
.wasm_multi_memory(true)
Expand Down Expand Up @@ -141,8 +151,7 @@ pub fn run(
.try_into_inner()
.expect("Log stream reference still exists");

logs.append(error_logs.as_bytes())
.expect("Couldn't append error logs");
logs.append(error_logs.as_bytes());

let raw_output = output_stream
.try_into_inner()
Expand Down Expand Up @@ -189,50 +198,50 @@ mod tests {
#[test]
fn test_js_function() {
let input = include_bytes!("../tests/fixtures/input/js_function_input.json").to_vec();
let function_run_result = run(
Path::new("tests/fixtures/build/js_function.wasm").to_path_buf(),
let function_run_result = run(FunctionRunParams {
function_path: Path::new("tests/fixtures/build/js_function.wasm").to_path_buf(),
input,
DEFAULT_EXPORT,
None,
);
export: DEFAULT_EXPORT,
..Default::default()
});

assert!(function_run_result.is_ok());
}

#[test]
fn test_exit_code_zero() {
let function_run_result = run(
Path::new("tests/fixtures/build/exit_code.wasm").to_path_buf(),
json!({ "code": 0 }).to_string().into(),
DEFAULT_EXPORT,
None,
)
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(),
export: DEFAULT_EXPORT,
..Default::default()
})
.unwrap();

assert_eq!(function_run_result.logs, "");
}

#[test]
fn test_exit_code_one() {
let function_run_result = run(
Path::new("tests/fixtures/build/exit_code.wasm").to_path_buf(),
json!({ "code": 1 }).to_string().into(),
DEFAULT_EXPORT,
None,
)
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(),
export: DEFAULT_EXPORT,
..Default::default()
})
.unwrap();

assert_eq!(function_run_result.logs, "module exited with code: 1");
}

#[test]
fn test_linear_memory_usage_in_kb() {
let function_run_result = run(
Path::new("tests/fixtures/build/linear_memory.wasm").to_path_buf(),
"{}".as_bytes().to_vec(),
DEFAULT_EXPORT,
None,
)
let function_run_result = run(FunctionRunParams {
function_path: Path::new("tests/fixtures/build/linear_memory.wasm").to_path_buf(),
input: "{}".as_bytes().to_vec(),
export: DEFAULT_EXPORT,
..Default::default()
})
.unwrap();

assert_eq!(function_run_result.memory_usage, 12800); // 200 * 64KiB pages
Expand All @@ -241,29 +250,35 @@ mod tests {
#[test]
fn test_logs_truncation() {
let input = "{}".as_bytes().to_vec();
let function_run_result = run(
Path::new("tests/fixtures/build/log_truncation_function.wasm").to_path_buf(),
let function_run_result = run(FunctionRunParams {
function_path: Path::new("tests/fixtures/build/log_truncation_function.wasm")
.to_path_buf(),
input,
DEFAULT_EXPORT,
None,
)
export: DEFAULT_EXPORT,
..Default::default()
})
.unwrap();

assert!(function_run_result
.logs
.contains(&"...[TRUNCATED]".red().to_string()));
assert!(
function_run_result.to_string().contains(
&"Logs would be truncated in production, length 6000 > 1000 limit"
.red()
.to_string()
),
"Expected logs to be truncated, but were: {function_run_result}"
);
}

#[test]
fn test_file_size_in_kb() {
let file_path = Path::new("tests/fixtures/build/exit_code.wasm");

let function_run_result = run(
file_path.to_path_buf(),
json!({ "code": 0 }).to_string().into(),
DEFAULT_EXPORT,
None,
)
let function_run_result = run(FunctionRunParams {
function_path: file_path.to_path_buf(),
input: json!({ "code": 0 }).to_string().into(),
export: DEFAULT_EXPORT,
..Default::default()
})
.unwrap();

assert_eq!(
Expand Down
13 changes: 13 additions & 0 deletions src/function_run_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::fmt;

const FUNCTION_LOG_LIMIT: usize = 1_000;

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct InvalidOutput {
pub error: String,
Expand Down Expand Up @@ -87,6 +89,17 @@ impl fmt::Display for FunctionRunResult {
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()
)?;
}

match &self.output {
FunctionOutput::JsonOutput(json_output) => {
writeln!(
Expand Down
97 changes: 13 additions & 84 deletions src/logs.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
use core::fmt;
use std::io;

use colored::Colorize;

const MAX_BOUNDED_LOG_BYTESIZE: usize = 1000;

#[derive(Debug)]
pub struct LogStream {
logs: Vec<String>,
capacity: usize, // in bytes
current_bytesize: usize,
}

impl Default for LogStream {
fn default() -> Self {
let capacity = MAX_BOUNDED_LOG_BYTESIZE;
let logs = Vec::new();
let current_bytesize = 0;
Self {
logs,
capacity,
current_bytesize,
}
}
Expand All @@ -36,7 +29,7 @@ impl fmt::Display for LogStream {

impl io::Write for LogStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.append(buf)
Ok(self.append(buf))
}

fn flush(&mut self) -> io::Result<()> {
Expand All @@ -45,46 +38,18 @@ impl io::Write for LogStream {
}

impl LogStream {
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
capacity,
..Default::default()
}
}

/// Append a buffer to the log stream and truncates when hitting the capacity.
/// We return the input buffer size regardless of whether we truncated or not to avoid a panic.
/// Append a buffer to the log stream.
///
/// # Arguments
/// * `buf` - the buffer to append
/// # Returns
/// * `Ok(usize)` - the number of bytes in the buffer that was passed in
/// * `Err(io::Error)` - if the buffer is empty
/// # Errors
/// * `io::Error` - if the buffer is empty
pub fn append(&mut self, buf: &[u8]) -> io::Result<usize> {
if self.current_bytesize > self.capacity {
return Ok(buf.len());
}

if buf.is_empty() {
return Ok(0);
}

pub fn append(&mut self, buf: &[u8]) -> usize {
let log = String::from_utf8_lossy(buf);
let (truncated, log) =
truncate_to_char_boundary(&log, self.capacity - self.current_bytesize);
let mut log = log.to_string();
if truncated {
log.push_str("...[TRUNCATED]".red().to_string().as_str());
}

let size = log.len();
let log_length = log.len();
self.current_bytesize += log_length;
self.logs.push(log.into());

self.current_bytesize += size;
self.logs.push(log);

Ok(buf.len())
log_length
}

#[must_use]
Expand All @@ -98,61 +63,25 @@ impl LogStream {
}
}

// truncate `&str` to length at most equal to `max`
// return `true` if it were truncated, and the new str.
fn truncate_to_char_boundary(s: &str, mut max: usize) -> (bool, &str) {
if max >= s.len() {
(false, s)
} else {
while !s.is_char_boundary(max) {
max -= 1;
}
(true, &s[..max])
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_bounded_log() {
let mut bounded_log = LogStream::with_capacity(15);
let mut bounded_log = LogStream::default();
let log = b"hello world";
bounded_log.append(log).unwrap();
bounded_log.append(log);
assert_eq!(Some("hello world"), bounded_log.last_message());
}

#[test]
fn test_bounded_log_when_truncated() {
let mut bounded_log = LogStream::with_capacity(10);
let log = b"hello world";
bounded_log.append(log).unwrap();
let truncation_message = "...[TRUNCATED]".red().to_string();
assert_eq!(
Some(format!("hello worl{}", truncation_message).as_str()),
bounded_log.last_message()
);
}

#[test]
fn test_bounded_log_when_truncated_nearest_valid_utf8() {
let mut bounded_log = LogStream::with_capacity(15);
bounded_log.append("✌️✌️✌️".as_bytes()).unwrap(); // ✌️ is 6 bytes, ✌ is 3;
let truncation_message = "...[TRUNCATED]".red().to_string();
assert_eq!(
Some(format!("✌\u{fe0f}\u{fe0f}✌{}", truncation_message).as_str()),
bounded_log.last_message()
);
}

#[test]
fn test_display() {
let mut logs = LogStream::with_capacity(10);
let mut logs = LogStream::default();
assert_eq!(String::new(), logs.to_string());

logs.append(b"hello").unwrap();
logs.append(b"world").unwrap();
logs.append(b"hello");
logs.append(b"world");

assert_eq!("helloworld", logs.to_string());
}
Expand Down
Loading

0 comments on commit 4cbb9f3

Please sign in to comment.