Add support for --resolve option per request

This commit is contained in:
jcamiel 2023-07-06 16:44:42 +02:00
parent 4f6a00170a
commit 1463c02d05
No known key found for this signature in database
GPG Key ID: 07FF11CFD55356CC
15 changed files with 322 additions and 160 deletions

View File

@ -120,6 +120,7 @@ option:
| follow-redirect-option
| insecure-option
| max-redirs-option
| resolve-option
| retry-option
| retry-interval-option
| variable-option
@ -135,6 +136,8 @@ insecure-option: "insecure" ":" boolean lt
max-redirs-option: "max-redirs" ":" integer lt
resolve-option: "resolve" ":" quoted-string lt
retry-option: "retry" ":" boolean lt
retry-interval-option: "retry-interval" ":" integer lt

View File

@ -0,0 +1,3 @@
curl --resolve foo.com:8000:127.0.0.1 --resolve bar.com:8000:127.0.0.1 --resolve baz.com:8000:127.0.0.1 'http://foo.com:8000/resolve'
curl --resolve foo.com:8000:127.0.0.1 --resolve bar.com:8000:127.0.0.1 --resolve baz.com:8000:127.0.0.1 'http://bar.com:8000/resolve'
curl --resolve foo.com:8000:127.0.0.1 --resolve bar.com:8000:127.0.0.1 --resolve baz.com:8000:127.0.0.1 'http://baz.com:8000/resolve'

View File

@ -0,0 +1,20 @@
# --resolve option allow to us custom address for a specific host and port pair.
GET http://foo.com:8000/resolve
[Options]
resolve: foo.com:8000:127.0.0.1
HTTP 200
`Hello World!`
GET http://bar.com:8000/resolve
[Options]
resolve: bar.com:8000:127.0.0.1
HTTP 200
`Hello World!`
GET http://baz.com:8000/resolve
[Options]
resolve: baz.com:8000:127.0.0.1
HTTP 200
`Hello World!`

View File

@ -0,0 +1 @@
Hello World!

View File

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

View File

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

View File

