diff options
Diffstat (limited to 'test_util/src/lib.rs')
-rw-r--r-- | test_util/src/lib.rs | 1273 |
1 files changed, 0 insertions, 1273 deletions
diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs deleted file mode 100644 index e4549154d..000000000 --- a/test_util/src/lib.rs +++ /dev/null @@ -1,1273 +0,0 @@ -// 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(), - ) -} - -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)); - } -} |