mirror of
https://github.com/Orange-OpenSource/hurl.git
synced 2024-10-26 08:18:26 +03:00
Creates Stdout struct (an indirection to sdtout that can be buffered or written immediately).
This commit is contained in:
parent
2f28dc744f
commit
314987918c
@ -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(())
|
||||
}
|
||||
|
@ -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(())
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user