Using explicit stdout output

This commit is contained in:
Fabrice Reix 2024-01-14 17:14:30 +01:00
parent 3927dcc622
commit 65e00babca
No known key found for this signature in database
GPG Key ID: BF5213154B2E7155
17 changed files with 130 additions and 41 deletions

View File

@ -0,0 +1,30 @@
* ------------------------------------------------------------------------------
* Executing entry 1
*
* Entry options:
* output: -
*
* Cookie store:
*
* Request:
* GET http://localhost:8000/stdout/text
*
* Request can be run with the following curl command:
* curl --output - 'http://localhost:8000/stdout/text'
*
> GET /stdout/text HTTP/1.1
> Host: localhost:8000
> Accept: */*
> User-Agent: hurl/~~~
>
* Response: (received 5 bytes in ~~~ ms)
*
< HTTP/1.1 200 OK
< Server: Werkzeug/~~~
< Date: ~~~
< Content-Type: text/html; charset=utf-8
< Content-Length: 5
< Server: Flask Server
< Connection: close
<
*

View File

@ -0,0 +1,8 @@
GET http://localhost:8000/stdout/text
[Options]
output: -
HTTP 200
`Hello`

View File

@ -0,0 +1 @@
HelloHello

View File

@ -0,0 +1,5 @@
Set-StrictMode -Version latest
$ErrorActionPreference = 'Stop'
hurl --verbose --output - tests_ok/stdout.hurl

View File

@ -0,0 +1,7 @@
# coding=utf-8
from app import app
@app.route("/stdout/text")
def stdout_text():
return "Hello"

View File

@ -0,0 +1,5 @@
#!/bin/bash
set -Eeuo pipefail
hurl --verbose tests_ok/stdout.hurl

View File

@ -28,7 +28,7 @@ use hurl_core::ast::Retry;
use super::variables::{parse as parse_variable, parse_value}; use super::variables::{parse as parse_variable, parse_value};
use super::OptionsError; use super::OptionsError;
use crate::cli::options::{ErrorFormat, HttpVersion, IpResolve}; use crate::cli::options::{ErrorFormat, HttpVersion, IpResolve, Output};
use crate::cli::OutputType; use crate::cli::OutputType;
pub fn cacert_file(arg_matches: &ArgMatches) -> Result<Option<String>, OptionsError> { pub fn cacert_file(arg_matches: &ArgMatches) -> Result<Option<String>, OptionsError> {
@ -254,8 +254,14 @@ pub fn no_proxy(arg_matches: &ArgMatches) -> Option<String> {
get::<String>(arg_matches, "noproxy") get::<String>(arg_matches, "noproxy")
} }
pub fn output(arg_matches: &ArgMatches) -> Option<String> { pub fn output(arg_matches: &ArgMatches) -> Option<Output> {
get::<String>(arg_matches, "output") get::<String>(arg_matches, "output").map(|filename| {
if filename == "-" {
Output::StdOut
} else {
Output::File(filename)
}
})
} }
pub fn output_type(arg_matches: &ArgMatches) -> OutputType { pub fn output_type(arg_matches: &ArgMatches) -> OutputType {

View File

@ -27,6 +27,7 @@ use std::time::Duration;
use clap::ArgMatches; use clap::ArgMatches;
use hurl::http; use hurl::http;
use hurl::http::RequestedHttpVersion; use hurl::http::RequestedHttpVersion;
use hurl::runner::Output;
use hurl::util::logger::{LoggerOptions, LoggerOptionsBuilder, Verbosity}; use hurl::util::logger::{LoggerOptions, LoggerOptionsBuilder, Verbosity};
use hurl::util::path::ContextDir; use hurl::util::path::ContextDir;
use hurl_core::ast::{Entry, Retry}; use hurl_core::ast::{Entry, Retry};
@ -63,7 +64,7 @@ pub struct Options {
pub junit_file: Option<String>, pub junit_file: Option<String>,
pub max_redirect: Option<usize>, pub max_redirect: Option<usize>,
pub no_proxy: Option<String>, pub no_proxy: Option<String>,
pub output: Option<String>, pub output: Option<Output>,
pub output_type: OutputType, pub output_type: OutputType,
pub path_as_is: bool, pub path_as_is: bool,
pub progress_bar: bool, pub progress_bar: bool,

View File

@ -35,6 +35,7 @@ use crate::http::request_spec::*;
use crate::http::response::*; use crate::http::response::*;
use crate::http::timings::Timings; use crate::http::timings::Timings;
use crate::http::{easy_ext, Call, Header, HttpError, Verbosity}; use crate::http::{easy_ext, Call, Header, HttpError, Verbosity};
use crate::runner::Output;
use crate::util::logger::Logger; use crate::util::logger::Logger;
use crate::util::path::ContextDir; use crate::util::path::ContextDir;
@ -737,7 +738,7 @@ impl Client {
&mut self, &mut self,
request_spec: &RequestSpec, request_spec: &RequestSpec,
context_dir: &ContextDir, context_dir: &ContextDir,
output: Option<&str>, output: Option<&Output>,
options: &ClientOptions, options: &ClientOptions,
) -> String { ) -> String {
let mut arguments = vec!["curl".to_string()]; let mut arguments = vec!["curl".to_string()];
@ -973,7 +974,7 @@ mod tests {
let data = b"GET /hello HTTP/1.1\r\nHost: localhost:8000\r\n\r\n"; let data = b"GET /hello HTTP/1.1\r\nHost: localhost:8000\r\n\r\n";
let lines = split_lines(data); let lines = split_lines(data);
assert_eq!(lines.len(), 3); assert_eq!(lines.len(), 3);
assert_eq!(lines.get(0).unwrap().as_str(), "GET /hello HTTP/1.1"); assert_eq!(lines.first().unwrap().as_str(), "GET /hello HTTP/1.1");
assert_eq!(lines.get(1).unwrap().as_str(), "Host: localhost:8000"); assert_eq!(lines.get(1).unwrap().as_str(), "Host: localhost:8000");
assert_eq!(lines.get(2).unwrap().as_str(), ""); assert_eq!(lines.get(2).unwrap().as_str(), "");
} }
@ -1118,7 +1119,8 @@ mod tests {
..Default::default() ..Default::default()
}; };
let context_dir = ContextDir::default(); let context_dir = ContextDir::default();
let output = Some("/tmp/foo.bin"); let file = Output::File("/tmp/foo.bin".to_string());
let output = Some(&file);
let options = ClientOptions { let options = ClientOptions {
aws_sigv4: Some("aws:amz:sts".to_string()), aws_sigv4: Some("aws:amz:sts".to_string()),
cacert_file: Some("/etc/cert.pem".to_string()), cacert_file: Some("/etc/cert.pem".to_string()),

View File

@ -27,15 +27,15 @@ pub fn write_json(
hurl_result: &HurlResult, hurl_result: &HurlResult,
content: &str, content: &str,
filename_in: &str, filename_in: &str,
filename_out: &Option<String>, filename_out: &Option<Output>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let json_result = hurl_result.to_json(content, filename_in); let json_result = hurl_result.to_json(content, filename_in);
let serialized = serde_json::to_string(&json_result).unwrap(); let serialized = serde_json::to_string(&json_result).unwrap();
let s = format!("{serialized}\n"); let s = format!("{serialized}\n");
let bytes = s.into_bytes(); let bytes = s.into_bytes();
match filename_out { match filename_out {
Some(file) => Output::File(file.to_string()).write(&bytes)?, Some(Output::File(file)) => Output::File(file.to_string()).write(&bytes)?,
None => Output::StdOut.write(&bytes)?, _ => Output::StdOut.write(&bytes)?,
} }
Ok(()) Ok(())
} }

View File

@ -31,7 +31,7 @@ pub fn write_body(
filename_in: &str, filename_in: &str,
include_headers: bool, include_headers: bool,
color: bool, color: bool,
filename_out: &Option<String>, filename_out: &Option<Output>,
logger: &Logger, logger: &Logger,
) -> Result<(), Error> { ) -> Result<(), Error> {
// By default, we output the body response bytes of the last entry // By default, we output the body response bytes of the last entry
@ -65,8 +65,8 @@ pub fn write_body(
output.extend(bytes); output.extend(bytes);
} }
match filename_out { match filename_out {
Some(file) => Output::File(file.to_string()).write(&output)?, Some(Output::File(file)) => Output::File(file.to_string()).write(&output)?,
None => Output::StdOut.write(&output)?, _ => runner::Output::StdOut.write(&output)?,
} }
} else { } else {
logger.info("No response has been received"); logger.info("No response has been received");

View File

@ -84,13 +84,9 @@ pub fn run(
log_request_spec(&http_request, logger); log_request_spec(&http_request, logger);
logger.debug("Request can be run with the following curl command:"); logger.debug("Request can be run with the following curl command:");
let output = &runner_options.output; let output = runner_options.output.clone();
let curl_command = http_client.curl_command_line( let curl_command =
&http_request, http_client.curl_command_line(&http_request, context_dir, output.as_ref(), &client_options);
context_dir,
output.as_deref(),
&client_options,
);
logger.debug(curl_command.as_str()); logger.debug(curl_command.as_str());
logger.debug(""); logger.debug("");

View File

@ -28,7 +28,7 @@ use hurl_core::parser;
use crate::http::Call; use crate::http::Call;
use crate::runner::runner_options::RunnerOptions; use crate::runner::runner_options::RunnerOptions;
use crate::runner::{entry, options, EntryResult, HurlResult, RunnerError, Value}; use crate::runner::{entry, options, EntryResult, HurlResult, Output, RunnerError, Value};
use crate::util::logger::{ErrorFormat, Logger, LoggerOptions, LoggerOptionsBuilder}; use crate::util::logger::{ErrorFormat, Logger, LoggerOptions, LoggerOptionsBuilder};
use crate::{http, runner}; use crate::{http, runner};
@ -220,14 +220,25 @@ pub fn run(
// an error. If we want to treat it as an error, we've to add it to the current // an error. If we want to treat it as an error, we've to add it to the current
// `entry_result` errors, and optionally deals with retry if we can't write to the // `entry_result` errors, and optionally deals with retry if we can't write to the
// specified path. // specified path.
if !runner_options.context_dir.is_access_allowed(&output) {
let inner = RunnerError::UnauthorizedFileAccess { let authorized = if let Output::File(filename) = &output {
path: PathBuf::from(output.clone()), if !runner_options.context_dir.is_access_allowed(filename) {
}; let inner = RunnerError::UnauthorizedFileAccess {
let error = runner::Error::new(entry.request.source_info, inner, false); path: PathBuf::from(filename.clone()),
logger.warning(&error.fixme()); };
} else if let Err(error) = entry_result.write_response(output) { let error = runner::Error::new(entry.request.source_info, inner, false);
logger.warning(&error.fixme()); logger.warning(&error.fixme());
false
} else {
true
}
} else {
true
};
if authorized {
if let Err(error) = entry_result.write_response(&output) {
logger.warning(&error.fixme());
}
} }
} }
} }
@ -508,7 +519,7 @@ mod test {
let non_default_options = get_non_default_options(&options); let non_default_options = get_non_default_options(&options);
assert_eq!(non_default_options.len(), 1); assert_eq!(non_default_options.len(), 1);
let first_non_default = non_default_options.get(0).unwrap(); let first_non_default = non_default_options.first().unwrap();
assert_eq!(first_non_default.0, "delay"); assert_eq!(first_non_default.0, "delay");
assert_eq!(first_non_default.1, "500ms"); assert_eq!(first_non_default.1, "500ms");

View File

@ -25,7 +25,7 @@ use hurl_core::ast::{
use crate::http::{IpResolve, RequestedHttpVersion}; use crate::http::{IpResolve, RequestedHttpVersion};
use crate::runner::template::{eval_expression, eval_template}; use crate::runner::template::{eval_expression, eval_template};
use crate::runner::{Error, Number, RunnerError, RunnerOptions, Value}; use crate::runner::{Error, Number, Output, RunnerError, RunnerOptions, Value};
use crate::util::logger::{Logger, Verbosity}; use crate::util::logger::{Logger, Verbosity};
/// Returns a new [`RunnerOptions`] based on the `entry` optional Options section /// Returns a new [`RunnerOptions`] based on the `entry` optional Options section
@ -166,9 +166,14 @@ pub fn get_entry_options(
let value = eval_natural_option(value, variables)?; let value = eval_natural_option(value, variables)?;
runner_options.max_redirect = Some(value as usize) runner_options.max_redirect = Some(value as usize)
} }
OptionKind::Output(filename) => { OptionKind::Output(output) => {
let value = eval_template(filename, variables)?; let filename = eval_template(output, variables)?;
runner_options.output = Some(value) let output = if filename == "-" {
Output::StdOut
} else {
Output::File(filename)
};
runner_options.output = Some(output)
} }
OptionKind::PathAsIs(value) => { OptionKind::PathAsIs(value) => {
let value = eval_boolean_option(value, variables)?; let value = eval_boolean_option(value, variables)?;

View File

@ -16,20 +16,31 @@
* *
*/ */
use std::fs::File; use std::fs::File;
use std::io;
#[cfg(target_family = "windows")] #[cfg(target_family = "windows")]
use std::io::IsTerminal; use std::io::IsTerminal;
use std::io::Write; use std::io::Write;
use std::{fmt, io};
use crate::runner::{Error, RunnerError}; use crate::runner::{Error, RunnerError};
use hurl_core::ast::{Pos, SourceInfo}; use hurl_core::ast::{Pos, SourceInfo};
/// Represents the output of write operation: can be either a file or stdout. /// Represents the output of write operation: can be either a file or stdout.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Output { pub enum Output {
StdOut, StdOut,
File(String), File(String),
} }
impl fmt::Display for Output {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let output = match self {
Output::StdOut => "-".to_string(),
Output::File(file) => file.to_string(),
};
write!(f, "{output}")
}
}
impl Output { impl Output {
/// Writes these `bytes` to the output. /// Writes these `bytes` to the output.
pub fn write(&self, bytes: &[u8]) -> Result<(), Error> { pub fn write(&self, bytes: &[u8]) -> Result<(), Error> {

View File

@ -108,7 +108,7 @@ pub type PredicateResult = Result<(), Error>;
impl EntryResult { impl EntryResult {
/// Writes the last HTTP response of this entry result to the file `filename`. /// Writes the last HTTP response of this entry result to the file `filename`.
/// The HTTP response can be decompressed if the entry's `compressed` option has been set. /// The HTTP response can be decompressed if the entry's `compressed` option has been set.
pub fn write_response(&self, filename: String) -> Result<(), Error> { pub fn write_response(&self, output: &Output) -> Result<(), Error> {
match self.calls.last() { match self.calls.last() {
Some(call) => { Some(call) => {
let response = &call.response; let response = &call.response;
@ -124,9 +124,9 @@ impl EntryResult {
return Err(Error::new(source_info, e.into(), false)); return Err(Error::new(source_info, e.into(), false));
} }
}; };
Output::File(filename).write(&bytes) output.write(&bytes)
} else { } else {
Output::File(filename).write(&response.body) output.write(&response.body)
} }
} }
None => Ok(()), None => Ok(()),

View File

@ -20,6 +20,7 @@ use std::time::Duration;
use hurl_core::ast::{Entry, Retry}; use hurl_core::ast::{Entry, Retry};
use crate::http::{IpResolve, RequestedHttpVersion}; use crate::http::{IpResolve, RequestedHttpVersion};
use crate::runner::Output;
use crate::util::path::ContextDir; use crate::util::path::ContextDir;
pub struct RunnerOptionsBuilder { pub struct RunnerOptionsBuilder {
@ -42,7 +43,7 @@ pub struct RunnerOptionsBuilder {
ip_resolve: IpResolve, ip_resolve: IpResolve,
max_redirect: Option<usize>, max_redirect: Option<usize>,
no_proxy: Option<String>, no_proxy: Option<String>,
output: Option<String>, output: Option<Output>,
path_as_is: bool, path_as_is: bool,
post_entry: Option<fn() -> bool>, post_entry: Option<fn() -> bool>,
pre_entry: Option<fn(Entry) -> bool>, pre_entry: Option<fn(Entry) -> bool>,
@ -256,7 +257,7 @@ impl RunnerOptionsBuilder {
} }
/// Specifies the file to output the HTTP response instead of stdout. /// Specifies the file to output the HTTP response instead of stdout.
pub fn output(&mut self, output: Option<String>) -> &mut Self { pub fn output(&mut self, output: Option<Output>) -> &mut Self {
self.output = output; self.output = output;
self self
} }
@ -404,7 +405,7 @@ pub struct RunnerOptions {
pub(crate) insecure: bool, pub(crate) insecure: bool,
pub(crate) max_redirect: Option<usize>, pub(crate) max_redirect: Option<usize>,
pub(crate) no_proxy: Option<String>, pub(crate) no_proxy: Option<String>,
pub(crate) output: Option<String>, pub(crate) output: Option<Output>,
pub(crate) path_as_is: bool, pub(crate) path_as_is: bool,
pub(crate) post_entry: Option<fn() -> bool>, pub(crate) post_entry: Option<fn() -> bool>,
pub(crate) pre_entry: Option<fn(Entry) -> bool>, pub(crate) pre_entry: Option<fn(Entry) -> bool>,