mirror of
https://github.com/Orange-OpenSource/hurl.git
synced 2024-12-23 11:02:43 +03:00
Improve error message with suggestions
This commit is contained in:
parent
c5a99a3c64
commit
e3952b8d84
@ -2,6 +2,6 @@ error: Parsing Method
|
||||
--> tests_error_parser/invalid_character_at_end.hurl:3:1
|
||||
|
|
||||
3 | XXX
|
||||
| ^ Available HTTP Method GET, POST, ...
|
||||
| ^ The HTTP method is not valid. Available HTTP Methods are GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE or PATCH
|
||||
|
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
error: Parsing section
|
||||
--> tests_error_parser/invalid_section.hurl:2:1
|
||||
error: Parsing request section name
|
||||
--> tests_error_parser/invalid_section.hurl:2:2
|
||||
|
|
||||
2 | [Asserts]
|
||||
| ^ This is not a valid section for a request
|
||||
| ^ the section is not valid. Valid values are QueryStringParams, FormParams, MultipartFormData or Cookies
|
||||
|
|
||||
|
||||
|
@ -2,6 +2,6 @@ error: Parsing Method
|
||||
--> tests_error_parser/method.hurl:1:1
|
||||
|
|
||||
1 | GeT http://localhost:8000/hello
|
||||
| ^ Available HTTP Method GET, POST, ...
|
||||
| ^ The HTTP method is not valid. Did you mean GET?
|
||||
|
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
error: Parsing predicate
|
||||
--> tests_error_parser/predicate.hurl:3:15
|
||||
--> tests_error_parser/predicate.hurl:4:15
|
||||
|
|
||||
3 | header "toto" tata startsWith "hello"
|
||||
4 | header "toto" tata startsWith "hello"
|
||||
| ^ expecting a predicate
|
||||
|
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
GET http://localhost:8000/hello
|
||||
HTTP/* 200
|
||||
[Asserts]
|
||||
header "toto" tata startsWith "hello"
|
||||
|
@ -1,7 +1,7 @@
|
||||
error: Parsing predicate value
|
||||
--> tests_error_parser/predicate_equal_value.hurl:3:22
|
||||
--> tests_error_parser/predicate_equal_value.hurl:4:22
|
||||
|
|
||||
3 | header "toto" equals xx
|
||||
4 | header "toto" equals xx
|
||||
| ^ invalid predicate value
|
||||
|
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
GET http://localhost:8000/hello
|
||||
HTTP/* 200
|
||||
[Asserts]
|
||||
header "toto" equals xx
|
||||
|
@ -1,7 +1,7 @@
|
||||
error: Parsing literal
|
||||
--> tests_error_parser/predicate_startwith_value.hurl:3:26
|
||||
--> tests_error_parser/predicate_startwith_value.hurl:4:26
|
||||
|
|
||||
3 | header "toto" startsWith 1
|
||||
4 | header "toto" startsWith 1
|
||||
| ^ expecting '"'
|
||||
|
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
GET http://localhost:8000/hello
|
||||
HTTP/* 200
|
||||
[Asserts]
|
||||
header "toto" startsWith 1
|
||||
|
@ -1,7 +1,7 @@
|
||||
error: Parsing section name
|
||||
error: Parsing request section name
|
||||
--> tests_error_parser/section_name.hurl:2:2
|
||||
|
|
||||
2 | [Unknown]
|
||||
| ^ the section Unknown is not valid
|
||||
| ^ the section is not valid. Valid values are QueryStringParams, FormParams, MultipartFormData or Cookies
|
||||
|
|
||||
|
||||
|
@ -18,6 +18,7 @@
|
||||
use super::ast::SourceInfo;
|
||||
use super::parser;
|
||||
use super::parser::ParseError;
|
||||
use core::cmp;
|
||||
|
||||
pub trait Error {
|
||||
fn source_info(&self) -> SourceInfo;
|
||||
@ -41,7 +42,8 @@ impl Error for parser::Error {
|
||||
ParseError::Filename { .. } => "Parsing Filename".to_string(),
|
||||
ParseError::Expecting { .. } => "Parsing literal".to_string(),
|
||||
ParseError::Space { .. } => "Parsing space".to_string(),
|
||||
ParseError::SectionName { .. } => "Parsing section name".to_string(),
|
||||
ParseError::RequestSectionName { .. } => "Parsing request section name".to_string(),
|
||||
ParseError::ResponseSectionName { .. } => "Parsing response section name".to_string(),
|
||||
ParseError::JsonpathExpr { .. } => "Parsing jsonpath expression".to_string(),
|
||||
ParseError::XPathExpr { .. } => "Parsing xpath expression".to_string(),
|
||||
ParseError::TemplateVariable { .. } => "Parsing template variable".to_string(),
|
||||
@ -62,13 +64,29 @@ impl Error for parser::Error {
|
||||
|
||||
fn fixme(&self) -> String {
|
||||
match self.inner.clone() {
|
||||
ParseError::Method { .. } => "Available HTTP Method GET, POST, ...".to_string(),
|
||||
ParseError::Method { name }
|
||||
=> format!("The HTTP method is not valid. {}", did_you_mean(
|
||||
&["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"],
|
||||
name.as_str(),
|
||||
"Available HTTP Methods are GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE or PATCH",
|
||||
)),
|
||||
ParseError::Version { .. } => "The http version must be 1.0, 1.1, 2 or *".to_string(),
|
||||
ParseError::Status { .. } => "The http status is not valid".to_string(),
|
||||
ParseError::Filename { .. } => "expecting a filename".to_string(),
|
||||
ParseError::Expecting { value } => format!("expecting '{}'", value),
|
||||
ParseError::Space { .. } => "expecting a space".to_string(),
|
||||
ParseError::SectionName { name } => format!("the section {} is not valid", name),
|
||||
ParseError::RequestSectionName { name }
|
||||
=> format!("the section is not valid. {}", did_you_mean(
|
||||
&["QueryStringParams", "FormParams", "MultipartFormData", "Cookies"],
|
||||
name.as_str(),
|
||||
"Valid values are QueryStringParams, FormParams, MultipartFormData or Cookies",
|
||||
)),
|
||||
ParseError::ResponseSectionName { name }
|
||||
=> format!("the section is not valid. {}", did_you_mean(
|
||||
&["Captures", "Asserts"],
|
||||
name.as_str(),
|
||||
"Valid values are Captures or Asserts",
|
||||
)),
|
||||
ParseError::JsonpathExpr { .. } => "expecting a jsonpath expression".to_string(),
|
||||
ParseError::XPathExpr { .. } => "expecting a xpath expression".to_string(),
|
||||
ParseError::TemplateVariable { .. } => "expecting a variable".to_string(),
|
||||
@ -95,3 +113,86 @@ impl Error for parser::Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn did_you_mean(valid_values: &[&str], actual: &str, default: &str) -> String {
|
||||
if let Some(suggest) = suggestion(valid_values, actual) {
|
||||
format!("Did you mean {}?", suggest)
|
||||
} else {
|
||||
default.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn suggestion(valid_values: &[&str], actual: &str) -> Option<String> {
|
||||
for value in valid_values {
|
||||
if levenshtein_distance(
|
||||
value.to_lowercase().as_str(),
|
||||
actual.to_lowercase().as_str(),
|
||||
) < 2
|
||||
{
|
||||
return Some(value.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// from https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Rust
|
||||
fn levenshtein_distance(s1: &str, s2: &str) -> usize {
|
||||
let v1: Vec<char> = s1.chars().collect();
|
||||
let v2: Vec<char> = s2.chars().collect();
|
||||
|
||||
fn min3<T: Ord>(v1: T, v2: T, v3: T) -> T {
|
||||
cmp::min(v1, cmp::min(v2, v3))
|
||||
}
|
||||
fn delta(x: char, y: char) -> usize {
|
||||
if x == y {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
let mut column: Vec<usize> = (0..=v1.len()).collect();
|
||||
for x in 1..=v2.len() {
|
||||
column[0] = x;
|
||||
let mut lastdiag = x - 1;
|
||||
for y in 1..=v1.len() {
|
||||
let olddiag = column[y];
|
||||
column[y] = min3(
|
||||
column[y] + 1,
|
||||
column[y - 1] + 1,
|
||||
lastdiag + delta(v1[y - 1], v2[x - 1]),
|
||||
);
|
||||
lastdiag = olddiag;
|
||||
}
|
||||
}
|
||||
column[v1.len()]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_levenshtein() {
|
||||
assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
|
||||
assert_eq!(levenshtein_distance("Saturday", "Sunday"), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_suggestion() {
|
||||
let valid_values = ["Captures", "Asserts"];
|
||||
assert_eq!(
|
||||
suggestion(&valid_values, "Asserts"),
|
||||
Some("Asserts".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
suggestion(&valid_values, "Assert"),
|
||||
Some("Asserts".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
suggestion(&valid_values, "assert"),
|
||||
Some("Asserts".to_string())
|
||||
);
|
||||
assert_eq!(suggestion(&valid_values, "asser"), None);
|
||||
}
|
||||
}
|
||||
|
@ -28,13 +28,14 @@ pub struct Error {
|
||||
pub enum ParseError {
|
||||
Expecting { value: String },
|
||||
|
||||
Method {},
|
||||
Method { name: String },
|
||||
Version {},
|
||||
Status {},
|
||||
Filename {},
|
||||
FileContentType {},
|
||||
Space {},
|
||||
SectionName { name: String },
|
||||
RequestSectionName { name: String },
|
||||
ResponseSectionName { name: String },
|
||||
JsonpathExpr {},
|
||||
XPathExpr {},
|
||||
TemplateVariable {},
|
||||
|
@ -123,7 +123,17 @@ fn response(reader: &mut Reader) -> ParseResult<'static, Response> {
|
||||
}
|
||||
|
||||
fn method(reader: &mut Reader) -> ParseResult<'static, Method> {
|
||||
if reader.is_eof() {
|
||||
return Err(Error {
|
||||
pos: reader.state.pos.clone(),
|
||||
recoverable: true,
|
||||
inner: ParseError::Method {
|
||||
name: "<EOF>".to_string(),
|
||||
},
|
||||
});
|
||||
}
|
||||
let start = reader.state.clone();
|
||||
let name = reader.read_while(|c| c.is_alphanumeric());
|
||||
let available_methods = vec![
|
||||
("GET", Method::Get),
|
||||
("HEAD", Method::Head),
|
||||
@ -137,15 +147,15 @@ fn method(reader: &mut Reader) -> ParseResult<'static, Method> {
|
||||
];
|
||||
|
||||
for (s, method) in available_methods {
|
||||
if try_literal(s, reader).is_ok() {
|
||||
if name == s {
|
||||
return Ok(method);
|
||||
}
|
||||
}
|
||||
|
||||
reader.state = start.clone();
|
||||
Err(Error {
|
||||
pos: start.pos,
|
||||
recoverable: reader.is_eof(),
|
||||
inner: ParseError::Method {},
|
||||
recoverable: false,
|
||||
inner: ParseError::Method { name },
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -27,41 +27,16 @@ use super::string::*;
|
||||
use super::ParseResult;
|
||||
|
||||
pub fn request_sections(reader: &mut Reader) -> ParseResult<'static, Vec<Section>> {
|
||||
let sections = zero_or_more(|p1| section(p1), reader)?;
|
||||
for section in sections.clone() {
|
||||
if ![
|
||||
"QueryStringParams",
|
||||
"FormParams",
|
||||
"MultipartFormData",
|
||||
"Cookies",
|
||||
]
|
||||
.contains(§ion.name())
|
||||
{
|
||||
return Err(Error {
|
||||
pos: section.source_info.start,
|
||||
recoverable: false,
|
||||
inner: ParseError::RequestSection,
|
||||
});
|
||||
}
|
||||
}
|
||||
let sections = zero_or_more(|p1| request_section(p1), reader)?;
|
||||
Ok(sections)
|
||||
}
|
||||
|
||||
pub fn response_sections(reader: &mut Reader) -> ParseResult<'static, Vec<Section>> {
|
||||
let sections = zero_or_more(|p1| section(p1), reader)?;
|
||||
for section in sections.clone() {
|
||||
if !["Captures", "Asserts"].contains(§ion.name()) {
|
||||
return Err(Error {
|
||||
pos: section.source_info.start,
|
||||
recoverable: false,
|
||||
inner: ParseError::RequestSection,
|
||||
});
|
||||
}
|
||||
}
|
||||
let sections = zero_or_more(|p1| response_section(p1), reader)?;
|
||||
Ok(sections)
|
||||
}
|
||||
|
||||
fn section(reader: &mut Reader) -> ParseResult<'static, Section> {
|
||||
fn request_section(reader: &mut Reader) -> ParseResult<'static, Section> {
|
||||
let line_terminators = optional_line_terminators(reader)?;
|
||||
let space0 = zero_or_more_spaces(reader)?;
|
||||
let start = reader.state.clone();
|
||||
@ -76,6 +51,38 @@ fn section(reader: &mut Reader) -> ParseResult<'static, Section> {
|
||||
"FormParams" => section_value_form_params(reader)?,
|
||||
"MultipartFormData" => section_value_multipart_form_data(reader)?,
|
||||
"Cookies" => section_value_cookies(reader)?,
|
||||
_ => {
|
||||
return Err(Error {
|
||||
pos: Pos {
|
||||
line: start.pos.line,
|
||||
column: start.pos.column + 1,
|
||||
},
|
||||
recoverable: false,
|
||||
inner: ParseError::RequestSectionName { name: name.clone() },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Section {
|
||||
line_terminators,
|
||||
space0,
|
||||
line_terminator0,
|
||||
value,
|
||||
source_info,
|
||||
})
|
||||
}
|
||||
|
||||
fn response_section(reader: &mut Reader) -> ParseResult<'static, Section> {
|
||||
let line_terminators = optional_line_terminators(reader)?;
|
||||
let space0 = zero_or_more_spaces(reader)?;
|
||||
let start = reader.state.clone();
|
||||
let name = section_name(reader)?;
|
||||
let source_info = SourceInfo {
|
||||
start: start.clone().pos,
|
||||
end: reader.state.clone().pos,
|
||||
};
|
||||
let line_terminator0 = line_terminator(reader)?;
|
||||
let value = match name.as_str() {
|
||||
"Captures" => section_value_captures(reader)?,
|
||||
"Asserts" => section_value_asserts(reader)?,
|
||||
_ => {
|
||||
@ -85,7 +92,7 @@ fn section(reader: &mut Reader) -> ParseResult<'static, Section> {
|
||||
column: start.pos.column + 1,
|
||||
},
|
||||
recoverable: false,
|
||||
inner: ParseError::SectionName { name: name.clone() },
|
||||
inner: ParseError::ResponseSectionName { name: name.clone() },
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -355,7 +362,7 @@ mod tests {
|
||||
Reader::init("[Asserts]\nheader \"Location\" equals \"https://google.fr\"\n");
|
||||
|
||||
assert_eq!(
|
||||
section(&mut reader).unwrap(),
|
||||
response_section(&mut reader).unwrap(),
|
||||
Section {
|
||||
line_terminators: vec![],
|
||||
space0: Whitespace {
|
||||
@ -447,7 +454,7 @@ mod tests {
|
||||
fn test_asserts_section_error() {
|
||||
let mut reader =
|
||||
Reader::init("x[Assertsx]\nheader Location equals \"https://google.fr\"\n");
|
||||
let error = section(&mut reader).err().unwrap();
|
||||
let error = response_section(&mut reader).err().unwrap();
|
||||
assert_eq!(error.pos, Pos { line: 1, column: 1 });
|
||||
assert_eq!(
|
||||
error.inner,
|
||||
@ -458,11 +465,11 @@ mod tests {
|
||||
assert_eq!(error.recoverable, true);
|
||||
|
||||
let mut reader = Reader::init("[Assertsx]\nheader Location equals \"https://google.fr\"\n");
|
||||
let error = section(&mut reader).err().unwrap();
|
||||
let error = response_section(&mut reader).err().unwrap();
|
||||
assert_eq!(error.pos, Pos { line: 1, column: 2 });
|
||||
assert_eq!(
|
||||
error.inner,
|
||||
ParseError::SectionName {
|
||||
ParseError::ResponseSectionName {
|
||||
name: String::from("Assertsx")
|
||||
}
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user