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::OptionsError;
use crate::cli::options::{ErrorFormat, HttpVersion, IpResolve};
use crate::cli::options::{ErrorFormat, HttpVersion, IpResolve, Output};
use crate::cli::OutputType;
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")
}
pub fn output(arg_matches: &ArgMatches) -> Option<String> {
get::<String>(arg_matches, "output")
pub fn output(arg_matches: &ArgMatches) -> Option<Output> {
get::<String>(arg_matches, "output").map(|filename| {
if filename == "-" {
Output::StdOut
} else {
Output::File(filename)
}
})
}
pub fn output_type(arg_matches: &ArgMatches) -> OutputType {

View File

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

View File

@ -35,6 +35,7 @@ use crate::http::request_spec::*;
use crate::http::response::*;
use crate::http::timings::Timings;
use crate::http::{easy_ext, Call, Header, HttpError, Verbosity};
use crate::runner::Output;
use crate::util::logger::Logger;
use crate::util::path::ContextDir;
@ -737,7 +738,7 @@ impl Client {
&mut self,
request_spec: &RequestSpec,
context_dir: &ContextDir,
output: Option<&str>,
output: Option<&Output>,
options: &ClientOptions,
) -> 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 lines = split_lines(data);
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(2).unwrap().as_str(), "");
}
@ -1118,7 +1119,8 @@ mod tests {
..Default::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 {
aws_sigv4: Some("aws:amz:sts".to_string()),
cacert_file: Some("/etc/cert.pem".to_string()),

View File

@ -27,15 +27,15 @@ pub fn write_json(
hurl_result: &HurlResult,
content: &str,
filename_in: &str,
filename_out: &Option<String>,
filename_out: &Option<Output>,
) -> 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();
match filename_out {
Some(file) => Output::File(file.to_string()).write(&bytes)?,
None => Output::StdOut.write(&bytes)?,
Some(Output::File(file)) => Output::File(file.to_string()).write(&bytes)?,
_ => Output::StdOut.write(&bytes)?,
}
Ok(())
}

View File

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

View File

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

View File

@ -28,7 +28,7 @@ use hurl_core::parser;
use crate::http::Call;
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::{http, runner};
@ -220,17 +220,28 @@ pub fn run(
// 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
// specified path.
if !runner_options.context_dir.is_access_allowed(&output) {
let authorized = if let Output::File(filename) = &output {
if !runner_options.context_dir.is_access_allowed(filename) {
let inner = RunnerError::UnauthorizedFileAccess {
path: PathBuf::from(output.clone()),
path: PathBuf::from(filename.clone()),
};
let error = runner::Error::new(entry.request.source_info, inner, false);
logger.warning(&error.fixme());
} else if let Err(error) = entry_result.write_response(output) {
false
} else {
true
}
} else {
true
};
if authorized {
if let Err(error) = entry_result.write_response(&output) {
logger.warning(&error.fixme());
}
}
}
}
entries.push(entry_result);
if retry {
@ -508,7 +519,7 @@ mod test {
let non_default_options = get_non_default_options(&options);
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.1, "500ms");

View File

@ -25,7 +25,7 @@ use hurl_core::ast::{
use crate::http::{IpResolve, RequestedHttpVersion};
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};
/// 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)?;
runner_options.max_redirect = Some(value as usize)
}
OptionKind::Output(filename) => {
let value = eval_template(filename, variables)?;
runner_options.output = Some(value)
OptionKind::Output(output) => {
let filename = eval_template(output, variables)?;
let output = if filename == "-" {
Output::StdOut
} else {
Output::File(filename)
};
runner_options.output = Some(output)
}
OptionKind::PathAsIs(value) => {
let value = eval_boolean_option(value, variables)?;

View File

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

View File

@ -108,7 +108,7 @@ pub type PredicateResult = Result<(), Error>;
impl EntryResult {
/// 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.
pub fn write_response(&self, filename: String) -> Result<(), Error> {
pub fn write_response(&self, output: &Output) -> Result<(), Error> {
match self.calls.last() {
Some(call) => {
let response = &call.response;
@ -124,9 +124,9 @@ impl EntryResult {
return Err(Error::new(source_info, e.into(), false));
}
};
Output::File(filename).write(&bytes)
output.write(&bytes)
} else {
Output::File(filename).write(&response.body)
output.write(&response.body)
}
}
None => Ok(()),

View File

@ -20,6 +20,7 @@ use std::time::Duration;
use hurl_core::ast::{Entry, Retry};
use crate::http::{IpResolve, RequestedHttpVersion};
use crate::runner::Output;
use crate::util::path::ContextDir;
pub struct RunnerOptionsBuilder {
@ -42,7 +43,7 @@ pub struct RunnerOptionsBuilder {
ip_resolve: IpResolve,
max_redirect: Option<usize>,
no_proxy: Option<String>,
output: Option<String>,
output: Option<Output>,
path_as_is: bool,
post_entry: Option<fn() -> bool>,
pre_entry: Option<fn(Entry) -> bool>,
@ -256,7 +257,7 @@ impl RunnerOptionsBuilder {
}
/// 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
}
@ -404,7 +405,7 @@ pub struct RunnerOptions {
pub(crate) insecure: bool,
pub(crate) max_redirect: Option<usize>,
pub(crate) no_proxy: Option<String>,
pub(crate) output: Option<String>,
pub(crate) output: Option<Output>,
pub(crate) path_as_is: bool,
pub(crate) post_entry: Option<fn() -> bool>,
pub(crate) pre_entry: Option<fn(Entry) -> bool>,