@ -26,7 +26,6 @@ use crate::runner::core::{Error, RunnerError, *};
use crate::runner::request::{cookie_storage_clear, cookie_storage_set, eval_request};
use crate::runner::response::{eval_asserts, eval_captures, eval_version_status_asserts};
use crate::runner::runner_options::RunnerOptions;
use crate::runner::template::eval_template;
use crate::runner::value::Value;
use crate::util::logger::{Logger, Verbosity};
@ -285,144 +284,3 @@ fn log_request_spec(request: &http::RequestSpec, logger: &Logger) {
}
logger.debug("");
}
/// Returns a new [`RunnerOptions`] based on the `entry` optional Options section
/// and a default `runner_options`.
/// The [`variables`] can also be updated if `variable` keys are present in the section.
pub fn get_entry_options(
entry: &Entry,
runner_options: &RunnerOptions,
variables: &mut HashMap<String, Value>,
logger: &Logger,
) -> Result<RunnerOptions, Error> {
let mut runner_options = runner_options.clone();
if !has_options(entry) {
return Ok(runner_options);
}
logger.debug("");
logger.debug_important("Entry options:");
for section in &entry.request.sections {
if let SectionValue::Options(options) = &section.value {
for option in options {
match option {
EntryOption::CaCertificate(option) => {
runner_options.cacert_file = Some(option.filename.value.clone());
logger.debug(format!("cacert: {}", option.filename.value).as_str());
}
EntryOption::ClientCert(option) => {
runner_options.client_cert_file = Some(option.filename.value.clone());
logger.debug(format!("cert: {}", option.filename.value).as_str());
}
EntryOption::ClientKey(option) => {
runner_options.client_key_file = Some(option.filename.value.clone());
logger.debug(format!("key: {}", option.filename.value).as_str());
}
EntryOption::Compressed(option) => {
runner_options.compressed = option.value;
logger.debug(format!("compressed: {}", option.value).as_str());
}
EntryOption::FollowLocation(option) => {
runner_options.follow_location = option.value;
logger.debug(format!("location: {}", option.value).as_str());
}
EntryOption::Insecure(option) => {
runner_options.insecure = option.value;
logger.debug(format!("insecure: {}", option.value).as_str());
}
EntryOption::MaxRedirect(option) => {
runner_options.max_redirect = Some(option.value);
logger.debug(format!("max-redirs: {}", option.value).as_str());
}
EntryOption::PathAsIs(option) => {
runner_options.path_as_is = option.value;
logger.debug(format!("path-as-is: {}", option.value).as_str());
}
EntryOption::Proxy(option) => {
runner_options.proxy = Some(option.value.clone());
logger.debug(format!("proxy: {}", option.value).as_str());
}
EntryOption::Retry(option) => {
runner_options.retry = option.value;
logger.debug(format!("retry: {}", option.value).as_str());
}
EntryOption::RetryInterval(option) => {
runner_options.retry_interval = Duration::from_millis(option.value);
logger.debug(format!("retry-interval: {}", option.value).as_str());
}
EntryOption::Variable(VariableOption {
value: VariableDefinition { name, value, .. },
..
}) => {
let value = eval_variable_value(value, variables)?;
variables.insert(name.clone(), value);
}
EntryOption::Verbose(option) => {
logger.debug(format!("verbose: {}", option.value).as_str());
}
EntryOption::VeryVerbose(option) => {
logger.debug(format!("very-verbose: {}", option.value).as_str());
}
}
}
}
}
Ok(runner_options)
}
fn eval_variable_value(
variable_value: &VariableValue,
variables: &mut HashMap<String, Value>,
) -> Result<Value, Error> {
match variable_value {
VariableValue::Null {} => Ok(Value::Null),
VariableValue::Bool(v) => Ok(Value::Bool(*v)),
VariableValue::Integer(v) => Ok(Value::Integer(*v)),
VariableValue::Float(Float { value, .. }) => Ok(Value::Float(*value)),
VariableValue::String(template) => {
let s = eval_template(template, variables)?;
Ok(Value::String(s))
}
}
}
/// Returns [`true`] if this `entry` has an Option section, [`false`] otherwise.
fn has_options(entry: &Entry) -> bool {
entry
.request
.sections
.iter()
.any(|s| matches!(s.value, SectionValue::Options(_)))
}
/// Returns the overridden `entry` verbosity, or the default `verbosity` file.
pub fn get_entry_verbosity(entry: &Entry, verbosity: &Option<Verbosity>) -> Option<Verbosity> {
let mut verbosity = *verbosity;
for section in &entry.request.sections {
if let SectionValue::Options(options) = &section.value {
for option in options {
match option {
EntryOption::Verbose(option) => {
verbosity = if option.value {
Some(Verbosity::Verbose)
} else {
None
}
}
EntryOption::VeryVerbose(option) => {
verbosity = if option.value {
Some(Verbosity::VeryVerbose)
} else {
None
}
}
_ => {}
}
}
}
}
verbosity
}

View File

