diff options
author | Asher Gomez <ashersaupingomez@gmail.com> | 2024-02-20 00:34:24 +1100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-19 06:34:24 -0700 |
commit | 2b279ad630651e973d5a31586f58809f005bc925 (patch) | |
tree | 3e3cbeb4126643c75381dd5422e8603a7488bb8a /tests/util/server/src/lib.rs | |
parent | eb542bc185c6c4ce1847417a2dfdf04862cd86db (diff) |
chore: move `test_util` to `tests/util/server` (#22444)
As discussed with @mmastrac.
---------
Signed-off-by: Asher Gomez <ashersaupingomez@gmail.com>
Co-authored-by: Matt Mastracci <matthew@mastracci.com>
Diffstat (limited to 'tests/util/server/src/lib.rs')
-rw-r--r-- | tests/util/server/src/lib.rs | 1277 |
1 files changed, 1277 insertions, 0 deletions
diff --git a/tests/util/server/src/lib.rs b/tests/util/server/src/lib.rs new file mode 100644 index 000000000..65dfe61ec --- /dev/null +++ b/tests/util/server/src/lib.rs @@ -0,0 +1,1277 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// Usage: provide a port as argument to run hyper_hello benchmark server +// otherwise this starts multiple servers on many ports for test endpoints. +use futures::FutureExt; +use futures::Stream; +use futures::StreamExt; +use once_cell::sync::Lazy; +use pretty_assertions::assert_eq; +use pty::Pty; +use regex::Regex; +use serde::Serialize; +use std::collections::HashMap; +use std::env; +use std::io::Write; +use std::path::PathBuf; +use std::process::Child; +use std::process::Command; +use std::process::Output; +use std::process::Stdio; +use std::result::Result; +use std::sync::Mutex; +use std::sync::MutexGuard; +use tokio::net::TcpStream; +use url::Url; + +pub mod assertions; +mod builders; +pub mod factory; +mod fs; +mod https; +pub mod lsp; +mod macros; +mod npm; +pub mod pty; +pub mod servers; +pub mod spawn; + +pub use builders::DenoChild; +pub use builders::TestCommandBuilder; +pub use builders::TestCommandOutput; +pub use builders::TestContext; +pub use builders::TestContextBuilder; +pub use fs::PathRef; +pub use fs::TempDir; + +pub const PERMISSION_VARIANTS: [&str; 5] = + ["read", "write", "env", "net", "run"]; +pub const PERMISSION_DENIED_PATTERN: &str = "PermissionDenied"; + +static GUARD: Lazy<Mutex<HttpServerCount>> = + Lazy::new(|| Mutex::new(HttpServerCount::default())); + +pub fn env_vars_for_npm_tests() -> Vec<(String, String)> { + vec![ + ("NPM_CONFIG_REGISTRY".to_string(), npm_registry_url()), + ("NO_COLOR".to_string(), "1".to_string()), + ] +} + +pub fn env_vars_for_jsr_tests() -> Vec<(String, String)> { + vec![ + ("JSR_URL".to_string(), jsr_registry_url()), + ("NO_COLOR".to_string(), "1".to_string()), + ] +} + +pub fn root_path() -> PathRef { + PathRef::new( + PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"))) + .parent() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap(), + ) +} + +pub fn prebuilt_path() -> PathRef { + third_party_path().join("prebuilt") +} + +pub fn tests_path() -> PathRef { + root_path().join("tests") +} + +pub fn testdata_path() -> PathRef { + tests_path().join("testdata") +} + +pub fn third_party_path() -> PathRef { + root_path().join("third_party") +} + +pub fn ffi_tests_path() -> PathRef { + root_path().join("tests").join("ffi") +} + +pub fn napi_tests_path() -> PathRef { + root_path().join("tests").join("napi") +} + +pub fn deno_config_path() -> PathRef { + root_path().join("tests").join("config").join("deno.json") +} + +/// Test server registry url. +pub fn npm_registry_url() -> String { + "http://localhost:4545/npm/registry/".to_string() +} + +pub fn npm_registry_unset_url() -> String { + "http://NPM_CONFIG_REGISTRY.is.unset".to_string() +} + +pub fn jsr_registry_url() -> String { + "http://127.0.0.1:4250/".to_string() +} + +pub fn jsr_registry_unset_url() -> String { + "http://JSR_URL.is.unset".to_string() +} + +pub fn std_path() -> PathRef { + root_path().join("tests").join("util").join("std") +} + +pub fn std_file_url() -> String { + Url::from_directory_path(std_path()).unwrap().to_string() +} + +pub fn target_dir() -> PathRef { + let current_exe = std::env::current_exe().unwrap(); + let target_dir = current_exe.parent().unwrap().parent().unwrap(); + PathRef::new(target_dir) +} + +pub fn deno_exe_path() -> PathRef { + // Something like /Users/rld/src/deno/target/debug/deps/deno + let mut p = target_dir().join("deno").to_path_buf(); + if cfg!(windows) { + p.set_extension("exe"); + } + PathRef::new(p) +} + +pub fn denort_exe_path() -> PathRef { + let mut p = target_dir().join("denort").to_path_buf(); + if cfg!(windows) { + p.set_extension("exe"); + } + PathRef::new(p) +} + +pub fn prebuilt_tool_path(tool: &str) -> PathRef { + let mut exe = tool.to_string(); + exe.push_str(if cfg!(windows) { ".exe" } else { "" }); + prebuilt_path().join(platform_dir_name()).join(exe) +} + +pub fn platform_dir_name() -> &'static str { + if cfg!(target_os = "linux") { + "linux64" + } else if cfg!(target_os = "macos") { + "mac" + } else if cfg!(target_os = "windows") { + "win" + } else { + unreachable!() + } +} + +pub fn test_server_path() -> PathBuf { + let mut p = target_dir().join("test_server").to_path_buf(); + if cfg!(windows) { + p.set_extension("exe"); + } + p +} + +fn ensure_test_server_built() { + // if the test server doesn't exist then remind the developer to build first + if !test_server_path().exists() { + panic!( + "Test server not found. Please cargo build before running the tests." + ); + } +} + +/// Returns a [`Stream`] of [`TcpStream`]s accepted from the given port. +async fn get_tcp_listener_stream( + name: &'static str, + port: u16, +) -> impl Stream<Item = Result<TcpStream, std::io::Error>> + Unpin + Send { + let host_and_port = &format!("localhost:{port}"); + + // Listen on ALL addresses that localhost can resolves to. + let accept = |listener: tokio::net::TcpListener| { + async { + let result = listener.accept().await; + Some((result.map(|r| r.0), listener)) + } + .boxed() + }; + + let mut addresses = vec![]; + let listeners = tokio::net::lookup_host(host_and_port) + .await + .expect(host_and_port) + .inspect(|address| addresses.push(*address)) + .map(tokio::net::TcpListener::bind) + .collect::<futures::stream::FuturesUnordered<_>>() + .collect::<Vec<_>>() + .await + .into_iter() + .map(|s| s.unwrap()) + .map(|listener| futures::stream::unfold(listener, accept)) + .collect::<Vec<_>>(); + + // Eye catcher for HttpServerCount + println!("ready: {name} on {:?}", addresses); + + futures::stream::select_all(listeners) +} + +#[derive(Default)] +struct HttpServerCount { + count: usize, + test_server: Option<Child>, +} + +impl HttpServerCount { + fn inc(&mut self) { + self.count += 1; + if self.test_server.is_none() { + assert_eq!(self.count, 1); + + println!("test_server starting..."); + let mut test_server = Command::new(test_server_path()) + .current_dir(testdata_path()) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to execute test_server"); + let stdout = test_server.stdout.as_mut().unwrap(); + use std::io::BufRead; + use std::io::BufReader; + let lines = BufReader::new(stdout).lines(); + + // Wait for all the servers to report being ready. + let mut ready_count = 0; + for maybe_line in lines { + if let Ok(line) = maybe_line { + if line.starts_with("ready:") { + ready_count += 1; + } + if ready_count == 12 { + break; + } + } else { + panic!("{}", maybe_line.unwrap_err()); + } + } + self.test_server = Some(test_server); + } + } + + fn dec(&mut self) { + assert!(self.count > 0); + self.count -= 1; + if self.count == 0 { + let mut test_server = self.test_server.take().unwrap(); + match test_server.try_wait() { + Ok(None) => { + test_server.kill().expect("failed to kill test_server"); + let _ = test_server.wait(); + } + Ok(Some(status)) => { + panic!("test_server exited unexpectedly {status}") + } + Err(e) => panic!("test_server error: {e}"), + } + } + } +} + +impl Drop for HttpServerCount { + fn drop(&mut self) { + assert_eq!(self.count, 0); + assert!(self.test_server.is_none()); + } +} + +fn lock_http_server<'a>() -> MutexGuard<'a, HttpServerCount> { + let r = GUARD.lock(); + if let Err(poison_err) = r { + // If panics happened, ignore it. This is for tests. + poison_err.into_inner() + } else { + r.unwrap() + } +} + +pub struct HttpServerGuard {} + +impl Drop for HttpServerGuard { + fn drop(&mut self) { + let mut g = lock_http_server(); + g.dec(); + } +} + +/// Adds a reference to a shared target/debug/test_server subprocess. When the +/// last instance of the HttpServerGuard is dropped, the subprocess will be +/// killed. +pub fn http_server() -> HttpServerGuard { + ensure_test_server_built(); + let mut g = lock_http_server(); + g.inc(); + HttpServerGuard {} +} + +/// Helper function to strip ansi codes. +pub fn strip_ansi_codes(s: &str) -> std::borrow::Cow<str> { + console_static_text::ansi::strip_ansi_codes(s) +} + +pub fn run( + cmd: &[&str], + input: Option<&[&str]>, + envs: Option<Vec<(String, String)>>, + current_dir: Option<&str>, + expect_success: bool, +) { + let mut process_builder = Command::new(cmd[0]); + process_builder.args(&cmd[1..]).stdin(Stdio::piped()); + + if let Some(dir) = current_dir { + process_builder.current_dir(dir); + } + if let Some(envs) = envs { + process_builder.envs(envs); + } + let mut prog = process_builder.spawn().expect("failed to spawn script"); + if let Some(lines) = input { + let stdin = prog.stdin.as_mut().expect("failed to get stdin"); + stdin + .write_all(lines.join("\n").as_bytes()) + .expect("failed to write to stdin"); + } + let status = prog.wait().expect("failed to wait on child"); + if expect_success != status.success() { + panic!("Unexpected exit code: {:?}", status.code()); + } +} + +pub fn run_collect( + cmd: &[&str], + input: Option<&[&str]>, + envs: Option<Vec<(String, String)>>, + current_dir: Option<&str>, + expect_success: bool, +) -> (String, String) { + let mut process_builder = Command::new(cmd[0]); + process_builder + .args(&cmd[1..]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if let Some(dir) = current_dir { + process_builder.current_dir(dir); + } + if let Some(envs) = envs { + process_builder.envs(envs); + } + let mut prog = process_builder.spawn().expect("failed to spawn script"); + if let Some(lines) = input { + let stdin = prog.stdin.as_mut().expect("failed to get stdin"); + stdin + .write_all(lines.join("\n").as_bytes()) + .expect("failed to write to stdin"); + } + let Output { + stdout, + stderr, + status, + } = prog.wait_with_output().expect("failed to wait on child"); + let stdout = String::from_utf8(stdout).unwrap(); + let stderr = String::from_utf8(stderr).unwrap(); + if expect_success != status.success() { + eprintln!("stdout: <<<{stdout}>>>"); + eprintln!("stderr: <<<{stderr}>>>"); + panic!("Unexpected exit code: {:?}", status.code()); + } + (stdout, stderr) +} + +pub fn run_and_collect_output( + expect_success: bool, + args: &str, + input: Option<Vec<&str>>, + envs: Option<Vec<(String, String)>>, + need_http_server: bool, +) -> (String, String) { + run_and_collect_output_with_args( + expect_success, + args.split_whitespace().collect(), + input, + envs, + need_http_server, + ) +} + +pub fn run_and_collect_output_with_args( + expect_success: bool, + args: Vec<&str>, + input: Option<Vec<&str>>, + envs: Option<Vec<(String, String)>>, + need_http_server: bool, +) -> (String, String) { + let mut deno_process_builder = deno_cmd() + .args_vec(args) + .current_dir(testdata_path()) + .stdin(Stdio::piped()) + .piped_output(); + if let Some(envs) = envs { + deno_process_builder = deno_process_builder.envs(envs); + } + let _http_guard = if need_http_server { + Some(http_server()) + } else { + None + }; + let mut deno = deno_process_builder + .spawn() + .expect("failed to spawn script"); + if let Some(lines) = input { + let stdin = deno.stdin.as_mut().expect("failed to get stdin"); + stdin + .write_all(lines.join("\n").as_bytes()) + .expect("failed to write to stdin"); + } + let Output { + stdout, + stderr, + status, + } = deno.wait_with_output().expect("failed to wait on child"); + let stdout = String::from_utf8(stdout).unwrap(); + let stderr = String::from_utf8(stderr).unwrap(); + if expect_success != status.success() { + eprintln!("stdout: <<<{stdout}>>>"); + eprintln!("stderr: <<<{stderr}>>>"); + panic!("Unexpected exit code: {:?}", status.code()); + } + (stdout, stderr) +} + +pub fn new_deno_dir() -> TempDir { + TempDir::new() +} + +pub fn deno_cmd() -> TestCommandBuilder { + let deno_dir = new_deno_dir(); + deno_cmd_with_deno_dir(&deno_dir) +} + +pub fn deno_cmd_with_deno_dir(deno_dir: &TempDir) -> TestCommandBuilder { + TestCommandBuilder::new(deno_dir.clone()) + .env("DENO_DIR", deno_dir.path()) + .env("NPM_CONFIG_REGISTRY", npm_registry_unset_url()) + .env("JSR_URL", jsr_registry_unset_url()) +} + +pub fn run_powershell_script_file( + script_file_path: &str, + args: Vec<&str>, +) -> std::result::Result<(), i64> { + let deno_dir = new_deno_dir(); + let mut command = Command::new("powershell.exe"); + + command + .env("DENO_DIR", deno_dir.path()) + .current_dir(testdata_path()) + .arg("-file") + .arg(script_file_path); + + for arg in args { + command.arg(arg); + } + + let output = command.output().expect("failed to spawn script"); + let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8(output.stderr).unwrap(); + println!("{stdout}"); + if !output.status.success() { + panic!( + "{script_file_path} executed with failing error code\n{stdout}{stderr}" + ); + } + + Ok(()) +} + +#[derive(Debug, Default)] +pub struct CheckOutputIntegrationTest<'a> { + pub args: &'a str, + pub args_vec: Vec<&'a str>, + pub output: &'a str, + pub input: Option<&'a str>, + pub output_str: Option<&'a str>, + pub exit_code: i32, + pub http_server: bool, + pub envs: Vec<(String, String)>, + pub env_clear: bool, + pub skip_strip_ansi: bool, + pub temp_cwd: bool, + /// Copies the files at the specified directory in the "testdata" directory + /// to the temp folder and runs the test from there. This is useful when + /// the test creates files in the testdata directory (ex. a node_modules folder) + pub copy_temp_dir: Option<&'a str>, + /// Relative to "testdata" directory + pub cwd: Option<&'a str>, +} + +impl<'a> CheckOutputIntegrationTest<'a> { + pub fn output(&self) -> TestCommandOutput { + let mut context_builder = TestContextBuilder::default(); + if self.temp_cwd { + context_builder = context_builder.use_temp_cwd(); + } + if let Some(dir) = &self.copy_temp_dir { + context_builder = context_builder.use_copy_temp_dir(dir); + } + if self.http_server { + context_builder = context_builder.use_http_server(); + } + + let context = context_builder.build(); + + let mut command_builder = context.new_command(); + + if !self.args.is_empty() { + command_builder = command_builder.args(self.args); + } + if !self.args_vec.is_empty() { + command_builder = command_builder.args_vec(self.args_vec.clone()); + } + if let Some(input) = &self.input { + command_builder = command_builder.stdin_text(input); + } + for (key, value) in &self.envs { + command_builder = command_builder.env(key, value); + } + if self.env_clear { + command_builder = command_builder.env_clear(); + } + if self.skip_strip_ansi { + command_builder = command_builder.skip_strip_ansi(); + } + if let Some(cwd) = &self.cwd { + command_builder = command_builder.current_dir(cwd); + } + + command_builder.run() + } +} + +pub fn wildcard_match(pattern: &str, text: &str) -> bool { + match wildcard_match_detailed(pattern, text) { + WildcardMatchResult::Success => true, + WildcardMatchResult::Fail(debug_output) => { + eprintln!("{}", debug_output); + false + } + } +} + +pub enum WildcardMatchResult { + Success, + Fail(String), +} + +pub fn wildcard_match_detailed( + pattern: &str, + text: &str, +) -> WildcardMatchResult { + fn annotate_whitespace(text: &str) -> String { + text.replace('\t', "\u{2192}").replace(' ', "\u{00B7}") + } + + // Normalize line endings + let original_text = text.replace("\r\n", "\n"); + let mut current_text = original_text.as_str(); + let pattern = pattern.replace("\r\n", "\n"); + let mut output_lines = Vec::new(); + + let parts = parse_wildcard_pattern_text(&pattern).unwrap(); + + let mut was_last_wildcard = false; + for (i, part) in parts.iter().enumerate() { + match part { + WildcardPatternPart::Wildcard => { + output_lines.push("<WILDCARD />".to_string()); + } + WildcardPatternPart::Text(search_text) => { + let is_last = i + 1 == parts.len(); + let search_index = if is_last && was_last_wildcard { + // search from the end of the file + current_text.rfind(search_text) + } else { + current_text.find(search_text) + }; + match search_index { + Some(found_index) if was_last_wildcard || found_index == 0 => { + output_lines.push(format!( + "<FOUND>{}</FOUND>", + colors::gray(annotate_whitespace(search_text)) + )); + current_text = ¤t_text[found_index + search_text.len()..]; + } + Some(index) => { + output_lines.push( + "==== FOUND SEARCH TEXT IN WRONG POSITION ====".to_string(), + ); + output_lines.push(colors::gray(annotate_whitespace(search_text))); + output_lines + .push("==== HAD UNKNOWN PRECEEDING TEXT ====".to_string()); + output_lines + .push(colors::red(annotate_whitespace(¤t_text[..index]))); + return WildcardMatchResult::Fail(output_lines.join("\n")); + } + None => { + let mut max_found_index = 0; + for (index, _) in search_text.char_indices() { + let sub_string = &search_text[..index]; + if let Some(found_index) = current_text.find(sub_string) { + if was_last_wildcard || found_index == 0 { + max_found_index = index; + } else { + break; + } + } else { + break; + } + } + if !was_last_wildcard && max_found_index > 0 { + output_lines.push(format!( + "<FOUND>{}</FOUND>", + colors::gray(annotate_whitespace( + &search_text[..max_found_index] + )) + )); + } + output_lines + .push("==== COULD NOT FIND SEARCH TEXT ====".to_string()); + output_lines.push(colors::green(annotate_whitespace( + if was_last_wildcard { + search_text + } else { + &search_text[max_found_index..] + }, + ))); + if was_last_wildcard && max_found_index > 0 { + output_lines.push(format!( + "==== MAX FOUND ====\n{}", + colors::red(annotate_whitespace( + &search_text[..max_found_index] + )) + )); + } + let actual_next_text = ¤t_text[max_found_index..]; + let max_next_text_len = 40; + let next_text_len = + std::cmp::min(max_next_text_len, actual_next_text.len()); + output_lines.push(format!( + "==== NEXT ACTUAL TEXT ====\n{}{}", + colors::red(annotate_whitespace( + &actual_next_text[..next_text_len] + )), + if actual_next_text.len() > max_next_text_len { + "[TRUNCATED]" + } else { + "" + }, + )); + return WildcardMatchResult::Fail(output_lines.join("\n")); + } + } + } + WildcardPatternPart::UnorderedLines(expected_lines) => { + assert!(!was_last_wildcard, "unsupported"); + let mut actual_lines = Vec::with_capacity(expected_lines.len()); + for _ in 0..expected_lines.len() { + match current_text.find('\n') { + Some(end_line_index) => { + actual_lines.push(¤t_text[..end_line_index]); + current_text = ¤t_text[end_line_index + 1..]; + } + None => { + break; + } + } + } + actual_lines.sort_unstable(); + let mut expected_lines = expected_lines.clone(); + expected_lines.sort_unstable(); + + if actual_lines.len() != expected_lines.len() { + output_lines + .push("==== HAD WRONG NUMBER OF UNORDERED LINES ====".to_string()); + output_lines.push("# ACTUAL".to_string()); + output_lines.extend( + actual_lines + .iter() + .map(|l| colors::green(annotate_whitespace(l))), + ); + output_lines.push("# EXPECTED".to_string()); + output_lines.extend( + expected_lines + .iter() + .map(|l| colors::green(annotate_whitespace(l))), + ); + return WildcardMatchResult::Fail(output_lines.join("\n")); + } + for (actual, expected) in actual_lines.iter().zip(expected_lines.iter()) + { + if actual != expected { + output_lines + .push("==== UNORDERED LINE DID NOT MATCH ====".to_string()); + output_lines.push(format!( + " ACTUAL: {}", + colors::red(annotate_whitespace(actual)) + )); + output_lines.push(format!( + "EXPECTED: {}", + colors::green(annotate_whitespace(expected)) + )); + return WildcardMatchResult::Fail(output_lines.join("\n")); + } else { + output_lines.push(format!( + "<FOUND>{}</FOUND>", + colors::gray(annotate_whitespace(expected)) + )); + } + } + } + } + was_last_wildcard = matches!(part, WildcardPatternPart::Wildcard); + } + + if was_last_wildcard || current_text.is_empty() { + WildcardMatchResult::Success + } else { + output_lines.push("==== HAD TEXT AT END OF FILE ====".to_string()); + output_lines.push(colors::red(annotate_whitespace(current_text))); + WildcardMatchResult::Fail(output_lines.join("\n")) + } +} + +#[derive(Debug)] +enum WildcardPatternPart<'a> { + Wildcard, + Text(&'a str), + UnorderedLines(Vec<&'a str>), +} + +fn parse_wildcard_pattern_text( + text: &str, +) -> Result<Vec<WildcardPatternPart>, monch::ParseErrorFailureError> { + use monch::*; + + fn parse_unordered_lines(input: &str) -> ParseResult<Vec<&str>> { + const END_TEXT: &str = "\n[UNORDERED_END]\n"; + let (input, _) = tag("[UNORDERED_START]\n")(input)?; + match input.find(END_TEXT) { + Some(end_index) => ParseResult::Ok(( + &input[end_index + END_TEXT.len()..], + input[..end_index].lines().collect::<Vec<_>>(), + )), + None => ParseError::fail(input, "Could not find [UNORDERED_END]"), + } + } + + enum InnerPart<'a> { + Wildcard, + UnorderedLines(Vec<&'a str>), + Char, + } + + struct Parser<'a> { + current_input: &'a str, + last_text_input: &'a str, + parts: Vec<WildcardPatternPart<'a>>, + } + + impl<'a> Parser<'a> { + fn parse(mut self) -> ParseResult<'a, Vec<WildcardPatternPart<'a>>> { + while !self.current_input.is_empty() { + let (next_input, inner_part) = or3( + map(tag("[WILDCARD]"), |_| InnerPart::Wildcard), + map(parse_unordered_lines, |lines| { + InnerPart::UnorderedLines(lines) + }), + map(next_char, |_| InnerPart::Char), + )(self.current_input)?; + match inner_part { + InnerPart::Wildcard => { + self.queue_previous_text(next_input); + self.parts.push(WildcardPatternPart::Wildcard); + } + InnerPart::UnorderedLines(expected_lines) => { + self.queue_previous_text(next_input); + self + .parts + .push(WildcardPatternPart::UnorderedLines(expected_lines)); + } + InnerPart::Char => { + // ignore + } + } + self.current_input = next_input; + } + + self.queue_previous_text(""); + + ParseResult::Ok(("", self.parts)) + } + + fn queue_previous_text(&mut self, next_input: &'a str) { + let previous_text = &self.last_text_input + [..self.last_text_input.len() - self.current_input.len()]; + if !previous_text.is_empty() { + self.parts.push(WildcardPatternPart::Text(previous_text)); + } + self.last_text_input = next_input; + } + } + + with_failure_handling(|input| { + Parser { + current_input: input, + last_text_input: input, + parts: Vec::new(), + } + .parse() + })(text) +} + +pub fn with_pty(deno_args: &[&str], action: impl FnMut(Pty)) { + let context = TestContextBuilder::default().use_temp_cwd().build(); + context.new_command().args_vec(deno_args).with_pty(action); +} + +pub struct WrkOutput { + pub latency: f64, + pub requests: u64, +} + +pub fn parse_wrk_output(output: &str) -> WrkOutput { + static REQUESTS_RX: Lazy<Regex> = + lazy_regex::lazy_regex!(r"Requests/sec:\s+(\d+)"); + static LATENCY_RX: Lazy<Regex> = + lazy_regex::lazy_regex!(r"\s+99%(?:\s+(\d+.\d+)([a-z]+))"); + + let mut requests = None; + let mut latency = None; + + for line in output.lines() { + if requests.is_none() { + if let Some(cap) = REQUESTS_RX.captures(line) { + requests = + Some(str::parse::<u64>(cap.get(1).unwrap().as_str()).unwrap()); + } + } + if latency.is_none() { + if let Some(cap) = LATENCY_RX.captures(line) { + let time = cap.get(1).unwrap(); + let unit = cap.get(2).unwrap(); + + latency = Some( + str::parse::<f64>(time.as_str()).unwrap() + * match unit.as_str() { + "ms" => 1.0, + "us" => 0.001, + "s" => 1000.0, + _ => unreachable!(), + }, + ); + } + } + } + + WrkOutput { + requests: requests.unwrap(), + latency: latency.unwrap(), + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct StraceOutput { + pub percent_time: f64, + pub seconds: f64, + pub usecs_per_call: Option<u64>, + pub calls: u64, + pub errors: u64, +} + +pub fn parse_strace_output(output: &str) -> HashMap<String, StraceOutput> { + let mut summary = HashMap::new(); + + // Filter out non-relevant lines. See the error log at + // https://github.com/denoland/deno/pull/3715/checks?check_run_id=397365887 + // This is checked in testdata/strace_summary2.out + let mut lines = output.lines().filter(|line| { + !line.is_empty() + && !line.contains("detached ...") + && !line.contains("unfinished ...") + && !line.contains("????") + }); + let count = lines.clone().count(); + + if count < 4 { + return summary; + } + + let total_line = lines.next_back().unwrap(); + lines.next_back(); // Drop separator + let data_lines = lines.skip(2); + + for line in data_lines { + let syscall_fields = line.split_whitespace().collect::<Vec<_>>(); + let len = syscall_fields.len(); + let syscall_name = syscall_fields.last().unwrap(); + if (5..=6).contains(&len) { + summary.insert( + syscall_name.to_string(), + StraceOutput { + percent_time: str::parse::<f64>(syscall_fields[0]).unwrap(), + seconds: str::parse::<f64>(syscall_fields[1]).unwrap(), + usecs_per_call: Some(str::parse::<u64>(syscall_fields[2]).unwrap()), + calls: str::parse::<u64>(syscall_fields[3]).unwrap(), + errors: if syscall_fields.len() < 6 { + 0 + } else { + str::parse::<u64>(syscall_fields[4]).unwrap() + }, + }, + ); + } + } + + let total_fields = total_line.split_whitespace().collect::<Vec<_>>(); + + let mut usecs_call_offset = 0; + summary.insert( + "total".to_string(), + StraceOutput { + percent_time: str::parse::<f64>(total_fields[0]).unwrap(), + seconds: str::parse::<f64>(total_fields[1]).unwrap(), + usecs_per_call: if total_fields.len() > 5 { + usecs_call_offset = 1; + Some(str::parse::<u64>(total_fields[2]).unwrap()) + } else { + None + }, + calls: str::parse::<u64>(total_fields[2 + usecs_call_offset]).unwrap(), + errors: str::parse::<u64>(total_fields[3 + usecs_call_offset]).unwrap(), + }, + ); + + summary +} + +pub fn parse_max_mem(output: &str) -> Option<u64> { + // Takes the output from "time -v" as input and extracts the 'maximum + // resident set size' and returns it in bytes. + for line in output.lines() { + if line + .to_lowercase() + .contains("maximum resident set size (kbytes)") + { + let value = line.split(": ").nth(1).unwrap(); + return Some(str::parse::<u64>(value).unwrap() * 1024); + } + } + + None +} + +pub(crate) mod colors { + use std::io::Write; + + use termcolor::Ansi; + use termcolor::Color; + use termcolor::ColorSpec; + use termcolor::WriteColor; + + pub fn bold<S: AsRef<str>>(s: S) -> String { + let mut style_spec = ColorSpec::new(); + style_spec.set_bold(true); + style(s, style_spec) + } + + pub fn red<S: AsRef<str>>(s: S) -> String { + fg_color(s, Color::Red) + } + + pub fn bold_red<S: AsRef<str>>(s: S) -> String { + bold_fg_color(s, Color::Red) + } + + pub fn green<S: AsRef<str>>(s: S) -> String { + fg_color(s, Color::Green) + } + + pub fn bold_green<S: AsRef<str>>(s: S) -> String { + bold_fg_color(s, Color::Green) + } + + pub fn bold_blue<S: AsRef<str>>(s: S) -> String { + bold_fg_color(s, Color::Blue) + } + + pub fn gray<S: AsRef<str>>(s: S) -> String { + fg_color(s, Color::Ansi256(245)) + } + + fn bold_fg_color<S: AsRef<str>>(s: S, color: Color) -> String { + let mut style_spec = ColorSpec::new(); + style_spec.set_bold(true); + style_spec.set_fg(Some(color)); + style(s, style_spec) + } + + fn fg_color<S: AsRef<str>>(s: S, color: Color) -> String { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(color)); + style(s, style_spec) + } + + fn style<S: AsRef<str>>(s: S, colorspec: ColorSpec) -> String { + let mut v = Vec::new(); + let mut ansi_writer = Ansi::new(&mut v); + ansi_writer.set_color(&colorspec).unwrap(); + ansi_writer.write_all(s.as_ref().as_bytes()).unwrap(); + ansi_writer.reset().unwrap(); + String::from_utf8_lossy(&v).into_owned() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn parse_wrk_output_1() { + const TEXT: &str = include_str!("./testdata/wrk1.txt"); + let wrk = parse_wrk_output(TEXT); + assert_eq!(wrk.requests, 1837); + assert!((wrk.latency - 6.25).abs() < f64::EPSILON); + } + + #[test] + fn parse_wrk_output_2() { + const TEXT: &str = include_str!("./testdata/wrk2.txt"); + let wrk = parse_wrk_output(TEXT); + assert_eq!(wrk.requests, 53435); + assert!((wrk.latency - 6.22).abs() < f64::EPSILON); + } + + #[test] + fn parse_wrk_output_3() { + const TEXT: &str = include_str!("./testdata/wrk3.txt"); + let wrk = parse_wrk_output(TEXT); + assert_eq!(wrk.requests, 96037); + assert!((wrk.latency - 6.36).abs() < f64::EPSILON); + } + + #[test] + fn strace_parse_1() { + const TEXT: &str = include_str!("./testdata/strace_summary.out"); + let strace = parse_strace_output(TEXT); + + // first syscall line + let munmap = strace.get("munmap").unwrap(); + assert_eq!(munmap.calls, 60); + assert_eq!(munmap.errors, 0); + + // line with errors + assert_eq!(strace.get("mkdir").unwrap().errors, 2); + + // last syscall line + let prlimit = strace.get("prlimit64").unwrap(); + assert_eq!(prlimit.calls, 2); + assert!((prlimit.percent_time - 0.0).abs() < f64::EPSILON); + + // summary line + assert_eq!(strace.get("total").unwrap().calls, 704); + assert_eq!(strace.get("total").unwrap().errors, 5); + assert_eq!(strace.get("total").unwrap().usecs_per_call, None); + } + + #[test] + fn strace_parse_2() { + const TEXT: &str = include_str!("./testdata/strace_summary2.out"); + let strace = parse_strace_output(TEXT); + + // first syscall line + let futex = strace.get("futex").unwrap(); + assert_eq!(futex.calls, 449); + assert_eq!(futex.errors, 94); + + // summary line + assert_eq!(strace.get("total").unwrap().calls, 821); + assert_eq!(strace.get("total").unwrap().errors, 107); + assert_eq!(strace.get("total").unwrap().usecs_per_call, None); + } + + #[test] + fn strace_parse_3() { + const TEXT: &str = include_str!("./testdata/strace_summary3.out"); + let strace = parse_strace_output(TEXT); + + // first syscall line + let futex = strace.get("mprotect").unwrap(); + assert_eq!(futex.calls, 90); + assert_eq!(futex.errors, 0); + + // summary line + assert_eq!(strace.get("total").unwrap().calls, 543); + assert_eq!(strace.get("total").unwrap().errors, 36); + assert_eq!(strace.get("total").unwrap().usecs_per_call, Some(6)); + } + + #[test] + fn parse_parse_wildcard_match_text() { + let result = + parse_wildcard_pattern_text("[UNORDERED_START]\ntesting\ntesting") + .err() + .unwrap(); + assert_contains!(result.to_string(), "Could not find [UNORDERED_END]"); + } + + #[test] + fn test_wildcard_match() { + let fixtures = vec![ + ("foobarbaz", "foobarbaz", true), + ("[WILDCARD]", "foobarbaz", true), + ("foobar", "foobarbaz", false), + ("foo[WILDCARD]baz", "foobarbaz", true), + ("foo[WILDCARD]baz", "foobazbar", false), + ("foo[WILDCARD]baz[WILDCARD]qux", "foobarbazqatqux", true), + ("foo[WILDCARD]", "foobar", true), + ("foo[WILDCARD]baz[WILDCARD]", "foobarbazqat", true), + // check with different line endings + ("foo[WILDCARD]\nbaz[WILDCARD]\n", "foobar\nbazqat\n", true), + ( + "foo[WILDCARD]\nbaz[WILDCARD]\n", + "foobar\r\nbazqat\r\n", + true, + ), + ( + "foo[WILDCARD]\r\nbaz[WILDCARD]\n", + "foobar\nbazqat\r\n", + true, + ), + ( + "foo[WILDCARD]\r\nbaz[WILDCARD]\r\n", + "foobar\nbazqat\n", + true, + ), + ( + "foo[WILDCARD]\r\nbaz[WILDCARD]\r\n", + "foobar\r\nbazqat\r\n", + true, + ), + ]; + + // Iterate through the fixture lists, testing each one + for (pattern, string, expected) in fixtures { + let actual = wildcard_match(pattern, string); + dbg!(pattern, string, expected); + assert_eq!(actual, expected); + } + } + + #[test] + fn test_wildcard_match2() { + // foo, bar, baz, qux, quux, quuz, corge, grault, garply, waldo, fred, plugh, xyzzy + + assert!(wildcard_match("foo[WILDCARD]baz", "foobarbaz")); + assert!(!wildcard_match("foo[WILDCARD]baz", "foobazbar")); + + let multiline_pattern = "[WILDCARD] +foo: +[WILDCARD]baz[WILDCARD]"; + + fn multi_line_builder(input: &str, leading_text: Option<&str>) -> String { + // If there is leading text add a newline so it's on it's own line + let head = match leading_text { + Some(v) => format!("{v}\n"), + None => "".to_string(), + }; + format!( + "{head}foo: +quuz {input} corge +grault" + ) + } + + // Validate multi-line string builder + assert_eq!( + "QUUX=qux +foo: +quuz BAZ corge +grault", + multi_line_builder("BAZ", Some("QUUX=qux")) + ); + + // Correct input & leading line + assert!(wildcard_match( + multiline_pattern, + &multi_line_builder("baz", Some("QUX=quux")), + )); + + // Should fail when leading line + assert!(!wildcard_match( + multiline_pattern, + &multi_line_builder("baz", None), + )); + + // Incorrect input & leading line + assert!(!wildcard_match( + multiline_pattern, + &multi_line_builder("garply", Some("QUX=quux")), + )); + + // Incorrect input & no leading line + assert!(!wildcard_match( + multiline_pattern, + &multi_line_builder("garply", None), + )); + } + + #[test] + fn test_wildcard_match_unordered_lines() { + // matching + assert!(wildcard_match( + concat!("[UNORDERED_START]\n", "B\n", "A\n", "[UNORDERED_END]\n"), + concat!("A\n", "B\n",) + )); + // different line + assert!(!wildcard_match( + concat!("[UNORDERED_START]\n", "Ba\n", "A\n", "[UNORDERED_END]\n"), + concat!("A\n", "B\n",) + )); + // different number of lines + assert!(!wildcard_match( + concat!( + "[UNORDERED_START]\n", + "B\n", + "A\n", + "C\n", + "[UNORDERED_END]\n" + ), + concat!("A\n", "B\n",) + )); + } + + #[test] + fn max_mem_parse() { + const TEXT: &str = include_str!("./testdata/time.out"); + let size = parse_max_mem(TEXT); + + assert_eq!(size, Some(120380 * 1024)); + } +} |