use std::{
collections::{BTreeMap, BTreeSet},
fmt,
};
use crate::{
json,
test::{ExpectValue, Expectation},
warning::SetDeferred,
};
use super::{
Caveat, CaveatDeferred, Element, Error, ErrorSet, Group, Id, Path, Set, Verdict, Warning,
};
pub trait VerdictTestExt<T, W: Warning> {
fn unwrap_only_error(self) -> Error<W>;
}
impl<T, W: Warning> VerdictTestExt<T, W> for Verdict<T, W>
where
T: fmt::Debug,
{
fn unwrap_only_error(self) -> Error<W> {
let error = match self {
Ok(c) => panic!("called `Result::unwrap_only_error` on an `Ok` value: {c:?}"),
Err(set) => {
let ErrorSet { error, warnings: _ } = set;
*error
}
};
error
}
}
impl<T, W> Caveat<T, W>
where
W: Warning,
{
#[track_caller]
pub fn unwrap(self) -> T {
let Self { value, warnings } = self;
assert!(warnings.is_empty(), "{:#?}", warnings.path_id_map());
value
}
}
impl<T, W> CaveatDeferred<T, W>
where
W: Warning,
{
pub fn unwrap(self) -> T {
let Self { value, warnings } = self;
assert!(warnings.is_empty(), "{:#?}", warnings.id_map());
value
}
}
impl<W> Group<W>
where
W: Warning,
{
fn path_and_ids(&self) -> (&str, Vec<Id>) {
let Self { element, warnings } = self;
(
element.path.as_str(),
warnings.iter().map(|w| w.id()).collect(),
)
}
fn ids(&self) -> Vec<Id> {
self.warnings.iter().map(|w| w.id()).collect()
}
}
pub(crate) struct Debug<'a, W: Warning> {
location: Location,
id: Id,
message: Message<'a, W>,
}
impl<W: Warning> fmt::Debug for Debug<'_, W> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_map()
.entry(&"location", &self.location)
.entry(&"id", &self.id)
.entry(&"message", &self.message)
.finish()
}
}
struct Location(&'static std::panic::Location<'static>);
impl fmt::Debug for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
struct Message<'a, W: Warning>(&'a W);
impl<W: Warning> fmt::Debug for Message<'_, W> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<W> Set<W>
where
W: Warning,
{
pub(crate) fn path_debug_map(&self) -> BTreeMap<&str, Vec<Debug<'_, W>>> {
self.0
.values()
.map(|group| {
let warnings = group
.warnings
.iter()
.map(|super::Source { location, warning }| Debug {
location: Location(location),
id: warning.id(),
message: Message(warning),
})
.collect();
(group.element.path.as_str(), warnings)
})
.collect()
}
pub(crate) fn into_path_as_str_map(self) -> BTreeMap<String, Vec<W>> {
self.0
.into_values()
.map(|Group { element, warnings }| {
let path = element.path.0;
let warnings = warnings
.into_iter()
.map(super::Source::into_warning)
.collect();
(path, warnings)
})
.collect()
}
}
impl<W: Warning> SetDeferred<W> {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn id_map(&self) -> Vec<Id> {
self.0.iter().map(|w| w.id()).collect()
}
}
#[derive(Debug)]
pub struct ErrorSourceContext<'buf, W: Warning> {
pub context: &'buf str,
pub element_path: Path,
pub element_position: json::LineCol,
pub error: W,
}
impl<W: Warning> fmt::Display for ErrorSourceContext<'_, W> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"The element at `{}` path `{}: {}` has an error: {}",
self.element_position, self.element_path, self.context, self.error
)
}
}
pub struct IncorrectSource(());
impl fmt::Debug for IncorrectSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl fmt::Display for IncorrectSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("The JSON given is not the JSON that generated these warnings")
.field(&self.0)
.finish()
}
}
impl std::error::Error for IncorrectSource {}
impl<W: Warning> Error<W> {
pub(crate) fn into_context(
self,
json: &str,
) -> Result<ErrorSourceContext<'_, W>, IncorrectSource> {
let Self { warning, element } = self;
let Element { id: _, span, path } = element;
let Some(lead_in) = json.get(..span.start) else {
return Err(IncorrectSource(()));
};
let element_position = json::line_col(lead_in);
let Some(context) = json.get(span.start..span.end) else {
return Err(IncorrectSource(()));
};
Ok(ErrorSourceContext {
context,
element_path: path,
element_position,
error: warning,
})
}
}
#[track_caller]
pub(crate) fn assert_warnings<W>(
expect_file_name: &str,
warnings: &Set<W>,
expected: Expectation<BTreeMap<String, Vec<String>>>,
) where
W: Warning,
{
let Expectation::Present(ExpectValue::Some(mut expected)) = expected else {
assert!(
warnings.is_empty(),
"There is no `warnings` field in the `{expect_file_name}` file but the tariff has warnings;\n{:#?}",
warnings.path_id_map()
);
return;
};
{
let warnings_grouped = warnings
.iter()
.map(|Group { element, warnings }| (element.path.as_str(), warnings))
.collect::<BTreeMap<_, _>>();
let mut elems_in_expect_without_warning = vec![];
for elem_path in expected.keys() {
if !warnings_grouped.contains_key(elem_path.as_str()) {
elems_in_expect_without_warning.push(elem_path);
}
}
assert!(elems_in_expect_without_warning.is_empty(),
"The expect file `{expect_file_name}` has entries for elements that have no warnings:\n\
{elems_in_expect_without_warning:#?}"
);
}
let mut elems_missing_from_expect = vec![];
let mut unequal_warnings = vec![];
for group in warnings {
let Some(warnings_expected) = expected.remove(group.element.path.as_str()) else {
elems_missing_from_expect.push(group);
continue;
};
let warnings_expected = warnings_expected
.into_iter()
.map(Id::from_string)
.collect::<BTreeSet<_>>();
let warnings = group.ids().into_iter().collect::<BTreeSet<_>>();
if warnings_expected != warnings {
unequal_warnings.push(group);
}
}
if !elems_missing_from_expect.is_empty() || !unequal_warnings.is_empty() {
let missing = elems_missing_from_expect
.into_iter()
.map(Group::path_and_ids)
.collect::<BTreeMap<_, _>>();
let unequal = unequal_warnings
.into_iter()
.map(Group::path_and_ids)
.collect::<BTreeMap<_, _>>();
match (!missing.is_empty(), !unequal.is_empty()) {
(true, true) => panic!(
"Elements with warnings but are not defined in the `{expect_file_name}` file:\n{missing:#?}\n\
Elements that are in the `{expect_file_name}` file but the warnings list is not correct.\n\
The warnings reported are: \n{unequal:#?}"
),
(true, false) => panic!("Elements with warnings but are not defined in the `{expect_file_name}` file:\n{missing:#?}"),
(false, true) => panic!(
"Elements that are in the `{expect_file_name}` file but the warnings list is not correct.\n\
The warnings reported are: \n{unequal:#?}"
),
(false, false) => (),
}
}
}