Update output

Can now be on the following:
- Binary response body
- JSON
- Textual Summary
This commit is contained in:
Fabrice Reix 2021-11-08 20:54:19 +01:00 committed by Fabrice Reix
parent 359f153e63
commit dd98920f80
19 changed files with 325 additions and 413 deletions

View File

@ -2,13 +2,10 @@
import json
import sys
def check_file(expected_file, actual_file):
expected_tests = json.loads(open(expected_file).read())
actual_tests = json.loads(open(actual_file).read())
assert len(expected_tests) == 1
assert len(actual_tests) == 1
check_output(expected_tests[0], actual_tests[0])
def check(expected, actual):
expected_test = json.loads(expected)
actual_test = json.loads(actual)
check_output(expected_test, actual_test)
def check_output(expected_test, actual_test):
@ -77,7 +74,9 @@ def main():
sys.exit(1)
expected_file = sys.argv[1]
actual_file = sys.argv[2]
check_file(expected_file, actual_file)
expected = open(expected_file).read()
actual = open(actual_file).read()
check_file(expected, actual)
if __name__ == '__main__':

View File

@ -43,12 +43,9 @@ def test(hurl_file):
options = open(options_file).read().strip().split(' ')
if os.path.exists(curl_file):
options.append('--verbose')
if os.path.exists(json_output_file):
json_output_actual_file = os.path.join(tempfile.mkdtemp(), 'output.json')
options.append('--json')
options.append(json_output_actual_file)
if os.path.exists(json_output_file):
options.append('--json')
cmd = ['hurl', hurl_file] + options
print(' '.join(cmd))
@ -75,6 +72,12 @@ def test(hurl_file):
print(f'actual: <{actual}>\nexpected: <{expected}>')
sys.exit(1)
# stdout (json)
if os.path.exists(json_output_file):
expected = open(json_output_file).read()
actual = result.stdout
check_json_output.check(expected, actual)
# stderr
f = hurl_file.replace('.hurl', '.' + get_os() + '.err')
if os.path.exists(f):
@ -119,9 +122,6 @@ def test(hurl_file):
print('actual: %s' % actual_commands[i])
sys.exit(1)
if os.path.exists(json_output_file):
check_json_output.check_file(json_output_file, json_output_actual_file)
def main():
for hurl_file in sys.argv[1:]:

View File

