Creates Stdout struct (an indirection to sdtout that can be buffered or written immediately).

This commit is contained in:
jcamiel 2024-03-12 10:18:29 +01:00 committed by hurl-bot
parent 2f28dc744f
commit 314987918c
No known key found for this signature in database
GPG Key ID: 1283A2B4A0DCAF8D
6 changed files with 133 additions and 65 deletions

View File

@ -17,6 +17,7 @@
*/
use crate::output::error::Error;
use crate::runner::{HurlResult, Output};
use crate::util::term::{Stdout, WriteMode};
/// Writes the `hurl_result` JSON representation to the file `filename_out`.
///
@ -31,11 +32,12 @@ pub fn write_json(
) -> Result<(), Error> {
let json_result = hurl_result.to_json(content, filename_in);
let serialized = serde_json::to_string(&json_result).unwrap();
let s = format!("{serialized}\n");
let bytes = s.into_bytes();
let mut stdout = Stdout::new(WriteMode::Immediate);
let bytes = format!("{serialized}\n");
let bytes = bytes.into_bytes();
match filename_out {
Some(Output::File(file)) => Output::File(file.to_string()).write(&bytes, None)?,
_ => Output::StdOut.write(&bytes, None)?,
Some(Output::File(file)) => Output::File(file.clone()).write(&bytes, &mut stdout, None)?,
_ => Output::StdOut.write(&bytes, &mut stdout, None)?,
}
Ok(())
}

View File

