mirror of
https://github.com/Orange-OpenSource/hurl.git
synced 2024-11-11 02:40:26 +03:00
Using explicit stdout output
This commit is contained in:
parent
3927dcc622
commit
65e00babca
30
integration/hurl/tests_ok/stdout.err.pattern
Normal file
30
integration/hurl/tests_ok/stdout.err.pattern
Normal 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
|
||||||
|
<
|
||||||
|
*
|
8
integration/hurl/tests_ok/stdout.hurl
Executable file
8
integration/hurl/tests_ok/stdout.hurl
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
GET http://localhost:8000/stdout/text
|
||||||
|
[Options]
|
||||||
|
output: -
|
||||||
|
HTTP 200
|
||||||
|
`Hello`
|
||||||
|
|
||||||
|
|
||||||
|
|
1
integration/hurl/tests_ok/stdout.out
Normal file
1
integration/hurl/tests_ok/stdout.out
Normal file
@ -0,0 +1 @@
|
|||||||
|
HelloHello
|
5
integration/hurl/tests_ok/stdout.ps1
Executable file
5
integration/hurl/tests_ok/stdout.ps1
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
Set-StrictMode -Version latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
hurl --verbose --output - tests_ok/stdout.hurl
|
||||||
|
|
||||||
|
|
7
integration/hurl/tests_ok/stdout.py
Normal file
7
integration/hurl/tests_ok/stdout.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
from app import app
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/stdout/text")
|
||||||
|
def stdout_text():
|
||||||
|
return "Hello"
|
5
integration/hurl/tests_ok/stdout.sh
Executable file
5
integration/hurl/tests_ok/stdout.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
hurl --verbose tests_ok/stdout.hurl
|
||||||
|
|
||||||
|
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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()),
|
||||||
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
@ -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("");
|
||||||
|
|
||||||
|
@ -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");
|
||||||
|
@ -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)?;
|
||||||
|
@ -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> {
|
||||||
|
@ -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(()),
|
||||||
|
@ -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>,
|
||||||
|
Loading…
Reference in New Issue
Block a user