Add progress bar for tests.

This commit is contained in:
jcamiel 2023-02-07 17:44:08 +01:00
parent 16b51d3265
commit 361fd8bd63
No known key found for this signature in database
GPG Key ID: 07FF11CFD55356CC
5 changed files with 233 additions and 161 deletions

View File

@ -122,7 +122,7 @@ def test(hurl_file: str):
expected_pattern_lines = [parse_pattern(line) for line in expected_lines]
actual_lines = actual.split("\n")
if len(actual_lines) != len(expected_pattern_lines):
print(">>> error in stderr / mismatch in number of lines")
print(">>> error in stdout / mismatch in number of lines")
print(
f"actual: {len(actual_lines)} lines\nexpected: {len(expected_lines)} lines"
)

View File

@ -208,9 +208,7 @@ impl Client {
// Return all request headers (not one by one)
easy::InfoType::HeaderOut => {
let mut lines = split_lines(data);
if verbose {
logger.method_version_out(&lines[0]);
}
logger.debug_method_version_out(&lines[0]);
// Extracts request headers from libcurl debug info.
lines.pop().unwrap(); // Remove last empty line.

View File

@ -15,6 +15,7 @@
* limitations under the License.
*
*/
use std::env;
use std::io::prelude::*;
use std::path::Path;
use std::time::Instant;
@ -87,7 +88,8 @@ fn main() {
);
}
let current_dir = std::env::current_dir();
let progress_bar = cli_options.test && !verbose && !is_ci() && atty::is(Stream::Stderr);
let current_dir = env::current_dir();
let current_dir = unwrap_or_exit(current_dir, EXIT_ERROR_UNDEFINED, &base_logger);
let current_dir = current_dir.as_path();
@ -110,15 +112,15 @@ fn main() {
let logger = builder
.color(color)
.verbose(verbose)
.test(cli_options.test)
.progress_bar(progress_bar)
.filename(filename)
.content(&content)
.build()
.unwrap();
if cli_options.test {
let total = filenames.len();
logger.test_running(current + 1, total);
}
// We try to parse the text file to an HurlFile instance.
let hurl_file = parser::parse_hurl_file(&content);
@ -132,9 +134,7 @@ fn main() {
let hurl_result = execute(&hurl_file, filename, current_dir, &cli_options, &logger);
let success = hurl_result.success;
if cli_options.test {
logger.test_completed(&hurl_result);
}
// We can output the result, either the raw body or a structured JSON representation.
let output_body = success
@ -448,3 +448,9 @@ fn get_summary(duration: u128, runs: &[Run]) -> String {
s.push_str(format!("Duration: {duration} ms\n").as_str());
s
}
/// Whether or not this running in a Continuous Integration environment.
/// Code borrowed from <https://github.com/rust-lang/cargo/blob/master/crates/cargo-util/src/lib.rs>
fn is_ci() -> bool {
env::var("CI").is_ok() || env::var("TF_BUILD").is_ok()
}

View File

@ -117,6 +117,8 @@ pub fn run(
let logger = builder
.color(logger.color)
.verbose(entry_verbosity.is_some())
.test(logger.test)
.progress_bar(entry_verbosity.is_none() && logger.progress_bar)
.filename(logger.filename)
.content(logger.content)
.build()
@ -136,6 +138,8 @@ pub fn run(
warn_deprecated(entry, &logger);
logger.test_progress(entry_index, n);
let options_result =
entry::get_entry_options(entry, runner_options, &mut variables, &logger);
let entry_result = match &options_result {
@ -185,13 +189,14 @@ pub fn run(
// If we're going to retry the entry, we log error only in verbose. Otherwise,
// we log error on stderr.
for e in &entry_result.errors {
logger.test_erase_line();
if retry {
logger.debug_error(e);
} else {
logger.error_rich(e);
}
}
entries.push(entry_result.clone());
entries.push(entry_result);
if let Some(post_entry) = runner_options.post_entry {
let exit = post_entry();
@ -219,6 +224,8 @@ pub fn run(
retry_count = 1;
}
logger.test_erase_line();
let time_in_ms = start.elapsed().as_millis();
let cookies = http_client.get_cookie_storage();
let filename = filename.to_string();

View File

@ -24,56 +24,44 @@ use std::cmp::max;
/// A simple logger to log app related event (start, high levels error, etc...).
/// When we run an [`hurl_core::ast::HurlFile`], user has to provide a dedicated Hurl logger (see [`Logger`]).
pub struct BaseLogger {
pub info: fn(&str),
pub debug: fn(&str),
pub warning: fn(&str),
pub error: fn(&str),
pub color: bool,
pub verbose: bool,
}
impl BaseLogger {
pub fn new(color: bool, verbose: bool) -> BaseLogger {
match (color, verbose) {
(true, true) => BaseLogger {
info: log_info,
debug: log_debug,
warning: log_warning,
error: log_error,
},
(false, true) => BaseLogger {
info: log_info,
debug: log_debug_no_color,
warning: log_warning_no_color,
error: log_error_no_color,
},
(true, false) => BaseLogger {
info: log_info,
debug: |_| {},
warning: log_warning,
error: log_error,
},
(false, false) => BaseLogger {
info: log_info,
debug: |_| {},
warning: log_warning_no_color,
error: log_error_no_color,
},
}
BaseLogger { color, verbose }
}
pub fn info(&self, message: &str) {
(self.info)(message)
log_info(message)
}
pub fn debug(&self, message: &str) {
(self.debug)(message)
if !self.verbose {
return;
}
if self.color {
log_debug(message)
} else {
log_debug_no_color(message)
}
}
pub fn warning(&self, message: &str) {
(self.warning)(message)
if self.color {
log_warning(message)
} else {
log_warning_no_color(message)
}
}
pub fn error(&self, message: &str) {
(self.error)(message)
if self.color {
log_error(message)
} else {
log_error_no_color(message)
}
}
}
@ -81,22 +69,10 @@ impl BaseLogger {
/// rich error for parsing and runtime errors. As the rich errors can display user content,
/// this logger should have access to the content of the file being run.
pub struct Logger<'a> {
info: fn(&str),
debug: fn(&str),
debug_curl: fn(&str),
debug_error: fn(&str, &str, &dyn Error),
debug_header_in: fn(&str, &str),
debug_header_out: fn(&str, &str),
debug_important: fn(&str),
debug_method_version_out: fn(&str),
debug_status_version_in: fn(&str),
warning: fn(&str),
error: fn(&str),
error_rich: fn(&str, &str, &dyn Error),
capture: fn(&str, &Value),
test_running: fn(&str, usize, usize),
test_completed: fn(result: &HurlResult),
pub(crate) color: bool,
pub(crate) verbose: bool,
pub(crate) progress_bar: bool,
pub(crate) test: bool,
pub(crate) filename: &'a str,
pub(crate) content: &'a str,
}
@ -105,6 +81,8 @@ pub struct Logger<'a> {
pub struct LoggerBuilder<'a> {
color: bool,
verbose: bool,
progress_bar: bool,
test: bool,
filename: Option<&'a str>,
content: Option<&'a str>,
}
@ -139,6 +117,18 @@ impl<'a> LoggerBuilder<'a> {
self
}
/// Sets progress bar.
pub fn progress_bar(&mut self, progress_bar: bool) -> &mut Self {
self.progress_bar = progress_bar;
self
}
/// Sets test.
pub fn test(&mut self, test: bool) -> &mut Self {
self.test = test;
self
}
/// Creates a new logger.
pub fn build(&self) -> Result<Logger, &'static str> {
if self.filename.is_none() {
@ -148,151 +138,182 @@ impl<'a> LoggerBuilder<'a> {
return Err("content is not set");
}
let logger = match (self.color, self.verbose) {
(true, true) => Logger {
info: log_info,
debug: log_debug,
debug_curl: log_debug_curl,
debug_error: log_debug_error,
debug_header_in: log_debug_header_in,
debug_header_out: log_debug_header_out,
debug_important: log_debug_important,
debug_method_version_out: log_debug_method_version_out,
debug_status_version_in: log_debug_status_version_in,
warning: log_warning,
error: log_error,
error_rich: log_error_rich,
capture: log_capture,
test_running: log_test_running,
test_completed: log_test_completed,
Ok(Logger {
color: self.color,
verbose: self.verbose,
progress_bar: self.progress_bar,
test: self.test,
filename: self.filename.unwrap(),
content: self.content.unwrap(),
},
(false, true) => Logger {
info: log_info,
debug: log_debug_no_color,
debug_curl: log_debug_curl_no_color,
debug_error: log_debug_error_no_color,
debug_header_in: log_debug_header_in_no_color,
debug_header_out: log_debug_header_out_no_color,
debug_important: log_debug_no_color,
debug_method_version_out: log_debug_method_version_out_no_color,
debug_status_version_in: log_debug_status_version_in_no_color,
warning: log_warning_no_color,
error: log_error_no_color,
error_rich: log_error_rich_no_color,
capture: log_capture_no_color,
test_running: log_test_running_no_color,
test_completed: log_test_completed_no_color,
color: self.color,
filename: self.filename.unwrap(),
content: self.content.unwrap(),
},
(true, false) => Logger {
info: log_info,
debug: |_| {},
debug_curl: |_| {},
debug_error: |_, _, _| {},
debug_header_in: |_, _| {},
debug_header_out: |_, _| {},
debug_important: |_| {},
debug_method_version_out: |_| {},
debug_status_version_in: |_| {},
warning: log_warning,
error: log_error,
error_rich: log_error_rich,
capture: |_, _| {},
test_running: log_test_running,
test_completed: log_test_completed,
color: self.color,
filename: self.filename.unwrap(),
content: self.content.unwrap(),
},
(false, false) => Logger {
info: log_info,
debug: |_| {},
debug_curl: |_| {},
debug_error: |_, _, _| {},
debug_header_in: |_, _| {},
debug_header_out: |_, _| {},
debug_important: |_| {},
debug_method_version_out: |_| {},
debug_status_version_in: |_| {},
warning: log_warning_no_color,
error: log_error_no_color,
error_rich: log_error_rich_no_color,
capture: |_, _| {},
test_running: log_test_running_no_color,
test_completed: log_test_completed_no_color,
color: self.color,
filename: self.filename.unwrap(),
content: self.content.unwrap(),
},
};
Ok(logger)
})
}
}
impl<'a> Logger<'a> {
pub fn info(&self, message: &str) {
(self.info)(message)
log_info(message)
}
pub fn debug(&self, message: &str) {
(self.debug)(message)
if !self.verbose {
return;
}
if self.color {
log_debug(message)
} else {
log_debug_no_color(message)
}
}
pub fn debug_curl(&self, message: &str) {
(self.debug_curl)(message)
if !self.verbose {
return;
}
if self.color {
log_debug_curl(message)
} else {
log_debug_curl_no_color(message)
}
}
pub fn debug_error(&self, error: &dyn Error) {
(self.debug_error)(self.filename, self.content, error)
if !self.verbose {
return;
}
if self.color {
log_debug_error(self.filename, self.content, error)
} else {
log_debug_error_no_color(self.filename, self.content, error)
}
}
pub fn debug_header_in(&self, name: &str, value: &str) {
(self.debug_header_in)(name, value)
if !self.verbose {
return;
}
if self.color {
log_debug_header_in(name, value)
} else {
log_debug_header_in_no_color(name, value)
}
}
pub fn debug_header_out(&self, name: &str, value: &str) {
(self.debug_header_out)(name, value)
if !self.verbose {
return;
}
if self.color {
log_debug_header_out(name, value)
} else {
log_debug_header_out_no_color(name, value)
}
}
pub fn debug_important(&self, message: &str) {
(self.debug_important)(message)
if !self.verbose {
return;
}
if self.color {
log_debug_important(message)
} else {
log_debug_no_color(message)
}
}
pub fn debug_status_version_in(&self, line: &str) {
(self.debug_status_version_in)(line)
if !self.verbose {
return;
}
if self.color {
log_debug_status_version_in(line)
} else {
log_debug_status_version_in_no_color(line)
}
}
pub fn warning(&self, message: &str) {
(self.warning)(message)
if self.color {
log_warning(message)
} else {
log_warning_no_color(message)
}
}
pub fn error(&self, message: &str) {
(self.error)(message)
if self.color {
log_error(message)
} else {
log_error_no_color(message)
}
}
pub fn error_rich(&self, error: &dyn Error) {
(self.error_rich)(self.filename, self.content, error)
if self.color {
log_error_rich(self.filename, self.content, error)
} else {
log_error_rich_no_color(self.filename, self.content, error)
}
}
pub fn method_version_out(&self, line: &str) {
(self.debug_method_version_out)(line)
pub fn debug_method_version_out(&self, line: &str) {
if !self.verbose {
return;
}
if self.color {
log_debug_method_version_out(line)
} else {
log_debug_method_version_out_no_color(line)
}
}
pub fn capture(&self, name: &str, value: &Value) {
(self.capture)(name, value)
if !self.verbose {
return;
}
if self.color {
log_capture(name, value)
} else {
log_capture_no_color(name, value)
}
}
pub fn test_running(&self, current: usize, total: usize) {
(self.test_running)(self.filename, current, total)
if !self.test {
return;
}
if self.color {
log_test_running(self.filename, current, total)
} else {
log_test_running_no_color(self.filename, current, total)
}
}
pub fn test_progress(&self, entry_index: usize, count: usize) {
if !self.progress_bar {
return;
}
log_test_progress(entry_index, count)
}
pub fn test_completed(&self, result: &HurlResult) {
(self.test_completed)(result)
if !self.test {
return;
}
if self.color {
log_test_completed(result)
} else {
log_test_completed_no_color(result)
}
}
pub fn test_erase_line(&self) {
if !self.progress_bar {
return;
}
// This is the "EL - Erase in Line" sequence. It clears from the cursor
// to the end of line.
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
eprint!("\x1B[K");
}
}
@ -432,6 +453,26 @@ fn log_test_running_no_color(filename: &str, current: usize, total: usize) {
eprintln!("{filename}: Running [{current}/{total}]")
}
fn log_test_progress(entry_index: usize, count: usize) {
let progress = progress_string(entry_index, count);
eprint!(" {progress}\r");
}
/// Returns the progress string with the current entry at `entry_index`.
fn progress_string(entry_index: usize, count: usize) -> String {
const WIDTH: usize = 24;
// We report the number of entries already processed.
let progress = (entry_index - 1) as f64 / count as f64;
let col = (progress * WIDTH as f64) as usize;
let completed = if col > 0 {
"=".repeat(col)
} else {
"".to_string()
};
let void = " ".repeat(WIDTH - col - 1);
format!("[{completed}>{void}] {entry_index}/{count}")
}
fn log_test_completed(result: &HurlResult) {
let state = if result.success {
"Success".green().bold()
@ -703,4 +744,24 @@ HTTP/1.0 200
|"#
)
}
#[rustfmt::skip]
#[test]
fn test_progress_string() {
// Progress strings with 20 entries:
assert_eq!(progress_string(1, 20), "[> ] 1/20");
assert_eq!(progress_string(2, 20), "[=> ] 2/20");
assert_eq!(progress_string(5, 20), "[====> ] 5/20");
assert_eq!(progress_string(10, 20), "[==========> ] 10/20");
assert_eq!(progress_string(15, 20), "[================> ] 15/20");
assert_eq!(progress_string(20, 20), "[======================> ] 20/20");
// Progress strings with 3 entries:
assert_eq!(progress_string(1, 3), "[> ] 1/3");
assert_eq!(progress_string(2, 3), "[========> ] 2/3");
assert_eq!(progress_string(3, 3), "[================> ] 3/3");
// Progress strings with 1 entries:
assert_eq!(progress_string(1, 1), "[> ] 1/1");
}
}