@ -28,7 +28,7 @@ use crate::http;
use crate::http::Call;
use crate::runner::core::*;
use crate::runner::runner_options::RunnerOptions;
use crate::runner::{entry, Value};
use crate::runner::{entry, options, Value};
use crate::util::logger::{ErrorFormat, Logger, LoggerOptions, LoggerOptionsBuilder};
/// Runs a Hurl `content` and returns a [`HurlResult`] upon completion.
@ -130,7 +130,7 @@ pub fn run(
logger.test_progress(entry_index, n);
// The real execution of the entry happens here, with the overridden entry options.
let options = entry::get_entry_options(entry, runner_options, &mut variables, &logger);
let options = options::get_entry_options(entry, runner_options, &mut variables, &logger);
let entry_result = match &options {
Ok(options) => entry::run(
entry,
@ -366,7 +366,7 @@ fn log_errors(entry_result: &EntryResult, content: &str, retry: bool, logger: &L
/// Verbosity can be overridden at entry level with an Options section so each
/// entry has its own logger.
fn get_entry_logger(entry: &Entry, logger_options: &LoggerOptions) -> Logger {
let entry_verbosity = entry::get_entry_verbosity(entry, &logger_options.verbosity);
let entry_verbosity = options::get_entry_verbosity(entry, &logger_options.verbosity);
let entry_logger_options = LoggerOptionsBuilder::new()
.color(logger_options.color)
.filename(&logger_options.filename)

View File

@ -40,6 +40,7 @@ mod hurl_file;
mod json;
mod multiline;
mod multipart;
mod options;
mod predicate;
mod predicate_value;
mod query;

View File

@ -0,0 +1,174 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2023 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use crate::runner::template;
use crate::runner::{Error, RunnerOptions, Value};
use crate::util::logger::{Logger, Verbosity};
use hurl_core::ast::{
Entry, EntryOption, Float, SectionValue, VariableDefinition, VariableOption, VariableValue,
};
use std::collections::HashMap;
use std::time::Duration;
/// Returns a new [`RunnerOptions`] based on the `entry` optional Options section
/// and a default `runner_options`.
/// The [`variables`] can also be updated if `variable` keys are present in the section.
pub fn get_entry_options(
entry: &Entry,
runner_options: &RunnerOptions,
variables: &mut HashMap<String, Value>,
logger: &Logger,
) -> Result<RunnerOptions, Error> {
let mut runner_options = runner_options.clone();
if !has_options(entry) {
return Ok(runner_options);
}
logger.debug("");
logger.debug_important("Entry options:");
for section in &entry.request.sections {
if let SectionValue::Options(options) = &section.value {
for option in options {
match option {
EntryOption::CaCertificate(option) => {
runner_options.cacert_file = Some(option.filename.value.clone());
logger.debug(format!("cacert: {}", option.filename.value).as_str());
}
EntryOption::ClientCert(option) => {
runner_options.client_cert_file = Some(option.filename.value.clone());
logger.debug(format!("cert: {}", option.filename.value).as_str());
}
EntryOption::ClientKey(option) => {
runner_options.client_key_file = Some(option.filename.value.clone());
logger.debug(format!("key: {}", option.filename.value).as_str());
}
EntryOption::Compressed(option) => {
runner_options.compressed = option.value;
logger.debug(format!("compressed: {}", option.value).as_str());
}
EntryOption::FollowLocation(option) => {
runner_options.follow_location = option.value;
logger.debug(format!("location: {}", option.value).as_str());
}
EntryOption::Insecure(option) => {
runner_options.insecure = option.value;
logger.debug(format!("insecure: {}", option.value).as_str());
}
EntryOption::MaxRedirect(option) => {
runner_options.max_redirect = Some(option.value);
logger.debug(format!("max-redirs: {}", option.value).as_str());
}
EntryOption::PathAsIs(option) => {
runner_options.path_as_is = option.value;
logger.debug(format!("path-as-is: {}", option.value).as_str());
}
EntryOption::Proxy(option) => {
runner_options.proxy = Some(option.value.clone());
logger.debug(format!("proxy: {}", option.value).as_str());
}
EntryOption::Resolve(option) => {
let mut resolves = runner_options.resolves;
resolves.push(option.value.clone());
runner_options.resolves = resolves;
logger.debug(format!("resolve: {}", option.value).as_str());
}
EntryOption::Retry(option) => {
runner_options.retry = option.value;
logger.debug(format!("retry: {}", option.value).as_str());
}
EntryOption::RetryInterval(option) => {
runner_options.retry_interval = Duration::from_millis(option.value);
logger.debug(format!("retry-interval: {}", option.value).as_str());
}
EntryOption::Variable(VariableOption {
value: VariableDefinition { name, value, .. },
..
}) => {
let value = eval_variable_value(value, variables)?;
logger.debug(format!("variable: {}={}", name, value).as_str());
variables.insert(name.clone(), value);
}
EntryOption::Verbose(option) => {
logger.debug(format!("verbose: {}", option.value).as_str());
}
EntryOption::VeryVerbose(option) => {
logger.debug(format!("very-verbose: {}", option.value).as_str());
}
}
}
}
}
Ok(runner_options)
}
/// Returns [`true`] if this `entry` has an Option section, [`false`] otherwise.
fn has_options(entry: &Entry) -> bool {
entry
.request
.sections
.iter()
.any(|s| matches!(s.value, SectionValue::Options(_)))
}
/// Returns the overridden `entry` verbosity, or the default `verbosity` file.
pub fn get_entry_verbosity(entry: &Entry, verbosity: &Option<Verbosity>) -> Option<Verbosity> {
let mut verbosity = *verbosity;
for section in &entry.request.sections {
if let SectionValue::Options(options) = &section.value {
for option in options {
match option {
EntryOption::Verbose(option) => {
verbosity = if option.value {
Some(Verbosity::Verbose)
} else {
None
}
}
EntryOption::VeryVerbose(option) => {
verbosity = if option.value {
Some(Verbosity::VeryVerbose)
} else {
None
}
}
_ => {}
}
}
}
}
verbosity
}
fn eval_variable_value(
variable_value: &VariableValue,
variables: &mut HashMap<String, Value>,
) -> Result<Value, Error> {
match variable_value {
VariableValue::Null {} => Ok(Value::Null),
VariableValue::Bool(v) => Ok(Value::Bool(*v)),
VariableValue::Integer(v) => Ok(Value::Integer(*v)),
VariableValue::Float(Float { value, .. }) => Ok(Value::Float(*value)),
VariableValue::String(template) => {
let s = template::eval_template(template, variables)?;
Ok(Value::String(s))
}
}
}

View File

@ -717,6 +717,7 @@ pub enum EntryOption {
MaxRedirect(MaxRedirectOption),
PathAsIs(PathAsIsOption),
Proxy(ProxyOption),
Resolve(ResolveOption),
Retry(RetryOption),
RetryInterval(RetryIntervalOption),
Variable(VariableOption),
@ -804,6 +805,16 @@ pub struct RetryIntervalOption {
pub line_terminator0: LineTerminator,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolveOption {
pub line_terminators: Vec<LineTerminator>,
pub space0: Whitespace,
pub space1: Whitespace,
pub space2: Whitespace,
pub value: String,
pub line_terminator0: LineTerminator,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RetryOption {
pub line_terminators: Vec<LineTerminator>,

View File

@ -211,6 +211,7 @@ impl HtmlFormatter {
EntryOption::MaxRedirect(option) => self.fmt_max_redirect_option(option),
EntryOption::PathAsIs(option) => self.fmt_path_as_is_option(option),
EntryOption::Proxy(option) => self.fmt_proxy_option(option),
EntryOption::Resolve(option) => self.fmt_resolve_option(option),
EntryOption::Retry(option) => self.fmt_retry_option(option),
EntryOption::RetryInterval(option) => self.fmt_retry_interval_option(option),
EntryOption::Variable(option) => self.fmt_variable_option(option),
@ -336,6 +337,19 @@ impl HtmlFormatter {
self.fmt_lt(&option.line_terminator0);
}
fn fmt_resolve_option(&mut self, option: &ResolveOption) {
self.fmt_lts(&option.line_terminators);
self.fmt_span_open("line");
self.fmt_space(&option.space0);
self.fmt_string("resolve");
self.fmt_space(&option.space1);
self.buffer.push(':');
self.fmt_space(&option.space2);
self.fmt_string(&option.value);
self.fmt_span_close();
self.fmt_lt(&option.line_terminator0);
}
fn fmt_retry_option(&mut self, option: &RetryOption) {
self.fmt_lts(&option.line_terminators);
self.fmt_span_open("line");

View File

@ -366,6 +366,7 @@ fn option(reader: &mut Reader) -> ParseResult<'static, EntryOption> {
option_max_redirect,
option_path_as_is,
option_proxy,
option_resolve,
option_retry,
option_retry_interval,
option_variable,
@ -589,6 +590,50 @@ fn proxy(reader: &mut Reader) -> ParseResult<'static, String> {
Ok(name)
}
fn option_resolve(reader: &mut Reader) -> ParseResult<'static, EntryOption> {
let line_terminators = optional_line_terminators(reader)?;
let space0 = zero_or_more_spaces(reader)?;
try_literal("resolve", reader)?;
let space1 = zero_or_more_spaces(reader)?;
try_literal(":", reader)?;
let space2 = zero_or_more_spaces(reader)?;
let value = resolve(reader)?;
let line_terminator0 = line_terminator(reader)?;
let option = ResolveOption {
line_terminators,
space0,
space1,
space2,
value,
line_terminator0,
};
Ok(EntryOption::Resolve(option))
}
fn resolve(reader: &mut Reader) -> ParseResult<'static, String> {
let start = reader.state.clone();
let name = reader.read_while(|c| c.is_alphanumeric() || *c == ':' || *c == '.');
if name.is_empty() {
return Err(Error {
pos: start.pos,
recoverable: false,
inner: ParseError::Expecting {
value: "resolve".to_string(),
},
});
}
if !name.contains(':') {
return Err(Error {
pos: start.pos,
recoverable: false,
inner: ParseError::Expecting {
value: "HOST:PORT:ADDR".to_string(),
},
});
}
Ok(name)
}
fn option_retry(reader: &mut Reader) -> ParseResult<'static, EntryOption> {
let line_terminators = optional_line_terminators(reader)?;
let space0 = zero_or_more_spaces(reader)?;

View File

@ -272,74 +272,78 @@ impl ToJson for Cookie {
impl ToJson for EntryOption {
fn to_json(&self) -> JValue {
let attributes = match self {
EntryOption::CaCertificate(value) => vec![
EntryOption::CaCertificate(value) => [
("name".to_string(), JValue::String("cacert".to_string())),
(
"value".to_string(),
JValue::String(value.filename.value.clone()),
),
],
EntryOption::ClientCert(value) => vec![
EntryOption::ClientCert(value) => [
("name".to_string(), JValue::String("cert".to_string())),
(
"value".to_string(),
JValue::String(value.filename.value.clone()),
),
],
EntryOption::ClientKey(value) => vec![
EntryOption::ClientKey(value) => [
("name".to_string(), JValue::String("key".to_string())),
(
"value".to_string(),
JValue::String(value.filename.value.clone()),
),
],
EntryOption::Compressed(value) => vec![
EntryOption::Compressed(value) => [
("name".to_string(), JValue::String("compressed".to_string())),
("value".to_string(), JValue::Boolean(value.value)),
],
EntryOption::Insecure(value) => vec![
EntryOption::Insecure(value) => [
("name".to_string(), JValue::String("insecure".to_string())),
("value".to_string(), JValue::Boolean(value.value)),
],
EntryOption::FollowLocation(value) => vec![
EntryOption::FollowLocation(value) => [
("name".to_string(), JValue::String("location".to_string())),
("value".to_string(), JValue::Boolean(value.value)),
],
EntryOption::MaxRedirect(value) => vec![
EntryOption::MaxRedirect(value) => [
("name".to_string(), JValue::String("max-redirs".to_string())),
("value".to_string(), JValue::Number(value.value.to_string())),
],
EntryOption::PathAsIs(value) => vec![
EntryOption::PathAsIs(value) => [
("name".to_string(), JValue::String("path-as-is".to_string())),
("value".to_string(), JValue::Boolean(value.value)),
],
EntryOption::Proxy(value) => vec![
EntryOption::Proxy(value) => [
("name".to_string(), JValue::String("proxy".to_string())),
("value".to_string(), JValue::String(value.value.clone())),
],
EntryOption::Retry(value) => vec![
EntryOption::Resolve(value) => [
("name".to_string(), JValue::String("resolve".to_string())),
("value".to_string(), JValue::Number(value.value.to_string())),
],
EntryOption::Retry(value) => [
("name".to_string(), JValue::String("retry".to_string())),
("value".to_string(), JValue::Number(value.value.to_string())),
],
EntryOption::RetryInterval(value) => vec![
EntryOption::RetryInterval(value) => [
(
"name".to_string(),
JValue::String("retry-interval".to_string()),
),
("value".to_string(), JValue::Number(value.value.to_string())),
],
EntryOption::Variable(value) => vec![
EntryOption::Variable(value) => [
("name".to_string(), JValue::String("variable".to_string())),
(
"value".to_string(),
JValue::String(format!("{}={}", value.value.name, value.value.value)),
),
],
EntryOption::Verbose(value) => vec![
EntryOption::Verbose(value) => [
("name".to_string(), JValue::String("verbose".to_string())),
("value".to_string(), JValue::Boolean(value.value)),
],
EntryOption::VeryVerbose(value) => vec![
EntryOption::VeryVerbose(value) => [
(
"name".to_string(),
JValue::String("very-verbose".to_string()),
@ -347,7 +351,7 @@ impl ToJson for EntryOption {
("value".to_string(), JValue::Boolean(value.value)),
],
};
JValue::Object(attributes)
JValue::Object(attributes.to_vec())
}
}

View File

@ -865,6 +865,7 @@ impl Tokenizable for EntryOption {
EntryOption::MaxRedirect(option) => option.tokenize(),
EntryOption::PathAsIs(option) => option.tokenize(),
EntryOption::Proxy(option) => option.tokenize(),
EntryOption::Resolve(option) => option.tokenize(),
EntryOption::Retry(option) => option.tokenize(),
EntryOption::RetryInterval(option) => option.tokenize(),
EntryOption::Variable(option) => option.tokenize(),
@ -1063,6 +1064,27 @@ impl Tokenizable for ProxyOption {
}
}
impl Tokenizable for ResolveOption {
fn tokenize(&self) -> Vec<Token> {
let mut tokens: Vec<Token> = vec![];
tokens.append(
&mut self
.line_terminators
.iter()
.flat_map(|e| e.tokenize())
.collect(),
);
tokens.append(&mut self.space0.tokenize());
tokens.push(Token::String("resolve".to_string()));
tokens.append(&mut self.space1.tokenize());
tokens.push(Token::Colon(String::from(":")));
tokens.append(&mut self.space2.tokenize());
tokens.push(Token::String(self.value.clone()));
tokens.append(&mut self.line_terminator0.tokenize());
tokens
}
}
impl Tokenizable for RetryOption {
fn tokenize(&self) -> Vec<Token> {
let mut tokens: Vec<Token> = vec![];