diff --git a/packages/hurl/src/cli/options.rs b/packages/hurl/src/cli/options.rs index d72dcf5c8b9..e170c3d7730 100644 --- a/packages/hurl/src/cli/options.rs +++ b/packages/hurl/src/cli/options.rs @@ -61,6 +61,7 @@ pub struct CliOptions { pub user_agent: Option, pub variables: HashMap, pub verbose: bool, + pub very_verbose: bool, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -297,10 +298,15 @@ pub fn app(version: &str) -> Command { .long("verbose") .help("Turn on verbose output"), ) + .arg( + clap::Arg::new("very_verbose") + .long("very-verbose") + .help("Turn on verbose output, uncluding HTTP response"), + ) } -pub fn parse_options(matches: ArgMatches) -> Result { - let cacert_file = match get_string(&matches, "cacert_file") { +pub fn parse_options(matches: &ArgMatches) -> Result { + let cacert_file = match get_string(matches, "cacert_file") { None => None, Some(filename) => { if !Path::new(&filename).is_file() { @@ -311,9 +317,9 @@ pub fn parse_options(matches: ArgMatches) -> Result { } } }; - let color = output_color(matches.clone()); - let compressed = has_flag(&matches, "compressed"); - let connect_timeout = match get_string(&matches, "connect_timeout") { + let color = output_color(matches); + let compressed = has_flag(matches, "compressed"); + let connect_timeout = match get_string(matches, "connect_timeout") { None => ClientOptions::default().connect_timeout, Some(s) => match s.parse::() { Ok(n) => Duration::from_secs(n), @@ -324,14 +330,13 @@ pub fn parse_options(matches: ArgMatches) -> Result { } }, }; - let cookie_input_file = get_string(&matches, "cookies_input_file"); - let cookie_output_file = get_string(&matches, "cookies_output_file"); - - let fail_fast = !has_flag(&matches, "fail_at_end"); - let file_root = get_string(&matches, "file_root"); - let follow_location = has_flag(&matches, "follow_location"); - let glob_files = match_glob_files(&matches)?; - let report_html = get_string(&matches, "report_html"); + let cookie_input_file = get_string(matches, "cookies_input_file"); + let cookie_output_file = get_string(matches, "cookies_output_file"); + let fail_fast = !has_flag(matches, "fail_at_end"); + let file_root = get_string(matches, "file_root"); + let follow_location = has_flag(matches, "follow_location"); + let glob_files = match_glob_files(matches)?; + let report_html = get_string(matches, "report_html"); let html_dir = if let Some(dir) = report_html { let path = Path::new(&dir); if !path.exists() { @@ -353,12 +358,12 @@ pub fn parse_options(matches: ArgMatches) -> Result { } else { None }; - let ignore_asserts = has_flag(&matches, "ignore_asserts"); - let include = has_flag(&matches, "include"); - let insecure = has_flag(&matches, "insecure"); - let interactive = has_flag(&matches, "interactive"); - let junit_file = get_string(&matches, "junit"); - let max_redirect = match get_string(&matches, "max_redirects").as_deref() { + let ignore_asserts = has_flag(matches, "ignore_asserts"); + let include = has_flag(matches, "include"); + let insecure = has_flag(matches, "insecure"); + let interactive = has_flag(matches, "interactive"); + let junit_file = get_string(matches, "junit"); + let max_redirect = match get_string(matches, "max_redirects").as_deref() { None => Some(50), Some("-1") => None, Some(s) => match s.parse::() { @@ -370,20 +375,20 @@ pub fn parse_options(matches: ArgMatches) -> Result { } }, }; - let no_proxy = get_string(&matches, "proxy"); - let output = get_string(&matches, "output"); - let test = has_flag(&matches, "test"); - let output_type = if has_flag(&matches, "json") { + let no_proxy = get_string(matches, "proxy"); + let output = get_string(matches, "output"); + let test = has_flag(matches, "test"); + let output_type = if has_flag(matches, "json") { OutputType::Json - } else if has_flag(&matches, "no_output") || test { + } else if has_flag(matches, "no_output") || test { OutputType::NoOutput } else { OutputType::ResponseBody }; - let progress = has_flag(&matches, "progress") || test; - let proxy = get_string(&matches, "proxy"); - let summary = has_flag(&matches, "summary") || test; - let timeout = match get_string(&matches, "max_time") { + let progress = has_flag(matches, "progress") || test; + let proxy = get_string(matches, "proxy"); + let summary = has_flag(matches, "summary") || test; + let timeout = match get_string(matches, "max_time") { None => ClientOptions::default().timeout, Some(s) => match s.parse::() { Ok(n) => Duration::from_secs(n), @@ -394,11 +399,12 @@ pub fn parse_options(matches: ArgMatches) -> Result { } }, }; - let to_entry = to_entry(matches.clone())?; - let user = get_string(&matches, "user"); - let user_agent = get_string(&matches, "user_agent"); + let to_entry = to_entry(matches)?; + let user = get_string(matches, "user"); + let user_agent = get_string(matches, "user_agent"); let variables = variables(matches.clone())?; - let verbose = has_flag(&matches, "verbose") || has_flag(&matches, "interactive"); + let very_verbose = has_flag(matches, "very_verbose"); + let verbose = has_flag(matches, "verbose") || has_flag(matches, "interactive") || very_verbose; Ok(CliOptions { cacert_file, @@ -430,21 +436,22 @@ pub fn parse_options(matches: ArgMatches) -> Result { user_agent, variables, verbose, + very_verbose, }) } -pub fn output_color(matches: ArgMatches) -> bool { - if has_flag(&matches, "color") { +pub fn output_color(matches: &ArgMatches) -> bool { + if has_flag(matches, "color") { true - } else if has_flag(&matches, "no_color") { + } else if has_flag(matches, "no_color") { false } else { atty::is(Stream::Stdout) } } -fn to_entry(matches: ArgMatches) -> Result, CliError> { - match get_string(&matches, "to_entry") { +fn to_entry(matches: &ArgMatches) -> Result, CliError> { + match get_string(matches, "to_entry") { Some(value) => match value.parse() { Ok(v) => Ok(Some(v)), Err(_) => Err(CliError { diff --git a/packages/hurl/src/http/client.rs b/packages/hurl/src/http/client.rs index 9945e85d815..87ed8cb1615 100644 --- a/packages/hurl/src/http/client.rs +++ b/packages/hurl/src/http/client.rs @@ -28,7 +28,7 @@ use super::options::ClientOptions; use super::request::*; use super::request_spec::*; use super::response::*; -use crate::http::HttpError; +use crate::http::{HttpError, Verbosity}; use std::str::FromStr; use url::Url; @@ -70,8 +70,11 @@ impl Client { } } + /// Execute an HTTP request and returns a list /// - /// Execute an http request + /// # Arguments + /// + /// * request - A request specification /// pub fn execute_with_redirect( &mut self, @@ -153,7 +156,7 @@ impl Client { self.set_headers(request); - let verbose = self.options.verbose; + let verbose = self.options.verbosity != None; let mut request_headers: Vec
= vec![]; let start = Instant::now(); @@ -251,6 +254,11 @@ impl Client { body, duration, }; + + if self.options.verbosity == Some(Verbosity::VeryVerbose) { + response.log_body(); + } + Ok((request, response)) } @@ -493,7 +501,7 @@ impl Client { /// Add cookie to Cookiejar /// pub fn add_cookie(&mut self, cookie: Cookie) { - if self.options.verbose { + if self.options.verbosity != None { eprintln!("* add to cookie store: {}", cookie); } self.handle @@ -505,7 +513,7 @@ impl Client { /// Clear cookie storage /// pub fn clear_cookie_storage(&mut self) { - if self.options.verbose { + if self.options.verbosity != None { eprintln!("* clear cookie storage"); } self.handle.cookie_list("ALL").unwrap(); @@ -632,6 +640,47 @@ pub fn decode_header(data: &[u8]) -> Option { } } +impl Response { + /// + /// Log a response body as text if possible, or a slice of body bytes. + /// + /// # Arguments + /// + /// * `response` - The HTTP response + fn log_body(&self) { + eprintln!("* Response:"); + + // We try to decode the HTTP body as text if the response has a text kind content type. + // If it ok, we print each line of the body in debug format. Otherwise, we + // print the body first 64 bytes. + if let Some(content_type) = self.content_type() { + if !content_type.contains("text") { + self.log_bytes(64); + return; + } + } + match self.text() { + Ok(text) => text.split('\n').for_each(|l| eprintln!("* {}", l)), + Err(_) => self.log_bytes(64), + } + } + + /// + /// Log a response bytes with a maximum size. + /// + /// # Arguments + /// + /// * `max` - The maximum number if bytes to log + fn log_bytes(&self, max: usize) { + let bytes = if self.body.len() > max { + &self.body[..max] + } else { + &self.body + }; + eprintln!("* Bytes <{}...>", hex::encode(bytes)) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/hurl/src/http/mod.rs b/packages/hurl/src/http/mod.rs index 871d3c76f0d..061f244ce65 100644 --- a/packages/hurl/src/http/mod.rs +++ b/packages/hurl/src/http/mod.rs @@ -20,7 +20,7 @@ pub use self::client::Client; pub use self::cookie::{CookieAttribute, ResponseCookie}; pub use self::core::{Cookie, Header, Param, RequestCookie}; pub use self::error::HttpError; -pub use self::options::ClientOptions; +pub use self::options::{ClientOptions, Verbosity}; pub use self::request::Request; #[cfg(test)] pub use self::request_spec::tests::*; diff --git a/packages/hurl/src/http/options.rs b/packages/hurl/src/http/options.rs index b64a04fbfd5..2d75e1bc779 100644 --- a/packages/hurl/src/http/options.rs +++ b/packages/hurl/src/http/options.rs @@ -26,7 +26,7 @@ pub struct ClientOptions { pub cookie_input_file: Option, pub proxy: Option, pub no_proxy: Option, - pub verbose: bool, + pub verbosity: Option, pub insecure: bool, pub timeout: Duration, pub connect_timeout: Duration, @@ -36,6 +36,12 @@ pub struct ClientOptions { pub context_dir: PathBuf, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Verbosity { + Verbose, + VeryVerbose, +} + impl Default for ClientOptions { fn default() -> Self { ClientOptions { @@ -45,7 +51,7 @@ impl Default for ClientOptions { cookie_input_file: None, proxy: None, no_proxy: None, - verbose: false, + verbosity: None, insecure: false, timeout: Duration::from_secs(300), connect_timeout: Duration::from_secs(300), @@ -130,7 +136,7 @@ mod tests { cookie_input_file: Some("cookie_file".to_string()), proxy: Some("localhost:3128".to_string()), no_proxy: None, - verbose: true, + verbosity: None, insecure: true, timeout: Duration::from_secs(10), connect_timeout: Duration::from_secs(20), diff --git a/packages/hurl/src/main.rs b/packages/hurl/src/main.rs index 884eeb2c58d..e2b58f39aec 100644 --- a/packages/hurl/src/main.rs +++ b/packages/hurl/src/main.rs @@ -27,6 +27,7 @@ use colored::*; use hurl::cli; use hurl::cli::{CliError, CliOptions, OutputType}; use hurl::http; +use hurl::http::Verbosity; use hurl::report; use hurl::report::canonicalize_filename; use hurl::runner; @@ -86,7 +87,7 @@ fn execute( filename: &str, contents: String, current_dir: &Path, - cli_options: CliOptions, + cli_options: &CliOptions, log_verbose: &impl Fn(&str), log_error_message: &impl Fn(bool, &str), progress: Option, @@ -123,7 +124,7 @@ fn execute( if let Some(n) = cli_options.max_redirect { log_verbose(format!(" max redirect: {}", n).as_str()); } - if let Some(proxy) = cli_options.proxy.clone() { + if let Some(proxy) = &cli_options.proxy { log_verbose(format!(" proxy: {}", proxy).as_str()); } @@ -145,19 +146,23 @@ fn execute( } } - let cacert_file = cli_options.cacert_file; + let cacert_file = cli_options.cacert_file.clone(); let follow_location = cli_options.follow_location; - let verbose = cli_options.verbose; + let verbosity = match (cli_options.verbose, cli_options.very_verbose) { + (true, true) => Some(Verbosity::VeryVerbose), + (true, _) => Some(Verbosity::Verbose), + _ => None, + }; let insecure = cli_options.insecure; let max_redirect = cli_options.max_redirect; - let proxy = cli_options.proxy; - let no_proxy = cli_options.no_proxy; - let cookie_input_file = cli_options.cookie_input_file; + let proxy = cli_options.proxy.clone(); + let no_proxy = cli_options.no_proxy.clone(); + let cookie_input_file = cli_options.cookie_input_file.clone(); let timeout = cli_options.timeout; let connect_timeout = cli_options.connect_timeout; - let user = cli_options.user; - let user_agent = cli_options.user_agent; + let user = cli_options.user.clone(); + let user_agent = cli_options.user_agent.clone(); let compressed = cli_options.compressed; let context_dir = match cli_options.file_root { None => { @@ -177,7 +182,7 @@ fn execute( cookie_input_file, proxy, no_proxy, - verbose, + verbosity, insecure, timeout, connect_timeout, @@ -199,19 +204,26 @@ fn execute( } else { || false }; + let fail_fast = cli_options.fail_fast; + let variables = cli_options.variables.clone(); + let to_entry = cli_options.to_entry; + let context_dir = context_dir.to_path_buf(); + let ignore_asserts = cli_options.ignore_asserts; + let very_verbose = cli_options.very_verbose; let options = RunnerOptions { - fail_fast: cli_options.fail_fast, - variables: cli_options.variables, - to_entry: cli_options.to_entry, - context_dir: context_dir.to_path_buf(), - ignore_asserts: cli_options.ignore_asserts, + fail_fast, + variables, + to_entry, + context_dir, + ignore_asserts, + very_verbose, pre_entry, post_entry, }; let result = runner::run_hurl_file( hurl_file, &mut client, - filename.to_string(), + filename, &options, &log_verbose, &log_error_message, @@ -257,11 +269,13 @@ fn main() { let matches = app.clone().get_matches(); init_colored(); - let verbose = cli::has_flag(&matches, "verbose") || cli::has_flag(&matches, "interactive"); + let verbose = cli::has_flag(&matches, "verbose") + || cli::has_flag(&matches, "very_verbose") + || cli::has_flag(&matches, "interactive"); let log_verbose = cli::make_logger_verbose(verbose); - let color = cli::output_color(matches.clone()); + let color = cli::output_color(&matches); let log_error_message = cli::make_logger_error_message(color); - let cli_options = unwrap_or_exit(cli::parse_options(matches.clone()), &log_error_message); + let cli_options = unwrap_or_exit(cli::parse_options(&matches), &log_error_message); let mut filenames = vec![]; if let Some(values) = cli::get_strings(&matches, "INPUT") { @@ -323,7 +337,7 @@ fn main() { filename, contents.clone(), current_dir, - cli_options.clone(), + &cli_options, &log_verbose, &log_error_message, progress, @@ -334,11 +348,13 @@ fn main() { && hurl_result.errors().is_empty() && !cli_options.interactive { - // default - // last entry + response + body + // By default, we output the body response bytes of the last entry if let Some(entry_result) = hurl_result.entries.last() { if let Some(response) = entry_result.response.clone() { let mut output = vec![]; + + // If include options is set, we output the HTTP response headers + // with status and version (to mimic curl outputs) if cli_options.include { let status_line = format!("HTTP/{} {}\n", response.version, response.status); diff --git a/packages/hurl/src/runner/core.rs b/packages/hurl/src/runner/core.rs index 84343c3028e..da27dba2cd1 100644 --- a/packages/hurl/src/runner/core.rs +++ b/packages/hurl/src/runner/core.rs @@ -30,6 +30,7 @@ pub struct RunnerOptions { pub to_entry: Option, pub context_dir: PathBuf, pub ignore_asserts: bool, + pub very_verbose: bool, // If true, log body response in verbose mode. pub pre_entry: fn(Entry) -> bool, pub post_entry: fn() -> bool, } diff --git a/packages/hurl/src/runner/entry.rs b/packages/hurl/src/runner/entry.rs index 22e13bf5c28..802a4836c01 100644 --- a/packages/hurl/src/runner/entry.rs +++ b/packages/hurl/src/runner/entry.rs @@ -87,7 +87,7 @@ pub fn run( } else { log_error_message( true, - format!("cookie string can not be parsed: '{}'", s).as_str(), + format!("Cookie string can not be parsed: '{}'", s).as_str(), ); } } @@ -101,7 +101,7 @@ pub fn run( log_verbose(cookie.to_string().as_str()); } log_verbose(""); - log_request(log_verbose, &http_request); + log_request_spec(log_verbose, &http_request); log_verbose( format!( "Request can be run with the following curl command:\n* {}\n*", @@ -139,7 +139,7 @@ pub fn run( let mut errors = vec![]; let time_in_ms = http_response.duration.as_millis(); - // Last call + // We runs capture and asserts on the last HTTP request/response chains. if i == calls.len() - 1 { captures = match entry.response.clone() { None => vec![], @@ -157,7 +157,7 @@ pub fn run( } }, }; - // update variables now! + // Update variables now! for capture_result in captures.clone() { variables.insert(capture_result.name, capture_result.value); } @@ -189,7 +189,7 @@ pub fn run( .collect(); if !captures.is_empty() { - log_verbose("Captures"); + log_verbose("Captures:"); for capture in captures.clone() { log_verbose(format!("{}: {}", capture.name, capture.value).as_str()); } @@ -211,37 +211,44 @@ pub fn run( entry_results } -pub fn log_request(log_verbose: impl Fn(&str), request: &http::RequestSpec) { +/// Log a HTTP request spec +/// +/// # Arguments +/// +/// * log_verbose - a log function +/// * request - an HTTP request spec +/// +fn log_request_spec(log_verbose: impl Fn(&str), request: &http::RequestSpec) { log_verbose("Request:"); log_verbose(format!("{} {}", request.method, request.url).as_str()); - for header in request.headers.clone() { + for header in &request.headers { log_verbose(header.to_string().as_str()); } if !request.querystring.is_empty() { log_verbose("[QueryStringParams]"); - for param in request.querystring.clone() { + for param in &request.querystring { log_verbose(param.to_string().as_str()); } } if !request.form.is_empty() { log_verbose("[FormParams]"); - for param in request.form.clone() { + for param in &request.form { log_verbose(param.to_string().as_str()); } } if !request.multipart.is_empty() { log_verbose("[MultipartFormData]"); - for param in request.multipart.clone() { + for param in &request.multipart { log_verbose(param.to_string().as_str()); } } if !request.cookies.is_empty() { log_verbose("[Cookies]"); - for cookie in request.cookies.clone() { + for cookie in &request.cookies { log_verbose(cookie.to_string().as_str()); } } - if let Some(s) = request.content_type.clone() { + if let Some(s) = &request.content_type { log_verbose(""); log_verbose(format!("Implicit content-type={}", s).as_str()); } diff --git a/packages/hurl/src/runner/hurl_file.rs b/packages/hurl/src/runner/hurl_file.rs index 676d470f5b5..fd30c015a37 100644 --- a/packages/hurl/src/runner/hurl_file.rs +++ b/packages/hurl/src/runner/hurl_file.rs @@ -35,7 +35,7 @@ use super::entry; /// use hurl::runner; /// /// // Parse Hurl file -/// let filename = "sample.hurl".to_string(); +/// let filename = "sample.hurl"; /// let s = r#" /// GET http://localhost:8000/hello /// HTTP/1.0 200 @@ -59,6 +59,7 @@ use super::entry; /// to_entry: None, /// context_dir: PathBuf::new(), /// ignore_asserts: false, +/// very_verbose: false, /// pre_entry: |_| true, /// post_entry: || true, /// }; @@ -80,7 +81,7 @@ use super::entry; pub fn run( hurl_file: HurlFile, http_client: &mut http::Client, - filename: String, + filename: &str, options: &RunnerOptions, log_verbose: &impl Fn(&str), log_error_message: &impl Fn(bool, &str), @@ -143,6 +144,7 @@ pub fn run( .is_none(); let cookies = http_client.get_cookie_storage(); + let filename = filename.to_string(); HurlResult { filename, entries, diff --git a/packages/hurl/tests/runner.rs b/packages/hurl/tests/runner.rs index d80c6fa4645..b6cdf30b721 100644 --- a/packages/hurl/tests/runner.rs +++ b/packages/hurl/tests/runner.rs @@ -56,6 +56,7 @@ fn test_hurl_file() { to_entry: None, context_dir: PathBuf::new(), ignore_asserts: false, + very_verbose: false, pre_entry: |_| true, post_entry: || true, }; @@ -68,7 +69,7 @@ fn test_hurl_file() { hurl_file, &mut client, //&mut variables, - filename.to_string(), + filename, &options, &log_verbose, &log_error_message, @@ -186,6 +187,7 @@ fn test_hello() { to_entry: None, context_dir: PathBuf::new(), ignore_asserts: false, + very_verbose: false, pre_entry: |_| true, post_entry: || true, }; @@ -195,7 +197,7 @@ fn test_hello() { let _hurl_log = runner::run_hurl_file( hurl_file, &mut client, - String::from("filename"), + "filename", &options, &log_verbose, &log_error_message,