Add retry-max-count option.

This commit is contained in:
jcamiel 2022-10-17 20:05:49 +02:00
parent d34647ca7e
commit e85354f0eb
No known key found for this signature in database
GPG Key ID: 07FF11CFD55356CC
17 changed files with 309 additions and 49 deletions

View File

@ -0,0 +1,206 @@
* Options:
* fail fast: true
* follow redirect: false
* insecure: false
* max redirect: 50
* retry: true
* retry max count: 5
* ------------------------------------------------------------------------------
* Executing entry 1
*
* Cookie store:
*
* Request:
* GET http://localhost:8000/not-found
*
* Request can be run with the following curl command:
* curl 'http://localhost:8000/not-found'
*
> GET /not-found HTTP/1.1
> Host: localhost:8000
> Accept: */*
> User-Agent: hurl/~~~
>
* Response: (received 232 bytes in ~~~ ms)
*
< HTTP/1.0 404 NOT FOUND
< Content-Type: text/html; charset=utf-8
< Content-Length: 232
< Server: Flask Server
< Date: ~~~
<
*
* Assert status code
* --> tests_failed/retry_max_count.hurl:2:8
* |
* 2 | HTTP/* 200
* | ^^^ actual value is <404>
* |
*
* Retry entry 1 (x1 pause 100 ms)
* ------------------------------------------------------------------------------
* Executing entry 1
*
* Cookie store:
*
* Request:
* GET http://localhost:8000/not-found
*
* Request can be run with the following curl command:
* curl 'http://localhost:8000/not-found'
*
> GET /not-found HTTP/1.1
> Host: localhost:8000
> Accept: */*
> User-Agent: hurl/~~~
>
* Response: (received 232 bytes in ~~~ ms)
*
< HTTP/1.0 404 NOT FOUND
< Content-Type: text/html; charset=utf-8
< Content-Length: 232
< Server: Flask Server
< Date: ~~~
<
*
* Assert status code
* --> tests_failed/retry_max_count.hurl:2:8
* |
* 2 | HTTP/* 200
* | ^^^ actual value is <404>
* |
*
* Retry entry 1 (x2 pause 100 ms)
* ------------------------------------------------------------------------------
* Executing entry 1
*
* Cookie store:
*
* Request:
* GET http://localhost:8000/not-found
*
* Request can be run with the following curl command:
* curl 'http://localhost:8000/not-found'
*
> GET /not-found HTTP/1.1
> Host: localhost:8000
> Accept: */*
> User-Agent: hurl/~~~
>
* Response: (received 232 bytes in ~~~ ms)
*
< HTTP/1.0 404 NOT FOUND
< Content-Type: text/html; charset=utf-8
< Content-Length: 232
< Server: Flask Server
< Date: ~~~
<
*
* Assert status code
* --> tests_failed/retry_max_count.hurl:2:8
* |
* 2 | HTTP/* 200
* | ^^^ actual value is <404>
* |
*
* Retry entry 1 (x3 pause 100 ms)
* ------------------------------------------------------------------------------
* Executing entry 1
*
* Cookie store:
*
* Request:
* GET http://localhost:8000/not-found
*
* Request can be run with the following curl command:
* curl 'http://localhost:8000/not-found'
*
> GET /not-found HTTP/1.1
> Host: localhost:8000
> Accept: */*
> User-Agent: hurl/~~~
>
* Response: (received 232 bytes in ~~~ ms)
*
< HTTP/1.0 404 NOT FOUND
< Content-Type: text/html; charset=utf-8
< Content-Length: 232
< Server: Flask Server
< Date: ~~~
<
*
* Assert status code
* --> tests_failed/retry_max_count.hurl:2:8
* |
* 2 | HTTP/* 200
* | ^^^ actual value is <404>
* |
*
* Retry entry 1 (x4 pause 100 ms)
* ------------------------------------------------------------------------------
* Executing entry 1
*
* Cookie store:
*
* Request:
* GET http://localhost:8000/not-found
*
* Request can be run with the following curl command:
* curl 'http://localhost:8000/not-found'
*
> GET /not-found HTTP/1.1
> Host: localhost:8000
> Accept: */*
> User-Agent: hurl/~~~
>
* Response: (received 232 bytes in ~~~ ms)
*
< HTTP/1.0 404 NOT FOUND
< Content-Type: text/html; charset=utf-8
< Content-Length: 232
< Server: Flask Server
< Date: ~~~
<
*
* Assert status code
* --> tests_failed/retry_max_count.hurl:2:8
* |
* 2 | HTTP/* 200
* | ^^^ actual value is <404>
* |
*
* Retry entry 1 (x5 pause 100 ms)
* ------------------------------------------------------------------------------
* Executing entry 1
*
* Cookie store:
*
* Request:
* GET http://localhost:8000/not-found
*
* Request can be run with the following curl command:
* curl 'http://localhost:8000/not-found'
*
> GET /not-found HTTP/1.1
> Host: localhost:8000
> Accept: */*
> User-Agent: hurl/~~~
>
* Response: (received 232 bytes in ~~ ms)
*
< HTTP/1.0 404 NOT FOUND
< Content-Type: text/html; charset=utf-8
< Content-Length: 232
< Server: Flask Server
< Date: ~~~
<
*
*
* Retry max count reached, no more retry
error: Assert status code
--> tests_failed/retry_max_count.hurl:2:8
|
2 | HTTP/* 200
| ^^^ actual value is <404>
|

View File

@ -0,0 +1 @@
4

View File

@ -0,0 +1,2 @@
<pre><code class="language-hurl"><span class="hurl-entry"><span class="request"><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/not-found</span></span>
</span><span class="response"><span class="line"><span class="version">HTTP/*</span> <span class="number">200</span></span></span></span></code></pre>

View File

@ -0,0 +1,2 @@
GET http://localhost:8000/not-found
HTTP/* 200

View File

@ -0,0 +1,6 @@
--retry
--retry-max-count
5
--retry-interval
100
--verbose

View File

@ -0,0 +1,7 @@
from app import app
from flask import request, abort
@app.route("/not-found")
def not_found():
abort(404)

View File

@ -4,4 +4,5 @@
* insecure: false * insecure: false
* max redirect: 50 * max redirect: 50
* retry: false * retry: false
* retry max count: 10
warning: No entry have been executed for file tests_ok/color.hurl warning: No entry have been executed for file tests_ok/color.hurl

View File

@ -23,7 +23,7 @@
< Content-Type: text/html; charset=utf-8 < Content-Type: text/html; charset=utf-8
< Content-Length: 12 < Content-Length: 12
< Server: Flask Server < Server: Flask Server
< Date: ~~~, ~~ ~~~ ~~~~ ~~:~~:~~ GMT < Date: ~~~
< <
* *
* ------------------------------------------------------------------------------ * ------------------------------------------------------------------------------
@ -53,7 +53,7 @@
< Content-Type: text/html; charset=utf-8 < Content-Type: text/html; charset=utf-8
< Content-Length: 12 < Content-Length: 12
< Server: Flask Server < Server: Flask Server
< Date: ~~~, ~~ ~~~ ~~~~ ~~:~~:~~ GMT < Date: ~~~
< <
* Response body: * Response body:
* Hello World! * Hello World!

View File

@ -4,6 +4,7 @@
* insecure: false * insecure: false
* max redirect: 50 * max redirect: 50
* retry: true * retry: true
* retry max count: 10
* ------------------------------------------------------------------------------ * ------------------------------------------------------------------------------
* Executing entry 1 * Executing entry 1
* *

View File

@ -4,6 +4,7 @@
* insecure: false * insecure: false
* max redirect: 50 * max redirect: 50
* retry: false * retry: false
* retry max count: 10
* ------------------------------------------------------------------------------ * ------------------------------------------------------------------------------
* Executing entry 1 * Executing entry 1
* *

View File

@ -4,6 +4,7 @@
* insecure: false * insecure: false
* max redirect: 50 * max redirect: 50
* retry: false * retry: false
* retry max count: 10
* ------------------------------------------------------------------------------ * ------------------------------------------------------------------------------
* Executing entry 1 * Executing entry 1
* *
@ -29,7 +30,7 @@
< Content-Length: 205 < Content-Length: 205
< Location: http://localhost:8000/very-verbose/redirected < Location: http://localhost:8000/very-verbose/redirected
< Server: Flask Server < Server: Flask Server
< Date: ~~~, ~~ ~~~ ~~~~ ~~:~~:~~ GMT < Date: ~~~
< <
* Response body: * Response body:
* <html> * <html>
@ -59,7 +60,7 @@
< Content-Type: text/html; charset=utf-8 < Content-Type: text/html; charset=utf-8
< Content-Length: 11 < Content-Length: 11
< Server: Flask Server < Server: Flask Server
< Date: ~~~, ~~ ~~~ ~~~~ ~~:~~:~~ GMT < Date: ~~~
< <
* Response body: * Response body:
* Redirected. * Redirected.
@ -88,7 +89,7 @@
< Content-Type: text/html; charset=ISO-8859-1 < Content-Type: text/html; charset=ISO-8859-1
< Content-Length: 4 < Content-Length: 4
< Server: Flask Server < Server: Flask Server
< Date: ~~~, ~~ ~~~ ~~~~ ~~:~~:~~ GMT < Date: ~~~
< <
* Response body: * Response body:
* café * café
@ -127,7 +128,7 @@
< Content-Length: 17 < Content-Length: 17
< Content-Encoding: br < Content-Encoding: br
< Server: Flask Server < Server: Flask Server
< Date: ~~~, ~~ ~~~ ~~~~ ~~:~~:~~ GMT < Date: ~~~
< <
* Response body: * Response body:
* Hello World! * Hello World!
@ -156,7 +157,7 @@
< Content-Type: image/jpeg < Content-Type: image/jpeg
< Content-Length: 25992 < Content-Length: 25992
< Server: Flask Server < Server: Flask Server
< Date: ~~~, ~~ ~~~ ~~~~ ~~:~~:~~ GMT < Date: ~~~
< <
* Response body: * Response body:
* Bytes <f198388ba26c2c53005f24643826384f15ba905b8ca070a470b61885c6639f8bbfe63fcee5fb498a630249e499e4eddcc9ca793406c14d02c97107e09c7af57a...> * Bytes <f198388ba26c2c53005f24643826384f15ba905b8ca070a470b61885c6639f8bbfe63fcee5fb498a630249e499e4eddcc9ca793406c14d02c97107e09c7af57a...>
@ -191,7 +192,7 @@
< Content-Type: text/html; charset=utf-8 < Content-Type: text/html; charset=utf-8
< Content-Length: 0 < Content-Length: 0
< Server: Flask Server < Server: Flask Server
< Date: ~~~, ~~ ~~~ ~~~~ ~~:~~:~~ GMT < Date: ~~~
< <
* Response body: * Response body:
* *
@ -222,7 +223,7 @@
< Content-Type: text/html; charset=utf-8 < Content-Type: text/html; charset=utf-8
< Content-Length: 4 < Content-Length: 4
< Server: Flask Server < Server: Flask Server
< Date: ~~~, ~~ ~~~ ~~~~ ~~:~~:~~ GMT < Date: ~~~
< <
* Response body: * Response body:
* Done * Done

View File

@ -56,6 +56,7 @@ pub struct CliOptions {
pub proxy: Option<String>, pub proxy: Option<String>,
pub retry: bool, pub retry: bool,
pub retry_interval: Duration, pub retry_interval: Duration,
pub retry_max_count: Option<usize>,
pub test: bool, pub test: bool,
pub timeout: Duration, pub timeout: Duration,
pub to_entry: Option<usize>, pub to_entry: Option<usize>,
@ -77,6 +78,7 @@ pub fn app(version: &str) -> Command {
let ClientOptions { let ClientOptions {
connect_timeout: default_connect_timeout, connect_timeout: default_connect_timeout,
max_redirect: default_max_redirect, max_redirect: default_max_redirect,
retry_max_count: default_retry_max_count,
timeout: default_timeout, timeout: default_timeout,
.. ..
} = ClientOptions::default(); } = ClientOptions::default();
@ -84,6 +86,7 @@ pub fn app(version: &str) -> Command {
let default_connect_timeout = default_connect_timeout.as_secs(); let default_connect_timeout = default_connect_timeout.as_secs();
let default_max_redirect = default_max_redirect.unwrap(); let default_max_redirect = default_max_redirect.unwrap();
let default_timeout = default_timeout.as_secs(); let default_timeout = default_timeout.as_secs();
let default_retry_max_count = default_retry_max_count.unwrap();
Command::new("hurl") Command::new("hurl")
.about("Run Hurl file(s) or standard input") .about("Run Hurl file(s) or standard input")
@ -289,6 +292,16 @@ pub fn app(version: &str) -> Command {
.default_value("1000") .default_value("1000")
.num_args(1) .num_args(1)
) )
.arg(
clap::Arg::new("retry_max_count")
.long("retry-max-count")
.value_name("NUM")
.help("Maximum number of retries, -1 for unlimited retries")
.default_value(default_retry_max_count.to_string())
.allow_hyphen_values(true)
.value_parser(value_parser!(i32).range(-1..))
.num_args(1)
)
.arg( .arg(
clap::Arg::new("test") clap::Arg::new("test")
.long("test") .long("test")
@ -421,6 +434,11 @@ pub fn parse_options(matches: &ArgMatches) -> Result<CliOptions, CliError> {
let retry = has_flag(matches, "retry"); let retry = has_flag(matches, "retry");
let retry_interval = get::<u64>(matches, "retry_interval").unwrap(); let retry_interval = get::<u64>(matches, "retry_interval").unwrap();
let retry_interval = Duration::from_millis(retry_interval); let retry_interval = Duration::from_millis(retry_interval);
let retry_max_count = get::<i32>(matches, "retry_max_count").unwrap();
let retry_max_count = match retry_max_count {
r if r == -1 => None,
r => Some(r as usize),
};
let timeout = get::<u64>(matches, "max_time").unwrap(); let timeout = get::<u64>(matches, "max_time").unwrap();
let timeout = Duration::from_secs(timeout); let timeout = Duration::from_secs(timeout);
let to_entry = get::<u32>(matches, "to_entry").map(|x| x as usize); let to_entry = get::<u32>(matches, "to_entry").map(|x| x as usize);
@ -454,6 +472,7 @@ pub fn parse_options(matches: &ArgMatches) -> Result<CliOptions, CliError> {
proxy, proxy,
retry, retry,
retry_interval, retry_interval,
retry_max_count,
test, test,
timeout, timeout,
to_entry, to_entry,

View File

@ -27,6 +27,7 @@ pub struct ClientOptions {
pub no_proxy: Option<String>, pub no_proxy: Option<String>,
pub verbosity: Option<Verbosity>, pub verbosity: Option<Verbosity>,
pub insecure: bool, pub insecure: bool,
pub retry_max_count: Option<usize>,
pub timeout: Duration, pub timeout: Duration,
pub connect_timeout: Duration, pub connect_timeout: Duration,
pub user: Option<String>, pub user: Option<String>,
@ -51,6 +52,7 @@ impl Default for ClientOptions {
no_proxy: None, no_proxy: None,
verbosity: None, verbosity: None,
insecure: false, insecure: false,
retry_max_count: Some(10),
timeout: Duration::from_secs(300), timeout: Duration::from_secs(300),
connect_timeout: Duration::from_secs(300), connect_timeout: Duration::from_secs(300),
user: None, user: None,
@ -135,6 +137,7 @@ mod tests {
no_proxy: None, no_proxy: None,
verbosity: None, verbosity: None,
insecure: true, insecure: true,
retry_max_count: Some(10),
timeout: Duration::from_secs(10), timeout: Duration::from_secs(10),
connect_timeout: Duration::from_secs(20), connect_timeout: Duration::from_secs(20),
user: Some("user:password".to_string()), user: Some("user:password".to_string()),

View File

@ -114,6 +114,9 @@ fn execute(
logger.debug(format!(" proxy: {}", proxy).as_str()); logger.debug(format!(" proxy: {}", proxy).as_str());
} }
logger.debug(format!(" retry: {}", cli_options.retry).as_str()); logger.debug(format!(" retry: {}", cli_options.retry).as_str());
if let Some(n) = cli_options.retry_max_count {
logger.debug(format!(" retry max count: {}", n).as_str());
}
if !cli_options.variables.is_empty() { if !cli_options.variables.is_empty() {
logger.debug_important("Variables:"); logger.debug_important("Variables:");
for (name, value) in cli_options.variables.clone() { for (name, value) in cli_options.variables.clone() {
@ -170,6 +173,7 @@ fn execute(
let to_entry = cli_options.to_entry; let to_entry = cli_options.to_entry;
let retry = cli_options.retry; let retry = cli_options.retry;
let retry_interval = cli_options.retry_interval; let retry_interval = cli_options.retry_interval;
let retry_max_count = cli_options.retry_max_count;
let ignore_asserts = cli_options.ignore_asserts; let ignore_asserts = cli_options.ignore_asserts;
let very_verbose = cli_options.very_verbose; let very_verbose = cli_options.very_verbose;
let runner_options = RunnerOptions { let runner_options = RunnerOptions {
@ -189,6 +193,7 @@ fn execute(
proxy, proxy,
retry, retry,
retry_interval, retry_interval,
retry_max_count,
timeout, timeout,
to_entry, to_entry,
user, user,
@ -213,12 +218,7 @@ fn execute(
} }
} }
/// Unwraps a result or exit with message. /// Unwraps a `result` or exit with message.
///
/// # Arguments
///
/// * result - Something to unwrap
/// * logger - A logger to log the error
fn unwrap_or_exit<T>(result: Result<T, CliError>, code: i32, logger: &BaseLogger) -> T { fn unwrap_or_exit<T>(result: Result<T, CliError>, code: i32, logger: &BaseLogger) -> T {
match result { match result {
Ok(v) => v, Ok(v) => v,

View File

@ -42,6 +42,7 @@ pub struct RunnerOptions {
pub proxy: Option<String>, pub proxy: Option<String>,
pub retry: bool, pub retry: bool,
pub retry_interval: Duration, pub retry_interval: Duration,
pub retry_max_count: Option<usize>,
pub timeout: Duration, pub timeout: Duration,
pub to_entry: Option<usize>, pub to_entry: Option<usize>,
pub user: Option<String>, pub user: Option<String>,
@ -75,6 +76,7 @@ impl Default for RunnerOptions {
proxy: None, proxy: None,
retry: false, retry: false,
retry_interval: Duration::from_millis(1000), retry_interval: Duration::from_millis(1000),
retry_max_count: Some(10),
timeout: Duration::from_secs(300), timeout: Duration::from_secs(300),
to_entry: None, to_entry: None,
user: None, user: None,

View File

@ -209,6 +209,7 @@ impl From<&RunnerOptions> for ClientOptions {
Verbosity::VeryVerbose => http::Verbosity::VeryVerbose, Verbosity::VeryVerbose => http::Verbosity::VeryVerbose,
}), }),
insecure: runner_options.insecure, insecure: runner_options.insecure,
retry_max_count: runner_options.retry_max_count,
timeout: runner_options.timeout, timeout: runner_options.timeout,
connect_timeout: runner_options.connect_timeout, connect_timeout: runner_options.connect_timeout,
user: runner_options.user.clone(), user: runner_options.user.clone(),

View File

@ -90,7 +90,7 @@ pub fn run(
let mut entries = vec![]; let mut entries = vec![];
let mut variables = variables.clone(); let mut variables = variables.clone();
let mut entry_index = 1; let mut entry_index = 1;
let mut retry_count = 0; let mut retry_count = 1;
let n = if let Some(to_entry) = runner_options.to_entry { let n = if let Some(to_entry) = runner_options.to_entry {
to_entry to_entry
} else { } else {
@ -104,13 +104,6 @@ pub fn run(
} }
let entry = &hurl_file.entries[entry_index - 1]; let entry = &hurl_file.entries[entry_index - 1];
if let Some(pre_entry) = runner_options.pre_entry {
let exit = pre_entry(entry.clone());
if exit {
break;
}
}
// We compute these new overridden options for this entry, before entering into the `run` // We compute these new overridden options for this entry, before entering into the `run`
// function because entry options can modify the logger and we want the preamble // function because entry options can modify the logger and we want the preamble
// "Executing entry..." to be displayed based on the entry level verbosity. // "Executing entry..." to be displayed based on the entry level verbosity.
@ -122,35 +115,51 @@ pub fn run(
logger.content, logger.content,
); );
if let Some(pre_entry) = runner_options.pre_entry {
let exit = pre_entry(entry.clone());
if exit {
break;
}
}
logger.debug_important( logger.debug_important(
"------------------------------------------------------------------------------", "------------------------------------------------------------------------------",
); );
logger.debug_important(format!("Executing entry {}", entry_index).as_str()); logger.debug_important(format!("Executing entry {}", entry_index).as_str());
let entry_result = let options_result =
match entry::get_entry_options(entry, runner_options, &mut variables, logger) { entry::get_entry_options(entry, runner_options, &mut variables, logger);
Ok(runner_options) => entry::run( let entry_result = match options_result {
entry, Ok(options) => entry::run(
entry_index, entry,
http_client, entry_index,
&mut variables, http_client,
&runner_options, &mut variables,
logger, &options,
), logger,
Err(error) => EntryResult { ),
entry_index, Err(error) => EntryResult {
calls: vec![], entry_index,
captures: vec![], calls: vec![],
asserts: vec![], captures: vec![],
errors: vec![error], asserts: vec![],
time_in_ms: 0, errors: vec![error],
compressed: false, time_in_ms: 0,
}, compressed: false,
}; },
};
// Check if we need to retry. // Check if we need to retry.
let has_error = !entry_result.errors.is_empty(); let has_error = !entry_result.errors.is_empty();
let retry = runner_options.retry && has_error; let retry_max_reached = match runner_options.retry_max_count {
None => false,
Some(r) => retry_count > r,
};
if retry_max_reached {
logger.debug("");
logger.debug_important("Retry max count reached, no more retry");
}
let retry = runner_options.retry && !retry_max_reached && has_error;
// If we're going to retry the entry, we log error only in verbose. Otherwise, // If we're going to retry the entry, we log error only in verbose. Otherwise,
// we log error on stderr. // we log error on stderr.
@ -170,15 +179,13 @@ pub fn run(
} }
} }
if runner_options.retry && has_error { if retry {
let delay = runner_options.retry_interval.as_millis(); let delay = runner_options.retry_interval.as_millis();
logger.debug(""); logger.debug("");
logger.debug_important( logger.debug_important(
format!( format!(
"Retry entry {} (x{} pause {} ms)", "Retry entry {} (x{} pause {} ms)",
entry_index, entry_index, retry_count, delay
retry_count + 1,
delay
) )
.as_str(), .as_str(),
); );
@ -192,7 +199,7 @@ pub fn run(
// We pass to the next entry // We pass to the next entry
entry_index += 1; entry_index += 1;
retry_count = 0; retry_count = 1;
} }
let time_in_ms = start.elapsed().as_millis(); let time_in_ms = start.elapsed().as_millis();