diff --git a/docs/spec/runner/run_cycle.drawio b/docs/spec/runner/run_cycle.drawio index cf506106a..42f2ed65a 100644 --- a/docs/spec/runner/run_cycle.drawio +++ b/docs/spec/runner/run_cycle.drawio @@ -1,17 +1,17 @@ - + - + - + - + @@ -31,7 +31,7 @@ - + @@ -68,7 +68,7 @@ - + diff --git a/docs/spec/runner/run_cycle.svg b/docs/spec/runner/run_cycle.svg index 04f14fef5..3b2da54ab 100644 --- a/docs/spec/runner/run_cycle.svg +++ b/docs/spec/runner/run_cycle.svg @@ -1,3 +1,3 @@ -
START
START
Entry 
to run?
Entry...
Eval
entry options
Eval...
YES
YES
SUCCESS
SUCCESS
NO
NO
Skip?
Skip?
Delay?
Delay?
NO
NO
Sleep delay
Sleep delay
YES
YES
Run HTTP requests
Run HTTP requests
NO
NO
Eval errors
Eval errors
Eval captures
Eval captures
Errors?
Errors?
Increment
repeat index
Increment...
NO
NO
Sleep
retry interval
Sleep...
YES
YES
Retry?
Retry?
YES
YES
ERROR
ERROR
NO
NO
Repeat?
Repeat?
YES
YES
Increment
entry index
Increment...
NO
NO
\ No newline at end of file +
START
START
Entry 
to run?
Entry...
Eval
entry options
Eval...
YES
YES
SUCCESS
SUCCESS
NO
NO
Skip?
Skip?
Delay?
Delay?
NO
NO
Sleep delay
Sleep delay
YES
YES
Run HTTP requests
Run HTTP requests
NO
NO
Eval errors
Eval errors
Eval captures
Eval captures
Errors?
Errors?
Increment
repeat index
Increment...
NO
NO
Sleep
retry interval
Sleep...
YES
YES
Retry?
Retry?
YES
YES
ERROR
ERROR
NO
NO
Repeat?
Repeat?
YES
YES
Increment
entry index
Increment...
NO
NO
\ No newline at end of file diff --git a/integration/hurl/integration.py b/integration/hurl/integration.py index 989e07238..97d1afbd1 100755 --- a/integration/hurl/integration.py +++ b/integration/hurl/integration.py @@ -26,7 +26,7 @@ def main(): + get_files("tests_failed/*." + extension) + get_files("tests_failed_not_linted/*." + extension) + get_files("tests_error_parser/*." + extension) - + get_files("ssl/*." + extension) + # + get_files("ssl/*." + extension) ) for f in sorted(script_files): test_script.test(f) diff --git a/integration/hurl/tests_failed/retry_option.err.pattern b/integration/hurl/tests_failed/retry_option.err.pattern index ac735cde7..df03df100 100644 --- a/integration/hurl/tests_failed/retry_option.err.pattern +++ b/integration/hurl/tests_failed/retry_option.err.pattern @@ -42,10 +42,6 @@ * ------------------------------------------------------------------------------ * Executing entry 1 * -* Entry options: -* retry: 2 -* retry-interval: 0 -* * Cookie store: * * Request: @@ -83,10 +79,6 @@ * ------------------------------------------------------------------------------ * Executing entry 1 * -* Entry options: -* retry: 2 -* retry-interval: 0 -* * Cookie store: * * Request: diff --git a/integration/hurl/tests_ok/delay_option.err.pattern b/integration/hurl/tests_ok/delay_option.err.pattern index c254dfaee..a2988eaea 100644 --- a/integration/hurl/tests_ok/delay_option.err.pattern +++ b/integration/hurl/tests_ok/delay_option.err.pattern @@ -31,7 +31,7 @@ * Entry options: * delay: 1000 * -* Delay entry 2 (x1 by 1000 ms) +* Delay entry 2 (pause 1000 ms) * * Cookie store: * diff --git a/integration/hurl/tests_ok/parallel.err.pattern b/integration/hurl/tests_ok/parallel.err.pattern index a485c3bf1..c07bfa6ce 100644 --- a/integration/hurl/tests_ok/parallel.err.pattern +++ b/integration/hurl/tests_ok/parallel.err.pattern @@ -7,7 +7,7 @@ * Entry options: * delay: 5000 * -* Delay entry 1 (x1 by 5000 ms) +* Delay entry 1 (pause 5000 ms) * * Cookie store: * @@ -41,7 +41,7 @@ * Entry options: * delay: 5000 * -* Delay entry 1 (x1 by 5000 ms) +* Delay entry 1 (pause 5000 ms) * * Cookie store: * @@ -75,7 +75,7 @@ * Entry options: * delay: 5000 * -* Delay entry 1 (x1 by 5000 ms) +* Delay entry 1 (pause 5000 ms) * * Cookie store: * @@ -109,7 +109,7 @@ * Entry options: * delay: 5000 * -* Delay entry 1 (x1 by 5000 ms) +* Delay entry 1 (pause 5000 ms) * * Cookie store: * diff --git a/integration/hurl/tests_ok/retry_option.err.pattern b/integration/hurl/tests_ok/retry_option.err.pattern index 0b04f92b5..5477437a2 100644 --- a/integration/hurl/tests_ok/retry_option.err.pattern +++ b/integration/hurl/tests_ok/retry_option.err.pattern @@ -72,10 +72,6 @@ * ------------------------------------------------------------------------------ * Executing entry 2 * -* Entry options: -* retry: 10 -* retry-interval: 100 -* * Cookie store: * * Request: @@ -114,10 +110,6 @@ * ------------------------------------------------------------------------------ * Executing entry 2 * -* Entry options: -* retry: 10 -* retry-interval: 100 -* * Cookie store: * * Request: @@ -156,10 +148,6 @@ * ------------------------------------------------------------------------------ * Executing entry 2 * -* Entry options: -* retry: 10 -* retry-interval: 100 -* * Cookie store: * * Request: @@ -198,10 +186,6 @@ * ------------------------------------------------------------------------------ * Executing entry 2 * -* Entry options: -* retry: 10 -* retry-interval: 100 -* * Cookie store: * * Request: diff --git a/integration/test_script.py b/integration/test_script.py index eea59fd4a..0f1c74b6e 100755 --- a/integration/test_script.py +++ b/integration/test_script.py @@ -142,7 +142,10 @@ def test_stderr(f, result): actual = ignore_lines(decode_string(result.stderr)) if actual != expected: print(">>> error in stderr") - print(f"actual: <{actual}>\nexpected: <{expected}>") + print("actual:") + print(actual) + print("expected:") + print(expected) sys.exit(1) diff --git a/packages/hurl/src/runner/hurl_file.rs b/packages/hurl/src/runner/hurl_file.rs index 72f5b33a3..c20c72199 100644 --- a/packages/hurl/src/runner/hurl_file.rs +++ b/packages/hurl/src/runner/hurl_file.rs @@ -140,7 +140,6 @@ pub fn run_entries( let mut variables = variables.clone(); let mut entry_index = runner_options.from_entry.unwrap_or(1); let n = runner_options.to_entry.unwrap_or(entries.len()); - let mut retry_count = 1; let default_verbosity = logger.verbosity; let start = Instant::now(); let timestamp = Utc::now().timestamp(); @@ -151,6 +150,7 @@ pub fn run_entries( // The `entry_index` is not always incremented of each loop tick: an entry can be retried upon // errors for instance. Each entry is executed with options that are computed from the global // runner options and the "overridden" request options. + // See loop { if entry_index > n { break; @@ -173,120 +173,74 @@ pub fn run_entries( logger.verbosity = entry_verbosity; } - logger.debug_important( - "------------------------------------------------------------------------------", - ); - logger.debug_important(&format!("Executing entry {entry_index}")); + log_run_entry(entry_index, logger); warn_deprecated(entry, logger); + // We can report the progression of the run for --test mode. if let Some(listener) = listener { listener.on_running(entry_index - 1, n); } - // The real execution of the entry happens here, with the overridden entry options. + // The real execution of the entry happens here, first: we compute the overridden request + // options. let options = options::get_entry_options(entry, runner_options, &mut variables, logger); - let mut entry_result = match &options { - Err(error) => EntryResult { + if let Err(error) = &options { + // If we have error evaluating request options, we consider it as a non retryable error + // and either break the runner or go to the next entries. + let entry_result = EntryResult { entry_index, source_info: entry.source_info(), errors: vec![error.clone()], ..Default::default() - }, - Ok(options) => { - if options.skip { - logger.debug(""); - logger.debug_important(&format!("Entry {entry_index} has been skipped")); - entry_index += 1; - continue; - } - - let delay = options.delay; - let delay_ms = delay.as_millis(); - if delay_ms > 0 { - logger.debug(""); - logger.debug_important(&format!( - "Delay entry {entry_index} (x{retry_count} by {delay_ms} ms)" - )); - thread::sleep(delay); - }; - - entry::run( - entry, - entry_index, - &mut http_client, - &mut variables, - options, - logger, - ) - } - }; - - // Check if we need to retry. - let mut has_error = !entry_result.errors.is_empty(); - let (retry_opts, retry_interval) = match &options { - Ok(options) => (options.retry, options.retry_interval), - Err(_) => (runner_options.retry, runner_options.retry_interval), - }; - // The retry threshold can only reached with a finite positive number of retries - let retry_max_reached = if let Retry::Finite(r) = retry_opts { - retry_count > r - } else { - false - }; - // If `retry_max_reached` is true, we print now a warning, before displaying any assert - // error so any potential error is the last thing displayed to the user. - // If `retry_max_reached` is not true (for instance `retry`is true, or there is no error - // we first log the error and a potential warning about retrying. - if retry_max_reached { - logger.debug_important("Retry max count reached, no more retry"); - logger.debug(""); - } - - // We logs eventual errors, only if we're not retrying the current entry... - // The retry does not take into account a possible output Error - let retry = !matches!(retry_opts, Retry::None) && !retry_max_reached && has_error; - - // When --output is overridden on a request level, we output the HTTP response only if the - // call has succeeded. - if let Ok(RunnerOptions { - output: Some(output), - .. - }) = options - { - if !has_error { - let source_info = get_output_source_info(entry); - if let Err(error) = entry_result.write_response( - &output, - &runner_options.context_dir, - stdout, - source_info, - ) { - entry_result.errors.push(error); - has_error = true; - } + }; + log_errors(&entry_result, content, false, logger); + entries_result.push(entry_result); + if runner_options.continue_on_error { + entry_index += 1; + continue; + } else { + break; } } - if has_error { - log_errors(&entry_result, content, retry, logger); - } - entries_result.push(entry_result); + let options = options.unwrap(); - if retry { - let delay = retry_interval.as_millis(); + // Should we skip? + if options.skip { logger.debug(""); - logger.debug_important(&format!( - "Retry entry {entry_index} (x{retry_count} pause {delay} ms)" - )); - retry_count += 1; - // If we retry the entry, we do not want to display a 'blank' progress bar during the - // sleep delay. During the pause, we artificially show the previously erased progress - // line. - thread::sleep(retry_interval); + logger.debug_important(&format!("Entry {entry_index} has been skipped")); + entry_index += 1; continue; } + // Should we delay? + let delay = options.delay; + let delay_ms = delay.as_millis(); + if delay_ms > 0 { + logger.debug(""); + logger.debug_important(&format!("Delay entry {entry_index} (pause {delay_ms} ms)")); + thread::sleep(delay); + }; + + // Loop for executing HTTP run requests, with optional retry. Only "HTTP" errors in options + // are taken into account for retry (errors while computing entry options and output error + // are not retried). + let results = run_request( + entry, + entry_index, + content, + &mut http_client, + &options, + &mut variables, + stdout, + logger, + ); + + let has_error = results.last().map_or(false, |r| !r.errors.is_empty()); + + entries_result.extend(results); + if let Some(post_entry) = runner_options.post_entry { let exit = post_entry(); if exit { @@ -299,7 +253,6 @@ pub fn run_entries( // We pass to the next entry entry_index += 1; - retry_count = 1; } let time_in_ms = start.elapsed().as_millis(); @@ -314,6 +267,90 @@ pub fn run_entries( } } +/// Runs an HTTP request and optional retry it until there are no HTTP errors. Returns a list of +/// [`EntryResult`]. +#[allow(clippy::too_many_arguments)] +fn run_request( + entry: &Entry, + entry_index: usize, + content: &str, + http_client: &mut Client, + options: &RunnerOptions, + variables: &mut HashMap, + stdout: &mut Stdout, + logger: &mut Logger, +) -> Vec { + let mut results = vec![]; + let mut retry_count = 1; + + loop { + let mut result = entry::run(entry, entry_index, http_client, variables, options, logger); + + // Check if we need to retry. + let mut has_error = !result.errors.is_empty(); + + // The retry threshold can only be reached with a finite positive number of retries + let retry_max_reached = if let Retry::Finite(r) = options.retry { + retry_count > r + } else { + false + }; + // If `retry_max_reached` is true, we print now a warning, before displaying any assert + // error so any potential error is the last thing displayed to the user. + // If `retry_max_reached` is not true (for instance `retry`is true, or there is no error + // we first log the error and a potential warning about retrying. + if retry_max_reached { + logger.debug_important("Retry max count reached, no more retry"); + logger.debug(""); + } + + // We log eventual errors, only if we're not retrying the current entry... + // The retry does not take into account a possible output Error + let retry = !matches!(options.retry, Retry::None) && !retry_max_reached && has_error; + + // When --output is overridden on a request level, we output the HTTP response only if the + // call has succeeded. Output errors are not taken into account for retrying requests. + if let Some(output) = &options.output { + if !has_error { + let source_info = get_output_source_info(entry); + if let Err(error) = + result.write_response(output, &options.context_dir, stdout, source_info) + { + result.errors.push(error); + has_error = true; + } + } + } + + if has_error { + log_errors(&result, content, retry, logger); + } + results.push(result); + + // No retry, we leave the HTTP run requests loop. + if !retry { + break; + } + + let delay = options.retry_interval.as_millis(); + logger.debug(""); + logger.debug_important(&format!( + "Retry entry {entry_index} (x{retry_count} pause {delay} ms)" + )); + retry_count += 1; + // If we retry the entry, we do not want to display a 'blank' progress bar during the + // sleep delay. During the pause, we artificially show the previously erased progress + // line. + thread::sleep(options.retry_interval); + + // TODO: We keep this log because we don't want to change stderr with the changes + // introduced by + log_run_entry(entry_index, logger); + } + + results +} + /// Use source_info from output option if this option has been defined fn get_output_source_info(entry: &Entry) -> SourceInfo { let mut source_info = entry.source_info(); @@ -509,6 +546,14 @@ fn log_errors(entry_result: &EntryResult, content: &str, retry: bool, logger: &m .for_each(|error| logger.error_runtime_rich(content, error, entry_result.source_info)); } +/// Logs the header indicating the begin of the entry run. +fn log_run_entry(entry_index: usize, logger: &mut Logger) { + logger.debug_important( + "------------------------------------------------------------------------------", + ); + logger.debug_important(&format!("Executing entry {entry_index}")); +} + #[cfg(test)] mod test { use super::*; diff --git a/packages/hurl/src/runner/options.rs b/packages/hurl/src/runner/options.rs index a5fd11a20..38d0e2ad2 100644 --- a/packages/hurl/src/runner/options.rs +++ b/packages/hurl/src/runner/options.rs @@ -41,12 +41,12 @@ pub fn get_entry_options( // When used globally (on the command line), `--output` writes the last successful request // to `output` file. We don't want to output every entry's response, so we initialise // output to `None`. - let mut runner_options = RunnerOptions { + let mut entry_options = RunnerOptions { output: None, ..runner_options }; if !has_options(entry) { - return Ok(runner_options); + return Ok(entry_options); } logger.debug(""); @@ -58,31 +58,31 @@ pub fn get_entry_options( match &option.kind { OptionKind::AwsSigV4(value) => { let value = eval_template(value, variables)?; - runner_options.aws_sigv4 = Some(value); + entry_options.aws_sigv4 = Some(value); } OptionKind::CaCertificate(filename) => { let value = eval_template(filename, variables)?; - runner_options.cacert_file = Some(value); + entry_options.cacert_file = Some(value); } OptionKind::ClientCert(filename) => { let value = eval_template(filename, variables)?; - runner_options.client_cert_file = Some(value); + entry_options.client_cert_file = Some(value); } OptionKind::ClientKey(filename) => { let value = eval_template(filename, variables)?; - runner_options.client_key_file = Some(value); + entry_options.client_key_file = Some(value); } OptionKind::Compressed(value) => { let value = eval_boolean_option(value, variables)?; - runner_options.compressed = value; + entry_options.compressed = value; } OptionKind::ConnectTo(value) => { let value = eval_template(value, variables)?; - runner_options.connects_to.push(value); + entry_options.connects_to.push(value); } OptionKind::Delay(value) => { let value = eval_natural_option(value, variables)?; - runner_options.delay = Duration::from_millis(value); + entry_options.delay = Duration::from_millis(value); } // HTTP version options (such as http1.0, http1.1, http2 etc...) are activated // through a flag. In an `[Options]` section, the signification of such a flag is: @@ -111,51 +111,51 @@ pub fn get_entry_options( OptionKind::Http10(value) => { let value = eval_boolean_option(value, variables)?; if value { - runner_options.http_version = RequestedHttpVersion::Http10; + entry_options.http_version = RequestedHttpVersion::Http10; } } OptionKind::Http11(value) => { let value = eval_boolean_option(value, variables)?; if value { - runner_options.http_version = RequestedHttpVersion::Http11; + entry_options.http_version = RequestedHttpVersion::Http11; } else { - runner_options.http_version = RequestedHttpVersion::Http10; + entry_options.http_version = RequestedHttpVersion::Http10; } } OptionKind::Http2(value) => { let value = eval_boolean_option(value, variables)?; if value { - runner_options.http_version = RequestedHttpVersion::Http2; + entry_options.http_version = RequestedHttpVersion::Http2; } else { - runner_options.http_version = RequestedHttpVersion::Http11; + entry_options.http_version = RequestedHttpVersion::Http11; } } OptionKind::Http3(value) => { let value = eval_boolean_option(value, variables)?; if value { - runner_options.http_version = RequestedHttpVersion::Http3; + entry_options.http_version = RequestedHttpVersion::Http3; } else { - runner_options.http_version = RequestedHttpVersion::Http2; + entry_options.http_version = RequestedHttpVersion::Http2; } } OptionKind::FollowLocation(value) => { let value = eval_boolean_option(value, variables)?; - runner_options.follow_location = value; + entry_options.follow_location = value; } OptionKind::FollowLocationTrusted(value) => { let value = eval_boolean_option(value, variables)?; if value { - runner_options.follow_location = true; + entry_options.follow_location = true; } - runner_options.follow_location_trusted = value; + entry_options.follow_location_trusted = value; } OptionKind::Insecure(value) => { let value = eval_boolean_option(value, variables)?; - runner_options.insecure = value; + entry_options.insecure = value; } OptionKind::IpV4(value) => { let value = eval_boolean_option(value, variables)?; - runner_options.ip_resolve = if value { + entry_options.ip_resolve = if value { IpResolve::IpV4 } else { IpResolve::IpV6 @@ -163,7 +163,7 @@ pub fn get_entry_options( } OptionKind::IpV6(value) => { let value = eval_boolean_option(value, variables)?; - runner_options.ip_resolve = if value { + entry_options.ip_resolve = if value { IpResolve::IpV6 } else { IpResolve::IpV4 @@ -171,56 +171,56 @@ pub fn get_entry_options( } OptionKind::MaxRedirect(value) => { let value = eval_natural_option(value, variables)?; - runner_options.max_redirect = Some(value as usize); + entry_options.max_redirect = Some(value as usize); } OptionKind::NetRc(value) => { let value = eval_boolean_option(value, variables)?; - runner_options.netrc = value; + entry_options.netrc = value; } OptionKind::NetRcFile(value) => { let filename = eval_template(value, variables)?; - runner_options.netrc_file = Some(filename); + entry_options.netrc_file = Some(filename); } OptionKind::NetRcOptional(value) => { let value = eval_boolean_option(value, variables)?; - runner_options.netrc_optional = value; + entry_options.netrc_optional = value; } OptionKind::Output(output) => { let filename = eval_template(output, variables)?; let output = Output::new(&filename); - runner_options.output = Some(output); + entry_options.output = Some(output); } OptionKind::PathAsIs(value) => { let value = eval_boolean_option(value, variables)?; - runner_options.path_as_is = value; + entry_options.path_as_is = value; } OptionKind::Proxy(value) => { let value = eval_template(value, variables)?; - runner_options.proxy = Some(value); + entry_options.proxy = Some(value); } OptionKind::Resolve(value) => { let value = eval_template(value, variables)?; - runner_options.resolves.push(value); + entry_options.resolves.push(value); } OptionKind::Retry(value) => { let value = eval_retry_option(value, variables)?; - runner_options.retry = value; + entry_options.retry = value; } OptionKind::RetryInterval(value) => { let value = eval_natural_option(value, variables)?; - runner_options.retry_interval = Duration::from_millis(value); + entry_options.retry_interval = Duration::from_millis(value); } OptionKind::Skip(value) => { let value = eval_boolean_option(value, variables)?; - runner_options.skip = value; + entry_options.skip = value; } OptionKind::UnixSocket(value) => { let value = eval_template(value, variables)?; - runner_options.unix_socket = Some(value); + entry_options.unix_socket = Some(value); } OptionKind::User(value) => { let value = eval_template(value, variables)?; - runner_options.user = Some(value); + entry_options.user = Some(value); } OptionKind::Variable(VariableDefinition { name, value, .. }) => { let value = eval_variable_value(value, variables)?; @@ -240,7 +240,7 @@ pub fn get_entry_options( } } } - Ok(runner_options) + Ok(entry_options) } /// Logs an entry option. diff --git a/packages/hurl/src/runner/xpath.rs b/packages/hurl/src/runner/xpath.rs index 0d33aeb33..720d60d30 100644 --- a/packages/hurl/src/runner/xpath.rs +++ b/packages/hurl/src/runner/xpath.rs @@ -73,7 +73,7 @@ pub fn eval_html(html: &str, expr: &str) -> Result { /// - /// These two functions should be removed when the issue is fixed in libxml crate. fn try_usize_to_i32(value: usize) -> Result { - if cfg!(target_pointer_width = "16") || (value < i32::max_value() as usize) { + if cfg!(target_pointer_width = "16") || (value < i32::MAX as usize) { // Cannot safely use our value comparison, but the conversion if always safe. // Or, if the value can be safely represented as a 32-bit signed integer. Ok(value as i32) diff --git a/packages/hurlfmt/src/curl/matches.rs b/packages/hurlfmt/src/curl/matches.rs index 81a3cede1..47c2fdfeb 100644 --- a/packages/hurlfmt/src/curl/matches.rs +++ b/packages/hurlfmt/src/curl/matches.rs @@ -59,10 +59,7 @@ pub fn url(arg_matches: &ArgMatches) -> String { } pub fn headers(arg_matches: &ArgMatches) -> Vec { - let mut headers = match get_strings(arg_matches, "headers") { - None => vec![], - Some(v) => v, - }; + let mut headers = get_strings(arg_matches, "headers").unwrap_or_default(); if !has_content_type(&headers) { if let Some(data) = get_string(arg_matches, "data") { if !data.starts_with('@') {