@ -20,6 +20,7 @@ use hurl_core::ast::{Pos, SourceInfo};
use crate::output::Error;
use crate::runner;
use crate::runner::{EntryResult, Output};
use crate::util::term::{Stdout, WriteMode};
/// Writes the `entry_result` last response to the file `filename_out`.
///
@ -32,6 +33,7 @@ pub fn write_body(
filename_out: &Option<Output>,
) -> Result<(), Error> {
if let Some(call) = entry_result.calls.last() {
let mut stdout = Stdout::new(WriteMode::Immediate);
let response = &call.response;
let mut output = vec![];
@ -60,8 +62,8 @@ pub fn write_body(
output.extend(bytes);
}
match filename_out {
Some(out) => out.write(&output, None)?,
None => Output::StdOut.write(&output, None)?,
Some(out) => out.write(&output, &mut stdout, None)?,
None => Output::StdOut.write(&output, &mut stdout, None)?,
}
}
Ok(())

View File

@ -30,7 +30,7 @@ use crate::runner::progress::{Progress, SeqProgress};
use crate::runner::runner_options::RunnerOptions;
use crate::runner::{entry, options, EntryResult, HurlResult, Value};
use crate::util::logger::{ErrorFormat, Logger, LoggerOptions};
use crate::util::term::{Stderr, WriteMode};
use crate::util::term::{Stderr, Stdout, WriteMode};
/// Runs a Hurl `content` and returns a [`HurlResult`] upon completion.
///
@ -80,13 +80,17 @@ pub fn run(
variables: &HashMap<String, Value>,
logger_options: &LoggerOptions,
) -> Result<HurlResult, String> {
// In this method, we run Hurl content sequentially, so we create a logger that prints debug
// message immediately (in parallel mode, we'll use buffered standard output and error).
// In this method, we run Hurl content sequentially. Standard output and standard error messages
// are written immediately (in parallel mode, we'll use buffered standard output and error).
let mut stdout = Stdout::new(WriteMode::Immediate);
let stderr = Stderr::new(WriteMode::Immediate);
// We also create a common logger for this run (logger verbosity can eventually be mutated on
// each entry) .
let color = logger_options.color;
let error_format = logger_options.error_format;
let filename = &logger_options.filename;
let verbosity = logger_options.verbosity;
let stderr = Stderr::new(WriteMode::Immediate);
let mut logger = Logger::new(color, error_format, filename, verbosity, stderr);
// Create a progress bar for the sequential run, progress will be report in the main thread,
@ -124,6 +128,7 @@ pub fn run(
content,
runner_options,
variables,
&mut stdout,
&progress,
&mut logger,
);
@ -144,6 +149,7 @@ fn run_entries(
content: &str,
runner_options: &RunnerOptions,
variables: &HashMap<String, Value>,
stdout: &mut Stdout,
progress: &dyn Progress,
logger: &mut Logger,
) -> HurlResult {
@ -275,9 +281,12 @@ fn run_entries(
// `entry_result` errors, and optionally deals with retry if we can't write to the
// specified path.
let source_info = entry.source_info();
if let Err(error) =
entry_result.write_response(&output, &runner_options.context_dir, source_info)
{
if let Err(error) = entry_result.write_response(
&output,
&runner_options.context_dir,
stdout,
source_info,
) {
logger.warning(&error.fixme());
}
}

View File

@ -15,14 +15,13 @@
* limitations under the License.
*
*/
use std::fmt;
use std::fs::File;
#[cfg(target_family = "windows")]
use std::io::IsTerminal;
use std::io::Write;
use std::{fmt, io};
use crate::runner::{Error, RunnerError};
use crate::util::path::ContextDir;
use crate::util::term::Stdout;
use hurl_core::ast::{Pos, SourceInfo};
/// Represents the output of write operation: can be either a file or stdout.
@ -44,9 +43,18 @@ impl fmt::Display for Output {
impl Output {
/// Writes these `bytes` to the output.
pub fn write(&self, bytes: &[u8], context_dir: Option<&ContextDir>) -> Result<(), Error> {
///
/// If output is a standard output variant, `stdout` is used to write the bytes.
/// If output is a file variant, an optional `context_dir` can be used to check authorized
/// write access.
pub fn write(
&self,
bytes: &[u8],
stdout: &mut Stdout,
context_dir: Option<&ContextDir>,
) -> Result<(), Error> {
match self {
Output::StdOut => match write_stdout(bytes) {
Output::StdOut => match stdout.write_all(bytes) {
Ok(_) => Ok(()),
Err(e) => Err(Error::new_file_write_access("stdout", &e.to_string())),
},
@ -94,27 +102,3 @@ impl Error {
)
}
}
#[cfg(target_family = "unix")]
fn write_stdout(buf: &[u8]) -> Result<(), io::Error> {
let mut handle = io::stdout().lock();
handle.write_all(buf)?;
Ok(())
}
#[cfg(target_family = "windows")]
fn write_stdout(buf: &[u8]) -> Result<(), io::Error> {
// From <https://doc.rust-lang.org/std/io/struct.Stdout.html>:
// > When operating in a console, the Windows implementation of this stream does not support
// > non-UTF-8 byte sequences. Attempting to write bytes that are not valid UTF-8 will return
// > an error.
// As a workaround to prevent error, we convert the buffer to an UTF-8 string (with potential
// bytes losses) before writing to the standard output of the Windows console.
if io::stdout().is_terminal() {
println!("{}", String::from_utf8_lossy(buf));
} else {
let mut handle = io::stdout().lock();
handle.write_all(buf)?;
}
Ok(())
}

View File

@ -24,6 +24,7 @@ use crate::runner::output::Output;
use crate::runner::value::Value;
use crate::runner::RunnerError;
use crate::util::path::ContextDir;
use crate::util::term::Stdout;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HurlResult {
@ -137,6 +138,7 @@ impl EntryResult {
&self,
output: &Output,
context_dir: &ContextDir,
stdout: &mut Stdout,
source_info: SourceInfo,
) -> Result<(), Error> {
let Some(call) = self.calls.last() else {
@ -164,9 +166,9 @@ impl EntryResult {
return Err(Error::new(source_info, e.into(), false));
}
};
output.write(&bytes, Some(context_dir))
output.write(&bytes, stdout, Some(context_dir))
} else {
output.write(&response.body, Some(context_dir))
output.write(&response.body, stdout, Some(context_dir))
}
}
}

View File

@ -15,11 +15,82 @@
* limitations under the License.
*
*/
use std::io;
#[cfg(target_family = "windows")]
use std::io::IsTerminal;
use std::io::Write;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum WriteMode {
/// Messages are printed immediately.
Immediate,
/// Messages are saved to an internal buffer, and can be retrieved with [`Stdout::buffer`] /
/// [`Stderr::buffer`].
Buffered,
}
/// Indirection for standard output.
///
/// Depending on `mode`, bytes are immediately printed to standard output, or buffered in an
/// internal buffer.
pub struct Stdout {
/// Write mode of the standard output: immediate or saved to a buffer.
mode: WriteMode,
/// Internal buffer, filled when `mode` is [`WriteMode::Buffered`]
buffer: Vec<u8>,
}
impl Stdout {
/// Creates a new standard output, buffered or immediate depending on `mode`.
pub fn new(mode: WriteMode) -> Self {
Stdout {
mode,
buffer: Vec::new(),
}
}
/// Attempts to write an entire buffer into standard output.
pub fn write_all(&mut self, buf: &[u8]) -> Result<(), io::Error> {
match self.mode {
WriteMode::Immediate => write_stdout(buf),
WriteMode::Buffered => self.buffer.write_all(buf),
}
}
/// Returns the buffered standard output.
pub fn buffer(&self) -> &[u8] {
&self.buffer
}
}
#[cfg(target_family = "unix")]
fn write_stdout(buf: &[u8]) -> Result<(), io::Error> {
let mut handle = io::stdout().lock();
handle.write_all(buf)?;
Ok(())
}
#[cfg(target_family = "windows")]
fn write_stdout(buf: &[u8]) -> Result<(), io::Error> {
// From <https://doc.rust-lang.org/std/io/struct.Stdout.html>:
// > When operating in a console, the Windows implementation of this stream does not support
// > non-UTF-8 byte sequences. Attempting to write bytes that are not valid UTF-8 will return
// > an error.
// As a workaround to prevent error, we convert the buffer to an UTF-8 string (with potential
// bytes losses) before writing to the standard output of the Windows console.
if io::stdout().is_terminal() {
println!("{}", String::from_utf8_lossy(buf));
} else {
let mut handle = io::stdout().lock();
handle.write_all(buf)?;
}
Ok(())
}
/// Indirection for standard error.
///
/// Depending on `mode`, messages are immediately printed to standard error, or buffered
/// in an internal buffer.
/// Depending on `mode`, messages are immediately printed to standard error, or buffered in an
/// internal buffer.
///
/// An optional `progress` string can be used to report temporary progress indication to the user.
/// It's always printed as the last lines of the standard error. When the standard error is created
@ -34,17 +105,8 @@ pub struct Stderr {
progress: String,
}
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum WriteMode {
/// Messages are printed immediately.
Immediate,
/// Messages are saved to an internal buffer, and can be retrieved with [`Stderr::buffer`].
Buffered,
}
impl Stderr {
/// Creates a new terminal.
/// Creates a new standard error, buffered or immediate depending on `mode`.
pub fn new(mode: WriteMode) -> Self {
Stderr {
mode,
@ -76,7 +138,6 @@ impl Stderr {
}
}
#[allow(dead_code)]
/// Sets the progress string (only in [`WriteMode::Immediate`] mode).
pub fn set_progress(&mut self, progress: &str) {
match self.mode {
@ -88,7 +149,6 @@ impl Stderr {
}
}
#[allow(dead_code)]
/// Returns the buffered standard error.
pub fn buffer(&self) -> &str {
&self.buffer
@ -97,15 +157,24 @@ impl Stderr {
#[cfg(test)]
mod tests {
use crate::util::term::{Stderr, WriteMode};
use crate::util::term::{Stderr, Stdout, WriteMode};
#[test]
fn term_buffered() {
let mut term = Stderr::new(WriteMode::Buffered);
term.eprintln("toto");
term.set_progress("some progress...\r");
term.eprintln("tutu");
fn buffered_stdout() {
let mut stdout = Stdout::new(WriteMode::Buffered);
stdout.write_all(b"Hello").unwrap();
stdout.write_all(b" ").unwrap();
stdout.write_all(b"World!").unwrap();
assert_eq!(stdout.buffer(), b"Hello World!")
}
assert_eq!(term.buffer(), "toto\ntutu\n")
#[test]
fn buffered_stderr() {
let mut stderr = Stderr::new(WriteMode::Buffered);
stderr.eprintln("toto");
stderr.set_progress("some progress...\r");
stderr.eprintln("tutu");
assert_eq!(stderr.buffer(), "toto\ntutu\n")
}
}