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
* max redirect: 50
* retry: false
* retry max count: 10
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-Length: 12
< Server: Flask Server
< Date: ~~~, ~~ ~~~ ~~~~ ~~:~~:~~ GMT
< Date: ~~~
<
*
* ------------------------------------------------------------------------------
@ -53,7 +53,7 @@
< Content-Type: text/html; charset=utf-8
< Content-Length: 12
< Server: Flask Server
< Date: ~~~, ~~ ~~~ ~~~~ ~~:~~:~~ GMT
< Date: ~~~
<
* Response body:
* Hello World!

View File

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

View File

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

View File

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

View File

@ -56,6 +56,7 @@ pub struct CliOptions {
pub proxy: Option<String>,
pub retry: bool,
pub retry_interval: Duration,
pub retry_max_count: Option<usize>,
pub test: bool,
pub timeout: Duration,
pub to_entry: Option<usize>,
@ -77,6 +78,7 @@ pub fn app(version: &str) -> Command {
let ClientOptions {
connect_timeout: default_connect_timeout,
max_redirect: default_max_redirect,
retry_max_count: default_retry_max_count,
timeout: default_timeout,
..
} = ClientOptions::default();
@ -84,6 +86,7 @@ pub fn app(version: &str) -> Command {
let default_connect_timeout = default_connect_timeout.as_secs();
let default_max_redirect = default_max_redirect.unwrap();
let default_timeout = default_timeout.as_secs();
let default_retry_max_count = default_retry_max_count.unwrap();
Command::new("hurl")
.about("Run Hurl file(s) or standard input")
@ -289,6 +292,16 @@ pub fn app(version: &str) -> Command {
.default_value("1000")
.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(
clap::Arg::new("test")
.long("test")
@ -421,6 +434,11 @@ pub fn parse_options(matches: &ArgMatches) -> Result<CliOptions, CliError> {
let retry = has_flag(matches, "retry");
let retry_interval = get::<u64>(matches, "retry_interval").unwrap();
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 = Duration::from_secs(timeout);
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,
retry,
retry_interval,
retry_max_count,
test,
timeout,
to_entry,

View File

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

View File

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

View File

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

View File

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

View File

@ -90,7 +90,7 @@ pub fn run(
let mut entries = vec![];
let mut variables = variables.clone();
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 {
to_entry
} else {
@ -104,13 +104,6 @@ pub fn run(
}
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`
// function because entry options can modify the logger and we want the preamble
// "Executing entry..." to be displayed based on the entry level verbosity.
@ -122,35 +115,51 @@ pub fn run(
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(format!("Executing entry {}", entry_index).as_str());
let entry_result =
match entry::get_entry_options(entry, runner_options, &mut variables, logger) {
Ok(runner_options) => entry::run(
entry,
entry_index,
http_client,
&mut variables,
&runner_options,
logger,
),
Err(error) => EntryResult {
entry_index,
calls: vec![],
captures: vec![],
asserts: vec![],
errors: vec![error],
time_in_ms: 0,
compressed: false,
},
};
let options_result =
entry::get_entry_options(entry, runner_options, &mut variables, logger);
let entry_result = match options_result {
Ok(options) => entry::run(
entry,
entry_index,
http_client,
&mut variables,
&options,
logger,
),
Err(error) => EntryResult {
entry_index,
calls: vec![],
captures: vec![],
asserts: vec![],
errors: vec![error],
time_in_ms: 0,
compressed: false,
},
};
// Check if we need to retry.
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,
// 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();
logger.debug("");
logger.debug_important(
format!(
"Retry entry {} (x{} pause {} ms)",
entry_index,
retry_count + 1,
delay
entry_index, retry_count, delay
)
.as_str(),
);
@ -192,7 +199,7 @@ pub fn run(
// We pass to the next entry
entry_index += 1;
retry_count = 0;
retry_count = 1;
}
let time_in_ms = start.elapsed().as_millis();