Create Term structure to manage immediate and buffered stdout/stderr.

This commit is contained in:
jcamiel 2024-03-03 13:38:43 +01:00
parent 831dce6c8c
commit 3c4646dfdd
No known key found for this signature in database
GPG Key ID: 07FF11CFD55356CC
7 changed files with 205 additions and 275 deletions

View File

@ -196,10 +196,11 @@ impl Client {
// Logs method, version and request headers now.
if verbose {
logger.debug_method_version_out(&lines[0]);
for header in &request_headers {
logger.debug_header_out(&header.name, &header.value);
}
logger.info(">");
let headers = request_headers
.iter()
.map(|h| (h.name.as_str(), h.value.as_str()))
.collect::<Vec<_>>();
logger.debug_headers_out(&headers);
}
// If we don't send any data, we log an empty body here instead of relying on
@ -309,10 +310,13 @@ impl Client {
.filter(|s| s.starts_with("HTTP/"))
.for_each(|s| logger.debug_status_version_in(s.trim()));
for header in &response.headers {
logger.debug_header_in(&header.name, &header.value);
}
logger.info("<");
let headers = response
.headers
.iter()
.map(|h| (h.name.as_str(), h.value.as_str()))
.collect::<Vec<_>>();
logger.debug_headers_in(&headers);
if very_verbose {
logger.debug_important("Response body:");
response.log_body(true, logger);

View File

@ -39,7 +39,7 @@ impl Response {
}
}
pub fn log_all(&self, logger: &Logger) {
pub fn log_info_all(&self, logger: &Logger) {
let status_line = self.get_status_line_headers(logger.color);
logger.info(&status_line);
self.log_body(false, logger);

View File

@ -68,8 +68,6 @@ pub fn write_body(
Some(Output::File(file)) => Output::File(file.to_string()).write(&output, None)?,
_ => runner::Output::StdOut.write(&output, None)?,
}
} else {
logger.info("No response has been received");
}
} else {
let source = if filename_in == "-" {

View File

@ -90,6 +90,7 @@ pub fn run(
}
};
// Now, we have a syntactically correct HurlFile instance, we can run it.
log_run_info(&hurl_file, runner_options, variables, &logger);
let result = run_entries(
@ -113,7 +114,6 @@ fn run_entries(
variables: &HashMap<String, Value>,
logger: &mut Logger,
) -> HurlResult {
// Now, we have a syntactically correct HurlFile instance, we can run it.
let mut http_client = Client::new();
let mut entries_result = vec![];
let mut variables = variables.clone();
@ -477,10 +477,7 @@ fn log_errors(entry_result: &EntryResult, content: &str, retry: bool, logger: &L
if logger.error_format == ErrorFormat::Long {
if let Some(Call { response, .. }) = entry_result.calls.last() {
response.log_all(logger);
} else {
logger.info("<No HTTP response>");
logger.info("");
response.log_info_all(logger);
}
}
entry_result

View File

@ -22,6 +22,7 @@ use hurl_core::ast::SourceInfo;
use hurl_core::error::Error;
use crate::runner::{HurlResult, Value};
use crate::util::term::{Term, WriteMode};
/// 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`]).
@ -36,7 +37,7 @@ impl BaseLogger {
}
pub fn info(&self, message: &str) {
log_info(message);
eprintln!("{message}");
}
pub fn debug(&self, message: &str) {
@ -44,25 +45,25 @@ impl BaseLogger {
return;
}
if self.color {
log_debug(message);
eprintln!("{} {message}", "*".blue().bold());
} else {
log_debug_no_color(message);
eprintln!("* {message}");
}
}
pub fn warning(&self, message: &str) {
if self.color {
log_warning(message);
eprintln!("{}: {}", "warning".yellow().bold(), message.bold());
} else {
log_warning_no_color(message);
eprintln!("warning: {message}");
}
}
pub fn error(&self, message: &str) {
if self.color {
log_error(message);
eprintln!("{}: {}", "error".red().bold(), message.bold());
} else {
log_error_no_color(message);
eprintln!("error: {message}");
}
}
}
@ -99,6 +100,7 @@ pub struct Logger {
pub(crate) progress_bar: bool,
pub(crate) test: bool,
pub(crate) verbosity: Option<Verbosity>,
term: Term,
}
impl From<&LoggerOptions> for Logger {
@ -110,6 +112,7 @@ impl From<&LoggerOptions> for Logger {
progress_bar: options.progress_bar,
test: options.test,
verbosity: options.verbosity,
term: Term::new(WriteMode::Immediate),
}
}
}
@ -204,7 +207,7 @@ impl Default for LoggerOptionsBuilder {
impl Logger {
pub fn info(&self, message: &str) {
log_info(message);
self.term.eprintln(message);
}
pub fn debug(&self, message: &str) {
@ -212,53 +215,9 @@ impl Logger {
return;
}
if self.color {
log_debug(message);
self.term.eprintln_prefix(&"*".blue().bold(), message);
} else {
log_debug_no_color(message);
}
}
pub fn debug_curl(&self, message: &str) {
if self.verbosity.is_none() {
return;
}
if self.color {
log_debug_curl(message);
} else {
log_debug_curl_no_color(message);
}
}
pub fn debug_error<E: Error>(&self, content: &str, error: &E, entry_src_info: SourceInfo) {
if self.verbosity.is_none() {
return;
}
if self.color {
log_debug_error(&self.filename, content, error, entry_src_info);
} else {
log_debug_error_no_color(&self.filename, content, error, entry_src_info);
}
}
pub fn debug_header_in(&self, name: &str, value: &str) {
if self.verbosity.is_none() {
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) {
if self.verbosity.is_none() {
return;
}
if self.color {
log_debug_header_out(name, value);
} else {
log_debug_header_out_no_color(name, value);
self.term.eprintln_prefix("*", message);
}
}
@ -267,45 +226,103 @@ impl Logger {
return;
}
if self.color {
log_debug_important(message);
self.term
.eprintln_prefix(&"*".blue().bold(), &message.bold());
} else {
log_debug_no_color(message);
self.term.eprintln_prefix("*", message);
}
}
pub fn debug_curl(&self, message: &str) {
if self.verbosity.is_none() {
return;
}
if self.color {
self.term.eprintln_prefix(&"**".blue().bold(), message);
} else {
self.term.eprintln_prefix("**", message);
}
}
pub fn debug_error<E: Error>(&self, content: &str, error: &E, entry_src_info: SourceInfo) {
if self.verbosity.is_none() {
return;
}
let message = error_string(
&self.filename,
content,
error,
Some(entry_src_info),
self.color,
);
split_lines(&message).iter().for_each(|l| self.debug(l));
}
pub fn debug_headers_in(&self, headers: &[(&str, &str)]) {
if self.verbosity.is_none() {
return;
}
for (name, value) in headers {
if self.color {
self.term
.eprintln(&format!("< {}: {}", name.cyan().bold(), value));
} else {
self.term.eprintln(&format!("< {}: {}", name, value));
}
}
self.term.eprintln("<");
}
pub fn debug_headers_out(&self, headers: &[(&str, &str)]) {
if self.verbosity.is_none() {
return;
}
for (name, value) in headers {
if self.color {
self.term
.eprintln(&format!("> {}: {}", name.cyan().bold(), value));
} else {
self.term.eprintln(&format!("> {}: {}", name, value));
}
}
self.term.eprintln(">");
}
pub fn debug_status_version_in(&self, line: &str) {
if self.verbosity.is_none() {
return;
}
if self.color {
log_debug_status_version_in(line);
self.term.eprintln(&format!("< {}", line.green().bold()));
} else {
log_debug_status_version_in_no_color(line);
self.term.eprintln(&format!("< {line}"));
}
}
pub fn warning(&self, message: &str) {
if self.color {
log_warning(message);
self.term.eprintln(&format!(
"{}: {}",
"warning".yellow().bold(),
message.bold()
));
} else {
log_warning_no_color(message);
self.term.eprintln(&format!("warning: {message}"));
}
}
pub fn error(&self, message: &str) {
if self.color {
log_error(message);
self.term
.eprintln(&format!("{}: {}", "error".red().bold(), message.bold()));
} else {
log_error_no_color(message);
self.term.eprintln(&format!("error: {message}"));
}
}
pub fn error_parsing_rich<E: Error>(&self, content: &str, error: &E) {
if self.color {
log_error_rich(&self.filename, content, error, None);
} else {
log_error_rich_no_color(&self.filename, content, error, None);
}
let message = error_string(&self.filename, content, error, None, self.color);
self.error_rich(&message);
}
pub fn error_runtime_rich<E: Error>(
@ -314,10 +331,22 @@ impl Logger {
error: &E,
entry_src_info: SourceInfo,
) {
let message = error_string(
&self.filename,
content,
error,
Some(entry_src_info),
self.color,
);
self.error_rich(&message);
}
fn error_rich(&self, message: &str) {
if self.color {
log_error_rich(&self.filename, content, error, Some(entry_src_info));
self.term
.eprintln(&format!("{}: {message}\n", "error".red().bold()));
} else {
log_error_rich_no_color(&self.filename, content, error, Some(entry_src_info));
self.term.eprintln(&format!("error: {message}\n"));
}
}
@ -326,9 +355,9 @@ impl Logger {
return;
}
if self.color {
log_debug_method_version_out(line);
self.term.eprintln(&format!("> {}", line.purple().bold()));
} else {
log_debug_method_version_out_no_color(line);
self.term.eprintln(&format!("> {line}"));
}
}
@ -337,20 +366,42 @@ impl Logger {
return;
}
if self.color {
log_capture(name, value);
self.term.eprintln(&format!(
"{} {}: {value}",
"*".blue().bold(),
name.yellow().bold()
));
} else {
log_capture_no_color(name, value);
self.term.eprintln(&format!("* {name}: {value}"));
}
}
}
impl Term {
fn eprintln_prefix(&self, prefix: &str, message: &str) {
if message.is_empty() {
self.eprintln(prefix);
} else {
self.eprintln(&format!("{prefix} {message}"));
}
}
}
/// WIP: These methods must be removed from `Logger`. Semantically, they're using for reporting
/// test progress and we need to extract them from `Logger`.
impl Logger {
pub fn test_running(&self, current: usize, total: usize) {
if !self.test {
return;
}
if self.color {
log_test_running(&self.filename, current, total);
eprintln!(
"{}: {} [{current}/{total}]",
self.filename.bold(),
"Running".cyan().bold()
);
} else {
log_test_running_no_color(&self.filename, current, total);
eprintln!("{}: Running [{current}/{total}]", self.filename);
}
}
@ -358,7 +409,8 @@ impl Logger {
if !self.progress_bar {
return;
}
log_test_progress(entry_index, count);
let progress = progress_string(entry_index, count);
eprint!(" {progress}\r");
}
pub fn test_completed(&self, result: &HurlResult) {
@ -366,9 +418,26 @@ impl Logger {
return;
}
if self.color {
log_test_completed(result, &self.filename);
let state = if result.success {
"Success".green().bold()
} else {
"Failure".red().bold()
};
let count = result.entries.iter().flat_map(|r| &r.calls).count();
eprintln!(
"{}: {} ({} request(s) in {} ms)",
self.filename.bold(),
state,
count,
result.time_in_ms
);
} else {
log_test_completed_no_color(result, &self.filename);
let state = if result.success { "Success" } else { "Failure" };
let count = result.entries.iter().flat_map(|r| &r.calls).count();
eprintln!(
"{}: {} ({} request(s) in {} ms)",
self.filename, state, count, result.time_in_ms
);
}
}
@ -383,162 +452,6 @@ impl Logger {
}
}
fn log_info(message: &str) {
eprintln!("{message}");
}
fn log_debug(message: &str) {
if message.is_empty() {
eprintln!("{}", "*".blue().bold());
} else {
eprintln!("{} {}", "*".blue().bold(), message);
}
}
fn log_debug_no_color(message: &str) {
if message.is_empty() {
eprintln!("*");
} else {
eprintln!("* {message}");
}
}
fn log_debug_curl(message: &str) {
if message.is_empty() {
eprintln!("{}", "**".blue().bold());
} else {
eprintln!("{} {}", "**".blue().bold(), message.green());
}
}
fn log_debug_curl_no_color(message: &str) {
if message.is_empty() {
eprintln!("**");
} else {
eprintln!("** {message}");
}
}
fn log_debug_important(message: &str) {
if message.is_empty() {
eprintln!("{}", "*".blue().bold());
} else {
eprintln!("{} {}", "*".blue().bold(), message.bold());
}
}
fn log_debug_error<E: Error>(filename: &str, content: &str, error: &E, entry_src_info: SourceInfo) {
let message = error_string(filename, content, error, Some(entry_src_info), true);
split_lines(&message).iter().for_each(|l| log_debug(l));
}
fn log_debug_error_no_color<E: Error>(
filename: &str,
content: &str,
error: &E,
entry_src_info: SourceInfo,
) {
let message = error_string(filename, content, error, Some(entry_src_info), false);
split_lines(&message)
.iter()
.for_each(|l| log_debug_no_color(l));
}
fn log_debug_header_in(name: &str, value: &str) {
eprintln!("< {}: {}", name.cyan().bold(), value);
}
fn log_debug_header_in_no_color(name: &str, value: &str) {
eprintln!("< {name}: {value}");
}
fn log_debug_header_out(name: &str, value: &str) {
eprintln!("> {}: {}", name.cyan().bold(), value);
}
fn log_debug_header_out_no_color(name: &str, value: &str) {
eprintln!("> {name}: {value}");
}
fn log_debug_method_version_out(line: &str) {
eprintln!("> {}", line.purple().bold());
}
fn log_debug_method_version_out_no_color(line: &str) {
eprintln!("> {line}");
}
fn log_debug_status_version_in(line: &str) {
eprintln!("< {}", line.green().bold());
}
fn log_debug_status_version_in_no_color(line: &str) {
eprintln!("< {line}");
}
fn log_warning(message: &str) {
eprintln!("{}: {}", "warning".yellow().bold(), message.bold());
}
fn log_warning_no_color(message: &str) {
eprintln!("warning: {message}");
}
fn log_error(message: &str) {
eprintln!("{}: {}", "error".red().bold(), message.bold());
}
fn log_error_no_color(message: &str) {
eprintln!("error: {message}");
}
fn log_error_rich<E: Error>(
filename: &str,
content: &str,
error: &E,
entry_src_info: Option<SourceInfo>,
) {
let message = error_string(filename, content, error, entry_src_info, true);
eprintln!("{}: {}\n", "error".red().bold(), &message);
}
fn log_error_rich_no_color<E: Error>(
filename: &str,
content: &str,
error: &E,
entry_src_info: Option<SourceInfo>,
) {
let message = error_string(filename, content, error, entry_src_info, false);
eprintln!("error: {}\n", &message);
}
fn log_capture(name: &str, value: &Value) {
eprintln!("{} {}: {}", "*".blue().bold(), name.yellow().bold(), value);
}
fn log_capture_no_color(name: &str, value: &Value) {
eprintln!("* {name}: {value}");
}
fn log_test_running(filename: &str, current: usize, total: usize) {
eprintln!(
"{}: {} [{}/{}]",
filename.bold(),
"Running".cyan().bold(),
current,
total
);
}
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;
@ -554,31 +467,6 @@ fn progress_string(entry_index: usize, count: usize) -> String {
format!("[{completed}>{void}] {entry_index}/{count}")
}
fn log_test_completed(result: &HurlResult, filename: &str) {
let state = if result.success {
"Success".green().bold()
} else {
"Failure".red().bold()
};
let count = result.entries.iter().flat_map(|r| &r.calls).count();
eprintln!(
"{}: {} ({} request(s) in {} ms)",
filename.bold(),
state,
count,
result.time_in_ms
);
}
fn log_test_completed_no_color(result: &HurlResult, filename: &str) {
let state = if result.success { "Success" } else { "Failure" };
let count = result.entries.iter().flat_map(|r| &r.calls).count();
eprintln!(
"{}: {} ({} request(s) in {} ms)",
filename, state, count, result.time_in_ms
);
}
/// Returns the string representation of an `error`, given `lines` of content and a `filename`.
///
/// The source information where the error occurred can be retrieved in `error`; optionally,

View File

@ -17,3 +17,4 @@
*/
pub mod logger;
pub mod path;
mod term;

View File

@ -0,0 +1,42 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2024 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
#[derive(Clone)]
pub struct Term {
mode: WriteMode,
}
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum WriteMode {
Immediate,
Buffered,
}
impl Term {
pub fn new(mode: WriteMode) -> Self {
Term { mode }
}
pub fn eprintln(&self, message: &str) {
match self.mode {
WriteMode::Immediate => eprintln!("{message}"),
WriteMode::Buffered => {}
}
}
}