#![allow(
clippy::panic,
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::expect_used
)]
#[derive(Debug)]
pub struct ExpectedErrorMessage<'a> {
error: &'a str,
help: Option<&'a str>,
prefix: bool,
underlines: Vec<&'a str>,
}
#[derive(Debug)]
pub struct ExpectedErrorMessageBuilder<'a> {
error: &'a str,
help: Option<&'a str>,
prefix: bool,
underlines: Vec<&'a str>,
}
impl<'a> ExpectedErrorMessageBuilder<'a> {
pub fn error(msg: &'a str) -> Self {
Self {
error: msg,
help: None,
prefix: false,
underlines: vec![],
}
}
pub fn error_starts_with(msg: &'a str) -> Self {
Self {
error: msg,
help: None,
prefix: true,
underlines: vec![],
}
}
pub fn help(self, msg: &'a str) -> Self {
Self {
help: Some(msg),
..self
}
}
pub fn exactly_one_underline(self, snippet: &'a str) -> Self {
Self {
underlines: vec![snippet],
..self
}
}
pub fn build(self) -> ExpectedErrorMessage<'a> {
ExpectedErrorMessage {
error: self.error,
help: self.help,
prefix: self.prefix,
underlines: self.underlines,
}
}
}
impl<'a> ExpectedErrorMessage<'a> {
pub fn matches(&self, src: Option<&'a str>, error: &impl miette::Diagnostic) -> bool {
self.matches_error(error) && self.matches_help(error) && self.matches_underlines(src, error)
}
fn matches_error(&self, error: &impl miette::Diagnostic) -> bool {
let e_string = error.to_string();
if self.prefix {
e_string.starts_with(self.error)
} else {
e_string == self.error
}
}
#[track_caller]
fn expect_error_matches(&self, src: impl Into<OriginalInput<'a>>, error: &miette::Report) {
let e_string = error.to_string();
if self.prefix {
assert!(
e_string.starts_with(self.error),
"for the following input:\n{}\nfor the following error:\n{error:?}\n\nactual error did not start with the expected prefix\n actual error: {error}\n expected prefix: {}", src.into(),
self.error,
);
} else {
assert_eq!(
&e_string,
self.error,
"for the following input:\n{}\nfor the following error:\n{error:?}\n\nactual error did not match expected", src.into(),
);
}
}
fn matches_help(&self, error: &impl miette::Diagnostic) -> bool {
let h_string = error.help().map(|h| h.to_string());
if self.prefix {
match (h_string.as_deref(), self.help) {
(Some(actual), Some(expected)) => actual.starts_with(expected),
(None, None) => true,
_ => false,
}
} else {
h_string.as_deref() == self.help
}
}
#[track_caller]
fn expect_help_matches(&self, src: impl Into<OriginalInput<'a>>, error: &miette::Report) {
let h_string = error.help().map(|h| h.to_string());
if self.prefix {
match (h_string.as_deref(), &self.help) {
(Some(actual), Some(expected)) => {
assert!(
actual.starts_with(expected),
"for the following input:\n{}\nfor the following error:\n{error:?}\n\nactual help did not start with the expected prefix\n actual help: {actual}\n expected help: {expected}", src.into(),
)
}
(None, None) => (),
(Some(actual), None) => panic!(
"for the following input:\n{}\nfor the following error:\n{error:?}\n\ndid not expect a help message, but found one: {actual}", src.into(),
),
(None, Some(expected)) => panic!(
"for the following input:\n{}\nfor the following error:\n{error:?}\n\ndid not find a help message, but expected one: {expected}", src.into(),
),
}
} else {
assert_eq!(
h_string.as_deref(),
self.help,
"for the following input:\n{}\nfor the following error:\n{error:?}\n\nactual help did not match expected", src.into(),
);
}
}
fn matches_underlines(&self, src: Option<&'a str>, err: &impl miette::Diagnostic) -> bool {
let expected_num_labels = self.underlines.len();
let actual_num_labels = err.labels().map(|iter| iter.count()).unwrap_or(0);
if expected_num_labels != actual_num_labels {
return false;
}
if expected_num_labels == 0 {
true
} else {
let src =
src.expect("src can be `None` only in the case where we expect no underlines");
for (expected, actual) in self
.underlines
.iter()
.zip(err.labels().unwrap_or_else(|| Box::new(std::iter::empty())))
{
let actual_snippet = {
let span = actual.inner();
&src[span.offset()..span.offset() + span.len()]
};
if expected != &actual_snippet {
return false;
}
}
true
}
}
#[track_caller]
fn expect_underlines_match(&self, src: Option<&'a str>, err: &miette::Report) {
let expected_num_labels = self.underlines.len();
let actual_num_labels = err.labels().map(|iter| iter.count()).unwrap_or(0);
assert_eq!(expected_num_labels, actual_num_labels, "in the following error:\n{err:?}\n\nexpected {expected_num_labels} underlines but found {actual_num_labels}"); if expected_num_labels != 0 {
let src =
src.expect("src can be `None` only in the case where we expect no underlines");
for (expected, actual) in self
.underlines
.iter()
.zip(err.labels().unwrap_or_else(|| Box::new(std::iter::empty())))
{
let actual_snippet = {
let span = actual.inner();
&src[span.offset()..span.offset() + span.len()]
};
assert_eq!(
expected,
&actual_snippet,
"in the following error:\n{err:?}\n\nexpected underlined portion to be:\n {expected}\nbut it was:\n {actual_snippet}", );
}
}
}
}
impl<'a> std::fmt::Display for ExpectedErrorMessage<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.prefix {
writeln!(f, "expected error to start with: {}", self.error)?;
match self.help {
Some(help) => writeln!(f, "expected help to start with: {help}")?,
None => writeln!(f, " with no help message")?,
}
} else {
writeln!(f, "expected error: {}", self.error)?;
match self.help {
Some(help) => writeln!(f, "expected help: {help}")?,
None => writeln!(f, " with no help message")?,
}
}
if self.underlines.is_empty() {
writeln!(f, "and expected no source locations / underlined segments.")?;
} else {
writeln!(f, "and expected the following underlined segments:")?;
for underline in &self.underlines {
writeln!(f, " {underline}")?;
}
}
Ok(())
}
}
#[derive(Debug)]
pub enum OriginalInput<'a> {
String(&'a str),
Json(&'a serde_json::Value),
}
impl<'a> From<&'a str> for OriginalInput<'a> {
fn from(value: &'a str) -> Self {
Self::String(value)
}
}
impl<'a> From<&'a serde_json::Value> for OriginalInput<'a> {
fn from(value: &'a serde_json::Value) -> Self {
Self::Json(value)
}
}
impl<'a> std::fmt::Display for OriginalInput<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::String(s) => write!(f, "{s}"),
Self::Json(val) => write!(f, "{}", serde_json::to_string_pretty(val).unwrap()),
}
}
}
#[track_caller] pub fn expect_err<'a>(
src: impl Into<OriginalInput<'a>> + Copy,
err: &miette::Report,
msg: &ExpectedErrorMessage<'a>,
) {
msg.expect_error_matches(src, err);
msg.expect_help_matches(src, err);
if msg.underlines.is_empty() {
msg.expect_underlines_match(None, err);
} else {
match src.into() {
OriginalInput::String(s) => {
msg.expect_underlines_match(Some(s), err);
}
OriginalInput::Json(val) => {
let src = serde_json::to_string_pretty(val).unwrap();
msg.expect_underlines_match(Some(&src), err);
}
}
}
}