From 9cb4587fa5db982592eb386384a3b7a431401066 Mon Sep 17 00:00:00 2001 From: Rainy Sinclair <844493+itsrainy@users.noreply.github.com> Date: Wed, 14 Dec 2022 18:06:58 -0500 Subject: [PATCH 01/10] Add test mode which runs the tests from a wasm produced by cargo test --- cli/Cargo.toml | 1 + cli/src/main.rs | 151 +++++++++++++++++++++++++++++++--- cli/src/opts.rs | 8 ++ lib/src/execute.rs | 197 +++++++++++++++++++++++++++++++++++++++++++++ lib/src/lib.rs | 4 +- lib/src/linking.rs | 4 +- 6 files changed, 351 insertions(+), 14 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0b615ee8..abf35ba5 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -30,6 +30,7 @@ name = "viceroy" path = "src/main.rs" [dependencies] +anyhow = "^1.0.31" hyper = { version = "^0.14.20", features = ["full"] } itertools = "^0.10.5" serde_json = "^1.0.59" diff --git a/cli/src/main.rs b/cli/src/main.rs index 8b6f1a10..1da945cd 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -13,6 +13,10 @@ #![cfg_attr(not(debug_assertions), doc(test(attr(allow(dead_code)))))] #![cfg_attr(not(debug_assertions), doc(test(attr(allow(unused_variables)))))] +use itertools::Itertools; +use std::process::ExitCode; +use viceroy_lib::TestStatus; + mod opts; use { @@ -27,7 +31,9 @@ use { tokio::time::timeout, tracing::{event, Level, Metadata}, tracing_subscriber::{filter::EnvFilter, fmt::writer::MakeWriter, FmtSubscriber}, - viceroy_lib::{config::FastlyConfig, BackendConnector, Error, ExecuteCtx, ViceroyService}, + viceroy_lib::{ + config::FastlyConfig, BackendConnector, Error, ExecuteCtx, TestResult, ViceroyService, + }, }; /// Starts up a Viceroy server. @@ -112,25 +118,119 @@ pub async fn serve(opts: Opts) -> Result<(), Error> { } #[tokio::main] -pub async fn main() -> Result<(), Error> { +pub async fn main() -> ExitCode { // Parse the command-line options, exiting if there are any errors let opts = Opts::parse(); install_tracing_subscriber(&opts); - - tokio::select! { - _ = tokio::signal::ctrl_c() => { - Ok(()) + if opts.test_mode() { + println!("Using Viceroy to run tests..."); + match run_wasm_tests(opts).await { + Ok(_) => ExitCode::SUCCESS, + Err(_) => ExitCode::FAILURE, } - res = serve(opts) => { - if let Err(ref e) = res { - event!(Level::ERROR, "{}", e); + } else { + match { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + Ok(()) + } + res = serve(opts) => { + if let Err(ref e) = res { + event!(Level::ERROR, "{}", e); + } + res + } } - res + } { + Ok(_) => ExitCode::SUCCESS, + Err(_) => ExitCode::FAILURE, } } } +const GREEN_OK: &str = "\x1b[32mok\x1b[0m"; +const RED_FAILED: &str = "\x1b[31mFAILED\x1b[0m"; +const YELLOW_IGNORED: &str = "\x1b[33mignored\x1b[0m"; +/// Execute a Wasm program in the Viceroy environment. +pub async fn run_wasm_tests(opts: Opts) -> Result<(), anyhow::Error> { + // Load the wasm module into an execution context + let ctx = create_execution_context(opts)?; + + // Call the wasm module with the `--list` argument to get test names + let tests = ctx.clone().list_test_names(false).await?; + // Call the wasm module with `--list --ignored`to get ignored tests + let ignored_tests = ctx.clone().list_test_names(true).await?; + + // Run the tests + println!("running {} tests", tests.len()); + let mut results: Vec = Vec::new(); + for test in &tests { + if ignored_tests.contains(test) { + // todo: diff these lists more efficiently + println!("test {} ... {YELLOW_IGNORED}", test); + results.push(TestResult::new( + test.clone(), + TestStatus::IGNORED, + String::new(), + String::new(), + )); + continue; + } + print!("test {} ... ", test); + let result = ctx.clone().execute_test(&test).await?; + print!( + "{}\n", + if result.status == TestStatus::PASSED { + GREEN_OK + } else { + RED_FAILED + } + ); + results.push(result); + } + + print_test_results(results); + Ok(()) +} + +fn print_test_results(results: Vec) { + let counts = results.iter().counts_by(|r| r.status); + let failed = results + .iter() + .filter(|r| r.status == TestStatus::FAILED) + .collect::>(); + + // Get the stderr output for each failing test + let stderr_block = failed + .iter() + .map(|f| format!("---- {} stderr ----\n{}", f.name, f.stderr)) + .join("\n"); + + // Get the list of names of failing tests + let failure_list = failed.iter().map(|f| format!("\t{}", f.name)).join("\n"); + + let result_summary = format!( + "test result: {}. {} passed; {} failed; {} ignored", + if counts.contains_key(&TestStatus::FAILED) { + RED_FAILED + } else { + GREEN_OK + }, + counts.get(&TestStatus::PASSED).unwrap_or(&0), + counts.get(&TestStatus::FAILED).unwrap_or(&0), + counts.get(&TestStatus::IGNORED).unwrap_or(&0) + ); + + if failed.len() > 0 { + print!("\nfailures:\n\n"); + print!("{stderr_block}"); + print!("\nfailures:\n"); + print!("{failure_list}\n"); + } + println!("\n{result_summary}"); +} + fn install_tracing_subscriber(opts: &Opts) { // Default to whatever a user provides, but if not set logging to work for // viceroy and viceroy-lib so that they can have output in the terminal @@ -224,3 +324,34 @@ impl<'a> MakeWriter<'a> for StdWriter { } } } + +fn create_execution_context(opts: Opts) -> Result { + let mut ctx = ExecuteCtx::new(opts.input(), opts.profiling_strategy())? + .with_log_stderr(opts.log_stderr()) + .with_log_stdout(opts.log_stdout()); + if let Some(config_path) = opts.config_path() { + let config = FastlyConfig::from_file(config_path)?; + let backends = config.backends(); + let dictionaries = config.dictionaries(); + let backend_names = itertools::join(backends.keys(), ", "); + + ctx = ctx + .with_backends(backends.clone()) + .with_dictionaries(dictionaries.clone()) + .with_config_path(config_path.into()); + + if backend_names.is_empty() { + event!( + Level::WARN, + "no backend definitions found in {}", + config_path.display() + ); + } + } else { + event!( + Level::WARN, + "no configuration provided, invoke with `-C ` to provide a configuration" + ); + } + Ok(ctx) +} diff --git a/cli/src/opts.rs b/cli/src/opts.rs index a5af89f0..e8b9b426 100644 --- a/cli/src/opts.rs +++ b/cli/src/opts.rs @@ -30,6 +30,9 @@ pub struct Opts { /// The path to a TOML file containing `local_server` configuration. #[arg(short = 'C', long = "config")] config_path: Option, + /// Whether to use Viceroy as a test runner for cargo test + #[arg(short = 't', long = "test", default_value = "false")] + test_mode: bool, /// Whether to treat stdout as a logging endpoint #[arg(long = "log-stdout", default_value = "false")] log_stdout: bool, @@ -66,6 +69,11 @@ impl Opts { self.config_path.as_deref() } + /// Whether to run Viceroy as a test runner + pub fn test_mode(&self) -> bool { + self.test_mode + } + /// Whether to treat stdout as a logging endpoint pub fn log_stdout(&self) -> bool { self.log_stdout diff --git a/lib/src/execute.rs b/lib/src/execute.rs index 03060351..5c586352 100644 --- a/lib/src/execute.rs +++ b/lib/src/execute.rs @@ -1,5 +1,8 @@ //! Guest code execution. +use std::net::Ipv4Addr; + +use anyhow::anyhow; use { crate::{ body::Body, @@ -28,6 +31,31 @@ use { wasmtime::{Engine, InstancePre, Linker, Module, ProfilingStrategy}, }; +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub enum TestStatus { + PASSED, + FAILED, + IGNORED, +} + +pub struct TestResult { + pub name: String, + pub status: TestStatus, + pub stdout: String, + pub stderr: String, +} + +impl TestResult { + pub fn new(name: String, status: TestStatus, stdout: String, stderr: String) -> Self { + Self { + name, + status, + stdout, + stderr, + } + } +} + /// Execution context used by a [`ViceroyService`](struct.ViceroyService.html). /// /// This is all of the state needed to instantiate a module, in order to respond to an HTTP @@ -354,6 +382,175 @@ impl ExecuteCtx { outcome } + + pub async fn list_test_names(self, only_ignored: bool) -> Result, anyhow::Error> { + // We're just using this instance to list the test names, so we can use + // a mock session rather than setting up a bunch of state just to throw + // it away + let session = Session::mock(); + + let mut store = create_store(&self, session).map_err(ExecutionError::Context)?; + store.data_mut().wasi().push_arg("wasm_program")?; + store.data_mut().wasi().push_arg("--list")?; + if only_ignored { + store.data_mut().wasi().push_arg("--ignored")?; + } + + let wp = wasi_common::pipe::WritePipe::new_in_memory(); + let stdout = Box::new(wp.clone()); + store.data_mut().wasi().set_stdout(stdout); + + let instance = self + .instance_pre + .instantiate_async(&mut store) + .await + .map_err(ExecutionError::Instantiation)?; + + // Pull out the `_start` function, which by convention with WASI is the main entry point for + // an application. + let main_func = instance + .get_typed_func::<(), (), _>(&mut store, "_start") + .map_err(ExecutionError::Typechecking)?; + + // Invoke the entrypoint function and collect its exit code + if let Err(trap) = main_func.call_async(&mut store, ()).await { + if let Some(st) = trap.i32_exit_status() { + if st != 0 { + Err(anyhow!("program exited with non-zero exit code: {st}"))? + } + } else { + Err(trap)? + } + }; + // Ensure the downstream response channel is closed, whether or not a response was + // sent during execution. + store.data_mut().close_downstream_response_sender(); + + drop(store); + + let output_string = String::from_utf8( + wp.try_into_inner() + .map_err(|_| anyhow!("multiple references outstanding to WritePipe"))? + .into_inner(), + )?; + let test_names = parse_list_output(output_string)?; + Ok(test_names) + } + + pub async fn execute_test(self, name: &str) -> Result { + // placeholders for request, result sender channel, and remote IP + let req = Request::get("http://example.com/").body(Body::empty())?; + let req_id = 0; + let (sender, _) = oneshot::channel(); + let remote = Ipv4Addr::LOCALHOST.into(); + + let session = Session::new( + req_id, + req, + sender, + remote, + self.backends.clone(), + self.geolocation.clone(), + self.tls_config.clone(), + self.dictionaries.clone(), + self.config_path.clone(), + self.object_store.clone(), + ); + + let mut store = create_store(&self, session).map_err(ExecutionError::Context)?; + store.data_mut().wasi().push_arg("wasm_program")?; + store.data_mut().wasi().push_arg("--exact")?; + store.data_mut().wasi().push_arg("--nocapture")?; + store.data_mut().wasi().push_arg(name)?; + + let out_pipe = wasi_common::pipe::WritePipe::new_in_memory(); + let err_pipe = wasi_common::pipe::WritePipe::new_in_memory(); + let stdout = Box::new(out_pipe.clone()); + let stderr = Box::new(err_pipe.clone()); + store.data_mut().wasi().set_stdout(stdout); + store.data_mut().wasi().set_stderr(stderr); + + let instance = self + .instance_pre + .instantiate_async(&mut store) + .await + .map_err(ExecutionError::Instantiation)?; + + // Pull out the `_start` function, which by convention with WASI is the main entry point for + // an application. + let main_func = instance + .get_typed_func::<(), (), _>(&mut store, "_start") + .map_err(ExecutionError::Typechecking)?; + + // Invoke the entrypoint function and collect its exit code + let test_outcome = main_func.call_async(&mut store, ()).await; + + // Ensure the downstream response channel is closed, whether or not a response was + // sent during execution. + store.data_mut().close_downstream_response_sender(); + + // Drop the store so we can read our stdout and stderr pipes + drop(store); + + let stdout_str = pipe_to_string(out_pipe)?; + let stderr_str = pipe_to_string(err_pipe)?; + + match test_outcome { + Ok(()) => Ok(TestResult::new( + String::from(name), + TestStatus::PASSED, + stdout_str, + stderr_str, + )), + Err(_) => Ok(TestResult::new( + String::from(name), + TestStatus::FAILED, + stdout_str, + stderr_str, + )), + } + } +} + +// This function takes a string in the following format and converts it into a +// list of test names: +// ``` +// test_name_1: test +// test_name_2: test +// test_name_3: test +// +// 5 tests, 0 benchmarks +// ``` +fn parse_list_output(output_string: String) -> Result, anyhow::Error> { + if output_string.starts_with("0 tests") { + return Ok(vec![]); + } + let (list_contents, _) = output_string + .split_once("\n\n") + .ok_or_else(|| anyhow!("expected double newlines in the test's --list output"))?; + let test_names = list_contents + .split("\n") + .map(|line| { + Ok::( + line.split_once(": ") + .ok_or_else(|| anyhow!("expected test line to contain the substring ': '"))? + .0 + .to_string(), + ) + }) + .collect::, _>>()?; + Ok(test_names) +} + +fn pipe_to_string( + pipe: wasi_common::pipe::WritePipe>>, +) -> Result { + let stdout_string = String::from_utf8( + pipe.try_into_inner() + .map_err(|_| anyhow!("multiple references outstanding to WritePipe"))? + .into_inner(), + )?; + Ok(stdout_string) } fn configure_wasmtime(profiling_strategy: ProfilingStrategy) -> wasmtime::Config { diff --git a/lib/src/lib.rs b/lib/src/lib.rs index a2383aff..3dbaa085 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -32,6 +32,6 @@ mod upstream; mod wiggle_abi; pub use { - error::Error, execute::ExecuteCtx, service::ViceroyService, upstream::BackendConnector, - wasmtime::ProfilingStrategy, + error::Error, execute::ExecuteCtx, execute::TestResult, execute::TestStatus, + service::ViceroyService, upstream::BackendConnector, wasmtime::ProfilingStrategy, }; diff --git a/lib/src/linking.rs b/lib/src/linking.rs index 1b3d0e1f..a0609a60 100644 --- a/lib/src/linking.rs +++ b/lib/src/linking.rs @@ -20,7 +20,7 @@ pub struct WasmCtx { } impl WasmCtx { - fn wasi(&mut self) -> &mut WasiCtx { + pub fn wasi(&mut self) -> &mut WasiCtx { &mut self.wasi } @@ -28,7 +28,7 @@ impl WasmCtx { &mut self.wasi_nn } - fn session(&mut self) -> &mut Session { + pub fn session(&mut self) -> &mut Session { &mut self.session } } From 93634e6ca632425ff4c0cdffb4e052f25f70e3b5 Mon Sep 17 00:00:00 2001 From: Rainy Sinclair <844493+itsrainy@users.noreply.github.com> Date: Tue, 17 Jan 2023 11:30:50 -0500 Subject: [PATCH 02/10] Remove test listing and orchestration and just run the _start function of the given binary, passing along any parameters following `--` --- cli/src/main.rs | 137 ++++++++++++++------------------------- cli/src/opts.rs | 17 +++-- lib/src/execute.rs | 156 ++------------------------------------------- lib/src/lib.rs | 4 +- 4 files changed, 69 insertions(+), 245 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 1da945cd..51402f56 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -13,9 +13,7 @@ #![cfg_attr(not(debug_assertions), doc(test(attr(allow(dead_code)))))] #![cfg_attr(not(debug_assertions), doc(test(attr(allow(unused_variables)))))] -use itertools::Itertools; use std::process::ExitCode; -use viceroy_lib::TestStatus; mod opts; @@ -31,9 +29,7 @@ use { tokio::time::timeout, tracing::{event, Level, Metadata}, tracing_subscriber::{filter::EnvFilter, fmt::writer::MakeWriter, FmtSubscriber}, - viceroy_lib::{ - config::FastlyConfig, BackendConnector, Error, ExecuteCtx, TestResult, ViceroyService, - }, + viceroy_lib::{config::FastlyConfig, BackendConnector, Error, ExecuteCtx, ViceroyService}, }; /// Starts up a Viceroy server. @@ -121,11 +117,10 @@ pub async fn serve(opts: Opts) -> Result<(), Error> { pub async fn main() -> ExitCode { // Parse the command-line options, exiting if there are any errors let opts = Opts::parse(); - install_tracing_subscriber(&opts); - if opts.test_mode() { - println!("Using Viceroy to run tests..."); - match run_wasm_tests(opts).await { + if opts.run_mode() { + // println!("Using Viceroy to run tests..."); + match run_wasm_main(opts).await { Ok(_) => ExitCode::SUCCESS, Err(_) => ExitCode::FAILURE, } @@ -149,86 +144,11 @@ pub async fn main() -> ExitCode { } } -const GREEN_OK: &str = "\x1b[32mok\x1b[0m"; -const RED_FAILED: &str = "\x1b[31mFAILED\x1b[0m"; -const YELLOW_IGNORED: &str = "\x1b[33mignored\x1b[0m"; /// Execute a Wasm program in the Viceroy environment. -pub async fn run_wasm_tests(opts: Opts) -> Result<(), anyhow::Error> { +pub async fn run_wasm_main(opts: Opts) -> Result<(), anyhow::Error> { // Load the wasm module into an execution context - let ctx = create_execution_context(opts)?; - - // Call the wasm module with the `--list` argument to get test names - let tests = ctx.clone().list_test_names(false).await?; - // Call the wasm module with `--list --ignored`to get ignored tests - let ignored_tests = ctx.clone().list_test_names(true).await?; - - // Run the tests - println!("running {} tests", tests.len()); - let mut results: Vec = Vec::new(); - for test in &tests { - if ignored_tests.contains(test) { - // todo: diff these lists more efficiently - println!("test {} ... {YELLOW_IGNORED}", test); - results.push(TestResult::new( - test.clone(), - TestStatus::IGNORED, - String::new(), - String::new(), - )); - continue; - } - print!("test {} ... ", test); - let result = ctx.clone().execute_test(&test).await?; - print!( - "{}\n", - if result.status == TestStatus::PASSED { - GREEN_OK - } else { - RED_FAILED - } - ); - results.push(result); - } - - print_test_results(results); - Ok(()) -} - -fn print_test_results(results: Vec) { - let counts = results.iter().counts_by(|r| r.status); - let failed = results - .iter() - .filter(|r| r.status == TestStatus::FAILED) - .collect::>(); - - // Get the stderr output for each failing test - let stderr_block = failed - .iter() - .map(|f| format!("---- {} stderr ----\n{}", f.name, f.stderr)) - .join("\n"); - - // Get the list of names of failing tests - let failure_list = failed.iter().map(|f| format!("\t{}", f.name)).join("\n"); - - let result_summary = format!( - "test result: {}. {} passed; {} failed; {} ignored", - if counts.contains_key(&TestStatus::FAILED) { - RED_FAILED - } else { - GREEN_OK - }, - counts.get(&TestStatus::PASSED).unwrap_or(&0), - counts.get(&TestStatus::FAILED).unwrap_or(&0), - counts.get(&TestStatus::IGNORED).unwrap_or(&0) - ); - - if failed.len() > 0 { - print!("\nfailures:\n\n"); - print!("{stderr_block}"); - print!("\nfailures:\n"); - print!("{failure_list}\n"); - } - println!("\n{result_summary}"); + let ctx = create_execution_context(&opts).await?; + ctx.run_main(opts.run()).await } fn install_tracing_subscriber(opts: &Opts) { @@ -325,19 +245,24 @@ impl<'a> MakeWriter<'a> for StdWriter { } } -fn create_execution_context(opts: Opts) -> Result { +async fn create_execution_context(opts: &Opts) -> Result { let mut ctx = ExecuteCtx::new(opts.input(), opts.profiling_strategy())? .with_log_stderr(opts.log_stderr()) .with_log_stdout(opts.log_stdout()); + if let Some(config_path) = opts.config_path() { let config = FastlyConfig::from_file(config_path)?; let backends = config.backends(); + let geolocation = config.geolocation(); let dictionaries = config.dictionaries(); + let object_store = config.object_store(); let backend_names = itertools::join(backends.keys(), ", "); ctx = ctx .with_backends(backends.clone()) + .with_geolocation(geolocation.clone()) .with_dictionaries(dictionaries.clone()) + .with_object_store(object_store.clone()) .with_config_path(config_path.into()); if backend_names.is_empty() { @@ -347,6 +272,42 @@ fn create_execution_context(opts: Opts) -> Result { config_path.display() ); } + if !opts.run_mode() { + for (name, backend) in backends.iter() { + let client = Client::builder().build(BackendConnector::new( + backend.clone(), + ctx.tls_config().clone(), + )); + let req = Request::get(&backend.uri).body(Body::empty()).unwrap(); + + event!(Level::INFO, "checking if backend '{}' is up", name); + match timeout(Duration::from_secs(5), client.request(req)).await { + // In the case that we don't time out but we have an error, we + // check that it's specifically a connection error as this is + // the only one that happens if the server is not up. + // + // We can't combine this with the case above due to needing the + // inner error to check if it's a connection error. The type + // checker complains about it. + Ok(Err(ref e)) if e.is_connect() => event!( + Level::WARN, + "backend '{}' on '{}' is not up right now", + name, + backend.uri + ), + // In the case we timeout we assume the backend is not up as 5 + // seconds to do a simple get should be enough for a healthy + // service + Err(_) => event!( + Level::WARN, + "backend '{}' on '{}' is not up right now", + name, + backend.uri + ), + Ok(_) => event!(Level::INFO, "backend '{}' is up", name), + } + } + } } else { event!( Level::WARN, diff --git a/cli/src/opts.rs b/cli/src/opts.rs index e8b9b426..d56347e7 100644 --- a/cli/src/opts.rs +++ b/cli/src/opts.rs @@ -30,9 +30,10 @@ pub struct Opts { /// The path to a TOML file containing `local_server` configuration. #[arg(short = 'C', long = "config")] config_path: Option, - /// Whether to use Viceroy as a test runner for cargo test - #[arg(short = 't', long = "test", default_value = "false")] - test_mode: bool, + /// Use Viceroy to run a module's _start function once, rather than in a + /// web server loop + #[arg(short = 'r', long = "run", default_value = "false")] + run_mode: bool, /// Whether to treat stdout as a logging endpoint #[arg(long = "log-stdout", default_value = "false")] log_stdout: bool, @@ -50,6 +51,9 @@ pub struct Opts { /// Set of experimental WASI modules to link against. #[arg(value_enum, long = "experimental_modules", required = false)] experimental_modules: Vec, + // Command line to start child process + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + run: Vec, } impl Opts { @@ -70,8 +74,8 @@ impl Opts { } /// Whether to run Viceroy as a test runner - pub fn test_mode(&self) -> bool { - self.test_mode + pub fn run_mode(&self) -> bool { + self.run_mode } /// Whether to treat stdout as a logging endpoint @@ -96,6 +100,9 @@ impl Opts { self.profiler.unwrap_or(ProfilingStrategy::None) } + pub fn run(&self) -> &[String] { + self.run.as_ref() + } // Set of experimental wasi modules to link against. pub fn wasi_modules(&self) -> HashSet { self.experimental_modules.iter().map(|x| x.into()).collect() diff --git a/lib/src/execute.rs b/lib/src/execute.rs index 5c586352..3fa0b037 100644 --- a/lib/src/execute.rs +++ b/lib/src/execute.rs @@ -31,31 +31,6 @@ use { wasmtime::{Engine, InstancePre, Linker, Module, ProfilingStrategy}, }; -#[derive(Copy, Clone, PartialEq, Eq, Hash)] -pub enum TestStatus { - PASSED, - FAILED, - IGNORED, -} - -pub struct TestResult { - pub name: String, - pub status: TestStatus, - pub stdout: String, - pub stderr: String, -} - -impl TestResult { - pub fn new(name: String, status: TestStatus, stdout: String, stderr: String) -> Self { - Self { - name, - status, - stdout, - stderr, - } - } -} - /// Execution context used by a [`ViceroyService`](struct.ViceroyService.html). /// /// This is all of the state needed to instantiate a module, in order to respond to an HTTP @@ -383,61 +358,7 @@ impl ExecuteCtx { outcome } - pub async fn list_test_names(self, only_ignored: bool) -> Result, anyhow::Error> { - // We're just using this instance to list the test names, so we can use - // a mock session rather than setting up a bunch of state just to throw - // it away - let session = Session::mock(); - - let mut store = create_store(&self, session).map_err(ExecutionError::Context)?; - store.data_mut().wasi().push_arg("wasm_program")?; - store.data_mut().wasi().push_arg("--list")?; - if only_ignored { - store.data_mut().wasi().push_arg("--ignored")?; - } - - let wp = wasi_common::pipe::WritePipe::new_in_memory(); - let stdout = Box::new(wp.clone()); - store.data_mut().wasi().set_stdout(stdout); - - let instance = self - .instance_pre - .instantiate_async(&mut store) - .await - .map_err(ExecutionError::Instantiation)?; - - // Pull out the `_start` function, which by convention with WASI is the main entry point for - // an application. - let main_func = instance - .get_typed_func::<(), (), _>(&mut store, "_start") - .map_err(ExecutionError::Typechecking)?; - - // Invoke the entrypoint function and collect its exit code - if let Err(trap) = main_func.call_async(&mut store, ()).await { - if let Some(st) = trap.i32_exit_status() { - if st != 0 { - Err(anyhow!("program exited with non-zero exit code: {st}"))? - } - } else { - Err(trap)? - } - }; - // Ensure the downstream response channel is closed, whether or not a response was - // sent during execution. - store.data_mut().close_downstream_response_sender(); - - drop(store); - - let output_string = String::from_utf8( - wp.try_into_inner() - .map_err(|_| anyhow!("multiple references outstanding to WritePipe"))? - .into_inner(), - )?; - let test_names = parse_list_output(output_string)?; - Ok(test_names) - } - - pub async fn execute_test(self, name: &str) -> Result { + pub async fn run_main(self, args: &[String]) -> Result<(), anyhow::Error> { // placeholders for request, result sender channel, and remote IP let req = Request::get("http://example.com/").body(Body::empty())?; let req_id = 0; @@ -459,16 +380,9 @@ impl ExecuteCtx { let mut store = create_store(&self, session).map_err(ExecutionError::Context)?; store.data_mut().wasi().push_arg("wasm_program")?; - store.data_mut().wasi().push_arg("--exact")?; - store.data_mut().wasi().push_arg("--nocapture")?; - store.data_mut().wasi().push_arg(name)?; - - let out_pipe = wasi_common::pipe::WritePipe::new_in_memory(); - let err_pipe = wasi_common::pipe::WritePipe::new_in_memory(); - let stdout = Box::new(out_pipe.clone()); - let stderr = Box::new(err_pipe.clone()); - store.data_mut().wasi().set_stdout(stdout); - store.data_mut().wasi().set_stderr(stderr); + for arg in args { + store.data_mut().wasi().push_arg(arg)?; + } let instance = self .instance_pre @@ -488,71 +402,13 @@ impl ExecuteCtx { // Ensure the downstream response channel is closed, whether or not a response was // sent during execution. store.data_mut().close_downstream_response_sender(); - - // Drop the store so we can read our stdout and stderr pipes - drop(store); - - let stdout_str = pipe_to_string(out_pipe)?; - let stderr_str = pipe_to_string(err_pipe)?; - match test_outcome { - Ok(()) => Ok(TestResult::new( - String::from(name), - TestStatus::PASSED, - stdout_str, - stderr_str, - )), - Err(_) => Ok(TestResult::new( - String::from(name), - TestStatus::FAILED, - stdout_str, - stderr_str, - )), + Ok(_) => Ok(()), + Err(_) => Err(anyhow!("Error running _start")), } } } -// This function takes a string in the following format and converts it into a -// list of test names: -// ``` -// test_name_1: test -// test_name_2: test -// test_name_3: test -// -// 5 tests, 0 benchmarks -// ``` -fn parse_list_output(output_string: String) -> Result, anyhow::Error> { - if output_string.starts_with("0 tests") { - return Ok(vec![]); - } - let (list_contents, _) = output_string - .split_once("\n\n") - .ok_or_else(|| anyhow!("expected double newlines in the test's --list output"))?; - let test_names = list_contents - .split("\n") - .map(|line| { - Ok::( - line.split_once(": ") - .ok_or_else(|| anyhow!("expected test line to contain the substring ': '"))? - .0 - .to_string(), - ) - }) - .collect::, _>>()?; - Ok(test_names) -} - -fn pipe_to_string( - pipe: wasi_common::pipe::WritePipe>>, -) -> Result { - let stdout_string = String::from_utf8( - pipe.try_into_inner() - .map_err(|_| anyhow!("multiple references outstanding to WritePipe"))? - .into_inner(), - )?; - Ok(stdout_string) -} - fn configure_wasmtime(profiling_strategy: ProfilingStrategy) -> wasmtime::Config { use wasmtime::{ Config, InstanceAllocationStrategy, PoolingAllocationConfig, PoolingAllocationStrategy, diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 3dbaa085..a2383aff 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -32,6 +32,6 @@ mod upstream; mod wiggle_abi; pub use { - error::Error, execute::ExecuteCtx, execute::TestResult, execute::TestStatus, - service::ViceroyService, upstream::BackendConnector, wasmtime::ProfilingStrategy, + error::Error, execute::ExecuteCtx, service::ViceroyService, upstream::BackendConnector, + wasmtime::ProfilingStrategy, }; From 609b550227937985b94f97ad146637cfcee6ad7a Mon Sep 17 00:00:00 2001 From: Rainy Sinclair <844493+itsrainy@users.noreply.github.com> Date: Tue, 17 Jan 2023 11:43:12 -0500 Subject: [PATCH 03/10] Add quiet mode for now --- cli/src/main.rs | 5 +++++ cli/src/opts.rs | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/cli/src/main.rs b/cli/src/main.rs index 51402f56..70454d93 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -165,6 +165,11 @@ fn install_tracing_subscriber(opts: &Opts) { } } } + // If the quiet flag is passed in, don't log anything (this should maybe + // just be a verbosity setting) + if opts.quiet() { + env::set_var("RUST_LOG", "viceroy=off,viceroy-lib=off"); + } // Build a subscriber, using the default `RUST_LOG` environment variable for our filter. let builder = FmtSubscriber::builder() .with_writer(StdWriter::new()) diff --git a/cli/src/opts.rs b/cli/src/opts.rs index d56347e7..67ea9a6e 100644 --- a/cli/src/opts.rs +++ b/cli/src/opts.rs @@ -51,6 +51,9 @@ pub struct Opts { /// Set of experimental WASI modules to link against. #[arg(value_enum, long = "experimental_modules", required = false)] experimental_modules: Vec, + /// Don't log viceroy events to stdout or stderr + #[arg(short = 'q', long = "quiet", default_value = "false")] + quiet: bool, // Command line to start child process #[arg(trailing_var_arg = true, allow_hyphen_values = true)] run: Vec, @@ -103,6 +106,10 @@ impl Opts { pub fn run(&self) -> &[String] { self.run.as_ref() } + pub fn quiet(&self) -> bool { + self.quiet + } + // Set of experimental wasi modules to link against. pub fn wasi_modules(&self) -> HashSet { self.experimental_modules.iter().map(|x| x.into()).collect() From d582825adc6ecbdf7fadb654c2a2845144303f8c Mon Sep 17 00:00:00 2001 From: Rainy Sinclair <844493+itsrainy@users.noreply.github.com> Date: Wed, 18 Jan 2023 14:25:14 -0500 Subject: [PATCH 04/10] Rebased --- cli/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 70454d93..16385b45 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -251,7 +251,7 @@ impl<'a> MakeWriter<'a> for StdWriter { } async fn create_execution_context(opts: &Opts) -> Result { - let mut ctx = ExecuteCtx::new(opts.input(), opts.profiling_strategy())? + let mut ctx = ExecuteCtx::new(opts.input(), opts.profiling_strategy(), opts.wasi_modules())? .with_log_stderr(opts.log_stderr()) .with_log_stdout(opts.log_stdout()); From 38e8de7c866ef81643b9e06ff84f12d7fe02965c Mon Sep 17 00:00:00 2001 From: Rainy Sinclair <844493+itsrainy@users.noreply.github.com> Date: Thu, 19 Jan 2023 16:35:10 -0500 Subject: [PATCH 05/10] Cleanup doc comments and add tests --- cli/src/main.rs | 2 +- cli/src/opts.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 16385b45..11802502 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -148,7 +148,7 @@ pub async fn main() -> ExitCode { pub async fn run_wasm_main(opts: Opts) -> Result<(), anyhow::Error> { // Load the wasm module into an execution context let ctx = create_execution_context(&opts).await?; - ctx.run_main(opts.run()).await + ctx.run_main(opts.wasm_args()).await } fn install_tracing_subscriber(opts: &Opts) { diff --git a/cli/src/opts.rs b/cli/src/opts.rs index 67ea9a6e..664e039e 100644 --- a/cli/src/opts.rs +++ b/cli/src/opts.rs @@ -54,9 +54,10 @@ pub struct Opts { /// Don't log viceroy events to stdout or stderr #[arg(short = 'q', long = "quiet", default_value = "false")] quiet: bool, - // Command line to start child process + /// Args to pass along to the binary being executed. This is only used when + /// run_mode=true #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - run: Vec, + wasm_args: Vec, } impl Opts { @@ -76,7 +77,7 @@ impl Opts { self.config_path.as_deref() } - /// Whether to run Viceroy as a test runner + /// Whether Viceroy should run the input once and then exit pub fn run_mode(&self) -> bool { self.run_mode } @@ -103,9 +104,13 @@ impl Opts { self.profiler.unwrap_or(ProfilingStrategy::None) } - pub fn run(&self) -> &[String] { - self.run.as_ref() + /// The arguments to pass to the underlying binary when run_mode=true + pub fn wasm_args(&self) -> &[String] { + self.wasm_args.as_ref() } + + /// Prevents Viceroy from logging to stdout and stderr (note: any logs + /// emitted by the INPUT program will still go to stdout/stderr) pub fn quiet(&self) -> bool { self.quiet } @@ -349,4 +354,39 @@ mod opts_tests { Err(_) => Ok(()), } } + + /// Test that trailing arguments are collected successfully + #[test] + fn trailing_args_are_collected() -> TestResult { + let args = &[ + "dummy-program-name", + &test_file("minimal.wat"), + "--", + "--trailing-arg", + "--trailing-arg-2", + ]; + let opts = Opts::try_parse_from(args)?; + assert_eq!(opts.wasm_args(), &["--trailing-arg", "--trailing-arg-2"]); + Ok(()) + } + + /// Input is still accepted after double-dash. This is how the input will be + /// passed by cargo nextest if using Viceroy in run-mode to run tests + #[test] + fn input_accepted_after_double_dash() -> TestResult { + let args = &[ + "dummy-program-name", + "--", + &test_file("minimal.wat"), + "--trailing-arg", + "--trailing-arg-2", + ]; + let opts = match Opts::try_parse_from(args) { + Ok(opts) => opts, + res => panic!("unexpected result: {:?}", res), + }; + assert_eq!(opts.input().to_str().unwrap(), &test_file("minimal.wat")); + assert_eq!(opts.wasm_args(), &["--trailing-arg", "--trailing-arg-2"]); + Ok(()) + } } From 17ac8c990b5d4d40b4f857a4f96a1c0bde45d882 Mon Sep 17 00:00:00 2001 From: Rainy Sinclair <844493+itsrainy@users.noreply.github.com> Date: Thu, 19 Jan 2023 16:59:34 -0500 Subject: [PATCH 06/10] Fix wasmtime invocation to work with new version --- lib/src/execute.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/execute.rs b/lib/src/execute.rs index 3fa0b037..3dc38581 100644 --- a/lib/src/execute.rs +++ b/lib/src/execute.rs @@ -393,7 +393,7 @@ impl ExecuteCtx { // Pull out the `_start` function, which by convention with WASI is the main entry point for // an application. let main_func = instance - .get_typed_func::<(), (), _>(&mut store, "_start") + .get_typed_func::<(), ()>(&mut store, "_start") .map_err(ExecutionError::Typechecking)?; // Invoke the entrypoint function and collect its exit code From 5f87e0a72b8069967251941486e42806408b0140 Mon Sep 17 00:00:00 2001 From: Rainy Sinclair <844493+itsrainy@users.noreply.github.com> Date: Fri, 20 Jan 2023 15:37:11 -0500 Subject: [PATCH 07/10] remove duplicate code --- cli/src/main.rs | 72 ++-------------------------------------------- lib/src/execute.rs | 1 + 2 files changed, 4 insertions(+), 69 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 11802502..9900e32a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -37,75 +37,7 @@ use { /// Create a new server, bind it to an address, and serve responses until an error occurs. pub async fn serve(opts: Opts) -> Result<(), Error> { // Load the wasm module into an execution context - let mut ctx = ExecuteCtx::new(opts.input(), opts.profiling_strategy(), opts.wasi_modules())? - .with_log_stderr(opts.log_stderr()) - .with_log_stdout(opts.log_stdout()); - - if let Some(config_path) = opts.config_path() { - let config = FastlyConfig::from_file(config_path)?; - let backends = config.backends(); - let geolocation = config.geolocation(); - let dictionaries = config.dictionaries(); - let object_store = config.object_store(); - let secret_stores = config.secret_stores(); - let backend_names = itertools::join(backends.keys(), ", "); - - ctx = ctx - .with_backends(backends.clone()) - .with_geolocation(geolocation.clone()) - .with_dictionaries(dictionaries.clone()) - .with_object_store(object_store.clone()) - .with_secret_stores(secret_stores.clone()) - .with_config_path(config_path.into()); - - if backend_names.is_empty() { - event!( - Level::WARN, - "no backend definitions found in {}", - config_path.display() - ); - } - - for (name, backend) in backends.iter() { - let client = Client::builder().build(BackendConnector::new( - backend.clone(), - ctx.tls_config().clone(), - )); - let req = Request::get(&backend.uri).body(Body::empty()).unwrap(); - - event!(Level::INFO, "checking if backend '{}' is up", name); - match timeout(Duration::from_secs(5), client.request(req)).await { - // In the case that we don't time out but we have an error, we - // check that it's specifically a connection error as this is - // the only one that happens if the server is not up. - // - // We can't combine this with the case above due to needing the - // inner error to check if it's a connection error. The type - // checker complains about it. - Ok(Err(ref e)) if e.is_connect() => event!( - Level::WARN, - "backend '{}' on '{}' is not up right now", - name, - backend.uri - ), - // In the case we timeout we assume the backend is not up as 5 - // seconds to do a simple get should be enough for a healthy - // service - Err(_) => event!( - Level::WARN, - "backend '{}' on '{}' is not up right now", - name, - backend.uri - ), - Ok(_) => event!(Level::INFO, "backend '{}' is up", name), - } - } - } else { - event!( - Level::WARN, - "no configuration provided, invoke with `-C ` to provide a configuration" - ); - } + let ctx = create_execution_context(&opts).await?; let addr = opts.addr(); ViceroyService::new(ctx).serve(addr).await?; @@ -261,6 +193,7 @@ async fn create_execution_context(opts: &Opts) -> Result Result Date: Fri, 27 Jan 2023 14:08:35 -0500 Subject: [PATCH 08/10] Pass actual filename to wasi --- cli/src/main.rs | 7 +++++-- lib/src/execute.rs | 13 +++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 9900e32a..fc743215 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -51,7 +51,6 @@ pub async fn main() -> ExitCode { let opts = Opts::parse(); install_tracing_subscriber(&opts); if opts.run_mode() { - // println!("Using Viceroy to run tests..."); match run_wasm_main(opts).await { Ok(_) => ExitCode::SUCCESS, Err(_) => ExitCode::FAILURE, @@ -80,7 +79,11 @@ pub async fn main() -> ExitCode { pub async fn run_wasm_main(opts: Opts) -> Result<(), anyhow::Error> { // Load the wasm module into an execution context let ctx = create_execution_context(&opts).await?; - ctx.run_main(opts.wasm_args()).await + ctx.run_main( + opts.input().file_stem().unwrap().to_str().unwrap(), + opts.wasm_args(), + ) + .await } fn install_tracing_subscriber(opts: &Opts) { diff --git a/lib/src/execute.rs b/lib/src/execute.rs index e4aa0bc2..5747bc76 100644 --- a/lib/src/execute.rs +++ b/lib/src/execute.rs @@ -2,7 +2,6 @@ use std::net::Ipv4Addr; -use anyhow::anyhow; use { crate::{ body::Body, @@ -358,7 +357,7 @@ impl ExecuteCtx { outcome } - pub async fn run_main(self, args: &[String]) -> Result<(), anyhow::Error> { + pub async fn run_main(self, program_name: &str, args: &[String]) -> Result<(), anyhow::Error> { // placeholders for request, result sender channel, and remote IP let req = Request::get("http://example.com/").body(Body::empty())?; let req_id = 0; @@ -380,7 +379,7 @@ impl ExecuteCtx { ); let mut store = create_store(&self, session).map_err(ExecutionError::Context)?; - store.data_mut().wasi().push_arg("wasm_program")?; + store.data_mut().wasi().push_arg(program_name)?; for arg in args { store.data_mut().wasi().push_arg(arg)?; } @@ -398,15 +397,13 @@ impl ExecuteCtx { .map_err(ExecutionError::Typechecking)?; // Invoke the entrypoint function and collect its exit code - let test_outcome = main_func.call_async(&mut store, ()).await; + let result = main_func.call_async(&mut store, ()).await; // Ensure the downstream response channel is closed, whether or not a response was // sent during execution. store.data_mut().close_downstream_response_sender(); - match test_outcome { - Ok(_) => Ok(()), - Err(_) => Err(anyhow!("Error running _start")), - } + + result } } From 8a78eadb2b81e4373054e303c0eb66204bac23f3 Mon Sep 17 00:00:00 2001 From: Rainy Sinclair <844493+itsrainy@users.noreply.github.com> Date: Fri, 27 Jan 2023 14:08:58 -0500 Subject: [PATCH 09/10] Mark run-mode args as experimental and hide from --help for now --- cli/src/opts.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cli/src/opts.rs b/cli/src/opts.rs index 664e039e..51865f44 100644 --- a/cli/src/opts.rs +++ b/cli/src/opts.rs @@ -30,9 +30,10 @@ pub struct Opts { /// The path to a TOML file containing `local_server` configuration. #[arg(short = 'C', long = "config")] config_path: Option, - /// Use Viceroy to run a module's _start function once, rather than in a - /// web server loop - #[arg(short = 'r', long = "run", default_value = "false")] + /// [EXPERIMENTAL] Use Viceroy to run a module's _start function once, + /// rather than in a web server loop. This is experimental and the specific + /// interface for this is going to change in the near future. + #[arg(short = 'r', long = "run", default_value = "false", hide = true)] run_mode: bool, /// Whether to treat stdout as a logging endpoint #[arg(long = "log-stdout", default_value = "false")] @@ -54,9 +55,9 @@ pub struct Opts { /// Don't log viceroy events to stdout or stderr #[arg(short = 'q', long = "quiet", default_value = "false")] quiet: bool, - /// Args to pass along to the binary being executed. This is only used when - /// run_mode=true - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + /// [EXPERIMENTAL] Args to pass along to the binary being executed. This is + /// only used when run_mode=true + #[arg(trailing_var_arg = true, allow_hyphen_values = true, hide = true)] wasm_args: Vec, } From e527c8c8bf265b0e064dc27b56e62c66659ae3ed Mon Sep 17 00:00:00 2001 From: Rainy Sinclair <844493+itsrainy@users.noreply.github.com> Date: Mon, 30 Jan 2023 17:19:51 -0500 Subject: [PATCH 10/10] Use to_string_lossy to get filename rather than to_str --- cli/src/main.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index fc743215..e6718a2e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -79,11 +79,11 @@ pub async fn main() -> ExitCode { pub async fn run_wasm_main(opts: Opts) -> Result<(), anyhow::Error> { // Load the wasm module into an execution context let ctx = create_execution_context(&opts).await?; - ctx.run_main( - opts.input().file_stem().unwrap().to_str().unwrap(), - opts.wasm_args(), - ) - .await + let program_name = match opts.input().file_stem() { + Some(stem) => stem.to_string_lossy(), + None => panic!("program cannot be a directory"), + }; + ctx.run_main(&program_name, opts.wasm_args()).await } fn install_tracing_subscriber(opts: &Opts) {