@ -1,71 +1,69 @@
[
{
"filename": "tests/assert_header.hurl",
"entries": [
{
"request": {
"method": "GET",
"url": "http://localhost:8000/assert-header"
{
"filename": "tests/assert_header.hurl",
"entries": [
{
"request": {
"method": "GET",
"url": "http://localhost:8000/assert-header"
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
},
"asserts": [
{
"line": 2,
"success": true
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
{
"line": 2,
"success": true
},
"asserts": [
{
"line": 2,
"success": true
},
{
"line": 2,
"success": true
},
{
"line": 3,
"success": true
},
{
"line": 4,
"success": true
},
{
"line": 5,
"success": true
},
{
"line": 7,
"success": true
},
{
"line": 8,
"success": true
},
{
"line": 9,
"success": true
},
{
"line": 10,
"success": true
},
{
"line": 11,
"success": true
},
{
"line": 12,
"success": true
},
{
"line": 13,
"success": true
},
{
"line": 14,
"success": true
}
]
}
]
}
]
{
"line": 3,
"success": true
},
{
"line": 4,
"success": true
},
{
"line": 5,
"success": true
},
{
"line": 7,
"success": true
},
{
"line": 8,
"success": true
},
{
"line": 9,
"success": true
},
{
"line": 10,
"success": true
},
{
"line": 11,
"success": true
},
{
"line": 12,
"success": true
},
{
"line": 13,
"success": true
},
{
"line": 14,
"success": true
}
]
}
]
}

View File

@ -1,32 +1,31 @@
[
{
"filename": "tests/error_assert_match_utf8.hurl",
"entries": [
{
"request": {
"method": "GET",
"url": "http://localhost:8000/error-assert/match-utf8"
{
"filename": "tests/error_assert_match_utf8.hurl",
"entries": [
{
"request": {
"method": "GET",
"url": "http://localhost:8000/error-assert/match-utf8"
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
},
"asserts": [
{
"success": true,
"line": 2
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
{
"success": true,
"line": 2
},
"asserts": [
{
"success": true,
"line": 2
},
{
"success": true,
"line": 2
},
{
"success": false,
"message": "Invalid Decoding\n --> tests/error_assert_match_utf8.hurl:4:1\n |\n 4 | body matches \".*\"\n | ^^^^ The body can not be decoded with charset 'utf-8'\n |",
"line": 4
}
]
}
]
}
]
{
"success": false,
"message": "Invalid Decoding\n --> tests/error_assert_match_utf8.hurl:4:1\n |\n 4 | body matches \".*\"\n | ^^^^ The body can not be decoded with charset 'utf-8'\n |",
"line": 4
}
]
}
]
}

View File

@ -1,28 +1,26 @@
[
{
"filename": "tests/error_assert_status.hurl",
"entries": [
{
"request": {
"method": "GET",
"url": "http://localhost:8000/not_found"
{
"filename": "tests/error_assert_status.hurl",
"entries": [
{
"request": {
"method": "GET",
"url": "http://localhost:8000/not_found"
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 404
},
"asserts": [
{
"success": true,
"line": 2
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 404
},
"asserts": [
{
"success": true,
"line": 2
},
{
"success": false,
"message": "Assert Status\n --> tests/error_assert_status.hurl:2:10\n |\n 2 | HTTP/1.0 200\n | ^^^ actual value is <404>\n |",
"line": 2
}
]
}
]
}
]
{
"success": false,
"message": "Assert Status\n --> tests/error_assert_status.hurl:2:10\n |\n 2 | HTTP/1.0 200\n | ^^^ actual value is <404>\n |",
"line": 2
}
]
}
]
}

View File

@ -0,0 +1 @@
{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/error-assert-template-variable-not-found"},"response":{"version":"HTTP/1.0","status":200,"asserts":[{"query":{"type":"header","name":"content-type"},"predicate":{"type":"equal","value":"{{content_type}}"}}]}}]}

View File

@ -1,32 +1,30 @@
[
{
"filename": "tests/error_assert_template_variable_not_found.hurl",
"entries": [
{
"request": {
"method": "GET",
"url": "http://localhost:8000/error-assert-template-variable-not-found"
{
"filename": "tests/error_assert_template_variable_not_found.hurl",
"entries": [
{
"request": {
"method": "GET",
"url": "http://localhost:8000/error-assert-template-variable-not-found"
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
},
"asserts": [
{
"success": true,
"line": 2
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
{
"success": true,
"line": 2
},
"asserts": [
{
"success": true,
"line": 2
},
{
"success": true,
"line": 2
},
{
"success": false,
"message": "Undefined Variable\n --> tests/error_assert_template_variable_not_found.hurl:4:33\n |\n 4 | header \"content-type\" equals \"{{content_type}}\"\n | ^^^^^^^^^^^^ You must set the variable content_type\n |",
"line": 4
}
]
}
]
}
]
{
"success": false,
"message": "Undefined Variable\n --> tests/error_assert_template_variable_not_found.hurl:4:33\n |\n 4 | header \"content-type\" equals \"{{content_type}}\"\n | ^^^^^^^^^^^^ You must set the variable content_type\n |",
"line": 4
}
]
}
]
}

View File

@ -1,67 +1,65 @@
[
{
"filename": "tests/error_assert_value_error.hurl",
"entries": [
{
"request": {
"method": "GET",
"url": "http://localhost:8000/error-assert-value"
{
"filename": "tests/error_assert_value_error.hurl",
"entries": [
{
"request": {
"method": "GET",
"url": "http://localhost:8000/error-assert-value"
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
},
"asserts": [
{
"success": true,
"line": 2
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
{
"success": true,
"line": 2
},
"asserts": [
{
"success": true,
"line": 2
},
{
"success": true,
"line": 2
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:4:0\n |\n 4 | header \"content-type\" equals \"XXX\"\n | actual: string <text/html; charset=utf-8>\n | expected: string <XXX>\n |",
"line": 4
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:5:0\n |\n 5 | header \"content-type\" notEquals \"text/html; charset=utf-8\"\n | actual: string <text/html; charset=utf-8>\n | expected: string <text/html; charset=utf-8>\n |",
"line": 5
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:6:0\n |\n 6 | jsonpath \"$.id\" equals \"000001\"\n | actual: none\n | expected: string <000001>\n |",
"line": 6
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:7:0\n |\n 7 | jsonpath \"$.values\" includes 100\n | actual: [int <1>, int <2>, int <3>]\n | expected: includes int <100>\n |",
"line": 7
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:8:0\n |\n 8 | jsonpath \"$.values\" not contains \"Hello\"\n | actual: [int <1>, int <2>, int <3>]\n | expected: not contains string <Hello>\n | >>> types between actual and expected are not consistent\n |",
"line": 8
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:9:0\n |\n 9 | jsonpath \"$.count\" greaterThan 5\n | actual: int <2>\n | expected: greater than int <5>\n |",
"line": 9
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:10:0\n |\n10 | jsonpath \"$.count\" isFloat\n | actual: int <2>\n | expected: float\n |",
"line": 10
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:11:0\n |\n11 | bytes contains hex,00;\n | actual: byte array <7b202276616c756573223a205b312c322c335d2c2022636f756e74223a20327d>\n | expected: contains byte array <00>\n |",
"line": 11
}
]
}
]
}
]
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:4:0\n |\n 4 | header \"content-type\" equals \"XXX\"\n | actual: string <text/html; charset=utf-8>\n | expected: string <XXX>\n |",
"line": 4
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:5:0\n |\n 5 | header \"content-type\" notEquals \"text/html; charset=utf-8\"\n | actual: string <text/html; charset=utf-8>\n | expected: string <text/html; charset=utf-8>\n |",
"line": 5
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:6:0\n |\n 6 | jsonpath \"$.id\" equals \"000001\"\n | actual: none\n | expected: string <000001>\n |",
"line": 6
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:7:0\n |\n 7 | jsonpath \"$.values\" includes 100\n | actual: [int <1>, int <2>, int <3>]\n | expected: includes int <100>\n |",
"line": 7
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:8:0\n |\n 8 | jsonpath \"$.values\" not contains \"Hello\"\n | actual: [int <1>, int <2>, int <3>]\n | expected: not contains string <Hello>\n | >>> types between actual and expected are not consistent\n |",
"line": 8
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:9:0\n |\n 9 | jsonpath \"$.count\" greaterThan 5\n | actual: int <2>\n | expected: greater than int <5>\n |",
"line": 9
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:10:0\n |\n10 | jsonpath \"$.count\" isFloat\n | actual: int <2>\n | expected: float\n |",
"line": 10
},
{
"success": false,
"message": "Assert Failure\n --> tests/error_assert_value_error.hurl:11:0\n |\n11 | bytes contains hex,00;\n | actual: byte array <7b202276616c756573223a205b312c322c335d2c2022636f756e74223a20327d>\n | expected: contains byte array <00>\n |",
"line": 11
}
]
}
]
}

View File

@ -1,32 +1,30 @@
[
{
"filename": "tests/error_query_header_not_found.hurl",
"entries": [
{
"request": {
"method": "GET",
"url": "http://localhost:8000/error-query-header-not-found"
{
"filename": "tests/error_query_header_not_found.hurl",
"entries": [
{
"request": {
"method": "GET",
"url": "http://localhost:8000/error-query-header-not-found"
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
},
"asserts": [
{
"success": true,
"line": 2
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
{
"success": true,
"line": 2
},
"asserts": [
{
"success": true,
"line": 2
},
{
"success": true,
"line": 2
},
{
"success": false,
"message": "Header not Found\n --> tests/error_query_header_not_found.hurl:3:1\n |\n 3 | Custom: XXX\n | ^^^^^^ This header has not been found in the response\n |",
"line": 3
}
]
}
]
}
]
{
"success": false,
"message": "Header not Found\n --> tests/error_query_header_not_found.hurl:3:1\n |\n 3 | Custom: XXX\n | ^^^^^^ This header has not been found in the response\n |",
"line": 3
}
]
}
]
}

View File

@ -1,62 +0,0 @@
[
{
"filename": "tests/hello.hurl",
"entries": [
{
"request": {
"method": "GET",
"url": "http://localhost:8000/hello"
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
},
"asserts": [
{
"success": true,
"line": 2
},
{
"success": true,
"line": 2
},
{
"success": true,
"line": 3
}
]
},
{
"request": {
"method": "GET",
"url": "http://localhost:8000/hello"
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
}
},
{
"request": {
"method": "GET",
"url": "http://localhost:8000/hello"
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
}
},
{
"request": {
"method": "GET",
"url": "http://localhost:8000/hello"
},
"response": {
"httpVersion": "HTTP/1.0",
"status": 200
}
}
],
"success": true
}
]

View File

@ -1 +0,0 @@
0

View File

@ -1,5 +0,0 @@
GET http://localhost:8000/test-mode
HTTP/1.0 200
```Hello World!```

View File

@ -1 +0,0 @@
--test

View File

@ -1,10 +0,0 @@
from tests import app
from flask import request
@app.route("/test-mode")
def test_mode():
assert 'Content-Type' not in request.headers
assert 'Content-Length' not in request.headers
assert len(request.data) == 0
return 'Hello World!'

View File

@ -26,7 +26,7 @@ pub use self::logger::{
pub use self::options::app;
pub use self::options::output_color;
pub use self::options::parse_options;
pub use self::options::CliOptions;
pub use self::options::{CliOptions, OutputType};
pub use self::variables::parse as parse_variable;
mod fs;

View File

@ -28,16 +28,6 @@ use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg(target_family = "unix")]
pub fn dev_null() -> String {
"/dev/null".to_string()
}
#[cfg(target_family = "windows")]
pub fn dev_null() -> String {
"nul".to_string()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CliOptions {
pub cacert_file: Option<String>,
@ -54,14 +44,13 @@ pub struct CliOptions {
pub include: bool,
pub insecure: bool,
pub interactive: bool,
pub json_file: Option<PathBuf>,
pub junit_file: Option<PathBuf>,
pub max_redirect: Option<usize>,
pub no_proxy: Option<String>,
pub output: Option<String>,
pub output_type: OutputType,
pub progress: bool,
pub proxy: Option<String>,
pub summary: bool,
pub timeout: Duration,
pub to_entry: Option<usize>,
pub user: Option<String>,
@ -69,6 +58,12 @@ pub struct CliOptions {
pub verbose: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OutputType {
ResponseBody,
Summary,
Json,
}
pub fn app() -> App<'static, 'static> {
App::new("hurl")
.about("Run hurl FILE(s) or standard input")
@ -169,9 +164,8 @@ pub fn app() -> App<'static, 'static> {
.arg(
clap::Arg::with_name("json")
.long("json")
.value_name("FILE")
.help("Write full session(s) to json file")
.takes_value(true),
.conflicts_with("summary")
.help("Write full session(s) to json output"),
)
.arg(
clap::Arg::with_name("junit")
@ -237,12 +231,13 @@ pub fn app() -> App<'static, 'static> {
.arg(
clap::Arg::with_name("summary")
.long("summary")
.conflicts_with("json")
.help("Print test metrics at the end of the run"),
)
.arg(
clap::Arg::with_name("test")
.long("test")
.help("Activate test mode; equals --output /dev/null --progress --summary"),
.help("This option has been deprecated. It will be removed in the next release"),
)
.arg(
clap::Arg::with_name("to_entry")
@ -355,12 +350,6 @@ pub fn parse_options(matches: ArgMatches) -> Result<CliOptions, CliError> {
let include = matches.is_present("include");
let insecure = matches.is_present("insecure");
let interactive = matches.is_present("interactive");
let json_file = if let Some(filename) = matches.value_of("json") {
let path = Path::new(filename);
Some(path.to_path_buf())
} else {
None
};
let junit_file = if let Some(filename) = matches.value_of("junit") {
let path = Path::new(filename);
Some(path.to_path_buf())
@ -380,16 +369,24 @@ pub fn parse_options(matches: ArgMatches) -> Result<CliOptions, CliError> {
},
};
let no_proxy = matches.value_of("proxy").map(|x| x.to_string());
let output = if let Some(filename) = matches.value_of("output") {
Some(filename.to_string())
} else if matches.is_present("test") {
Some(dev_null())
let output = matches
.value_of("output")
.map(|filename| filename.to_string());
let output_type = if matches.is_present("summary") {
OutputType::Summary
} else if matches.is_present("json") {
OutputType::Json
} else {
None
OutputType::ResponseBody
};
let progress = matches.is_present("progress") || matches.is_present("test");
let proxy = matches.value_of("proxy").map(|x| x.to_string());
let summary = matches.is_present("summary") || matches.is_present("test");
if matches.is_present("test") {
eprintln!("The option --test is deprecated");
eprintln!("It will be removed in the next version");
}
let timeout = match matches.value_of("max_time") {
None => ClientOptions::default().timeout,
Some(s) => match s.parse::<u64>() {
@ -422,13 +419,12 @@ pub fn parse_options(matches: ArgMatches) -> Result<CliOptions, CliError> {
insecure,
interactive,
junit_file,
json_file,
max_redirect,
no_proxy,
output,
output_type,
progress,
proxy,
summary,
timeout,
to_entry,
user,

View File

@ -27,9 +27,8 @@ use colored::*;
use curl::Version;
use hurl::cli;
use hurl::cli::{CliError, CliOptions};
use hurl::cli::{CliError, CliOptions, OutputType};
use hurl::http;
use hurl::json;
use hurl::report;
use hurl::runner;
use hurl::runner::{HurlResult, RunnerOptions};
@ -292,11 +291,6 @@ fn main() {
};
let start = Instant::now();
let mut json_results = vec![];
if let Some(file_path) = cli_options.json_file.clone() {
json_results = unwrap_or_exit(&log_error_message, json::parse_json(file_path));
}
for (current, filename) in filenames.iter().enumerate() {
let contents = match cli::read_to_string(filename) {
@ -368,10 +362,13 @@ fn main() {
response.body
};
output.append(&mut body.clone());
unwrap_or_exit(
&log_error_message,
write_output(output, cli_options.output.clone()),
);
if matches!(cli_options.output_type, OutputType::ResponseBody) {
unwrap_or_exit(
&log_error_message,
write_output(output, cli_options.output.clone()),
);
}
} else {
cli::log_info("no response has been received");
}
@ -390,25 +387,20 @@ fn main() {
hurl_results.push(hurl_result.clone());
if cli_options.json_file.is_some() {
if matches!(cli_options.output_type, OutputType::Json) {
let lines: Vec<String> = regex::Regex::new(r"\n|\r\n")
.unwrap()
.split(&contents)
.map(|l| l.to_string())
.collect();
let json_result = hurl_result.to_json(&lines);
json_results.push(json_result);
let serialized = serde_json::to_string(&json_result).unwrap();
unwrap_or_exit(
&log_error_message,
write_output(serialized.into_bytes(), cli_options.output.clone()),
);
}
}
let duration = start.elapsed().as_millis();
if let Some(file_path) = cli_options.json_file.clone() {
log_verbose(format!("Writing json report to {}", file_path.display()).as_str());
unwrap_or_exit(
&log_error_message,
json::write_json_report(file_path, json_results),
);
}
if let Some(junit_path) = cli_options.junit_file {
log_verbose(format!("Writing Junit report to {}", junit_path.display()).as_str());
@ -438,8 +430,13 @@ fn main() {
);
}
if cli_options.summary {
print_summary(duration, hurl_results.clone())
if matches!(cli_options.output_type, OutputType::Summary) {
let duration = start.elapsed().as_millis();
let summary = get_summary(duration, hurl_results.clone());
unwrap_or_exit(
&log_error_message,
write_output(summary.into_bytes(), cli_options.output.clone()),
);
}
std::process::exit(exit_code(hurl_results));
@ -558,23 +555,32 @@ fn write_cookies_file(file_path: PathBuf, hurl_results: Vec<HurlResult>) -> Resu
Ok(())
}
fn print_summary(duration: u128, hurl_results: Vec<HurlResult>) {
fn get_summary(duration: u128, hurl_results: Vec<HurlResult>) -> String {
let total = hurl_results.len();
let success = hurl_results.iter().filter(|r| r.success).count();
let failed = total - success;
eprintln!("--------------------------------------------------------------------------------");
eprintln!("Executed: {}", total);
eprintln!(
"Succeeded: {} ({:.1}%)",
success,
100.0 * success as f32 / total as f32
let mut s =
"--------------------------------------------------------------------------------\n"
.to_string();
s.push_str(format!("Executed: {}\n", total).as_str());
s.push_str(
format!(
"Succeeded: {} ({:.1}%)\n",
success,
100.0 * success as f32 / total as f32
)
.as_str(),
);
eprintln!(
"Failed: {} ({:.1}%)",
failed,
100.0 * failed as f32 / total as f32
s.push_str(
format!(
"Failed: {} ({:.1}%)\n",
failed,
100.0 * failed as f32 / total as f32
)
.as_str(),
);
eprintln!("Duration: {}ms", duration);
s.push_str(format!("Duration: {}ms\n", duration).as_str());
s
}
fn get_version_info() -> String {