use std::{
collections::{BTreeMap, BTreeSet},
fmt,
};
use super::{
Caveat, CaveatDeferred, Element, Error, ErrorSet, Group, Id, Set, Source, Verdict, Warning,
};
use crate::{
json::{self, test::PathGlob},
test::{ExpectValue, Expectation, WarningMap},
warning::SetDeferred,
};
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
}
pub fn into_warnings(self) -> Set<W> {
self.warnings
}
}
impl<W> ErrorSet<W>
where
W: Warning,
{
#[track_caller]
pub fn unwrap(self) -> Error<W> {
let Self { error, warnings } = self;
let warnings = Set(warnings);
assert!(warnings.is_empty(), "{:#?}", warnings.path_id_map());
*error
}
}
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> Set<W>
where
W: Warning,
{
pub(crate) fn into_path_as_str_map(self) -> BTreeMap<String, Vec<W>> {
self.0
.into_values()
.map(|Group { element, warnings }| {
let warnings = warnings.into_iter().map(Source::into_warning).collect();
(element.path.into_string(), 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()
}
pub fn into_warnings(self) -> Vec<W> {
self.0.into_iter().map(|s| s.warning).collect()
}
}
#[derive(Debug)]
pub struct ErrorSourceContext<'buf, W: Warning> {
pub context: &'buf str,
pub element_path: json::Path,
pub element_position: json::Location,
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> {
#[expect(
clippy::as_conversions,
reason = "The index is guaranteed within bounds by the parser"
)]
pub(crate) fn into_context(
self,
json: &str,
) -> Result<ErrorSourceContext<'_, W>, IncorrectSource> {
let Self { warning, element } = self;
let Element {
id: _,
span,
path,
location: _,
} = element;
let Some(lead_in) = json.get(..span.start as usize) else {
return Err(IncorrectSource(()));
};
let element_position = json::line_col(lead_in);
let Some(context) = json.get(span.start as usize..span.end as usize) else {
return Err(IncorrectSource(()));
};
Ok(ErrorSourceContext {
context,
element_path: path,
element_position,
error: warning,
})
}
}
#[track_caller]
pub(crate) fn assert_warnings<W>(
expectation: &str,
warnings: &Set<W>,
expected: Expectation<WarningMap>,
) where
W: Warning,
{
let reported = warnings
.iter()
.map(|group| Reported {
path: &group.element.path,
ids: group.warnings.iter().map(|source| source.id()).collect(),
messages: group
.warnings
.iter()
.map(|source| source.to_string())
.collect(),
})
.collect::<Vec<_>>();
assert_reported(expectation, &reported, expected);
}
#[track_caller]
pub(crate) fn assert_path_map_warnings<W>(
expectation: &str,
warnings: &BTreeMap<json::Path, Vec<W>>,
expected: Expectation<WarningMap>,
) where
W: Warning,
{
let reported = warnings
.iter()
.map(|(path, warnings)| Reported {
path,
ids: warnings.iter().map(Warning::id).collect(),
messages: warnings.iter().map(ToString::to_string).collect(),
})
.collect::<Vec<_>>();
assert_reported(expectation, &reported, expected);
}
struct Reported<'caller> {
path: &'caller json::Path,
ids: BTreeSet<Id>,
messages: Vec<String>,
}
#[track_caller]
fn assert_reported(
expectation: &str,
reported: &[Reported<'_>],
expected: Expectation<WarningMap>,
) {
let Expectation::Present(ExpectValue::Some(expected)) = expected else {
let ids = reported
.iter()
.map(|report| (report.path.as_str(), &report.ids))
.collect::<BTreeMap<_, _>>();
let messages = reported
.iter()
.map(|report| (report.path.as_str(), &report.messages))
.collect::<BTreeMap<_, _>>();
assert!(
reported.is_empty(),
"There is no {expectation} but these warnings were reported;\n{ids:#?}\n\
These warnings have the messages:\n{messages:#?}"
);
return;
};
let mut elems_missing_from_expect = vec![];
let mut unequal_warnings = vec![];
let mut ambiguous_elems = vec![];
for report in reported {
let Reported {
path,
ids,
messages,
} = report;
let mut entries_matched = expected.iter().filter(|(glob, _ids)| glob.matches(path));
let Some((entry, ids_expected)) = entries_matched.next() else {
elems_missing_from_expect.push(report);
continue;
};
let entries_extra = entries_matched.map(|(glob, _ids)| glob).collect::<Vec<_>>();
if !entries_extra.is_empty() {
let entries = std::iter::once(entry).chain(entries_extra).collect();
ambiguous_elems.push(Ambiguous {
path: path.as_str(),
entries,
});
continue;
}
let ids_expected = ids_expected
.iter()
.cloned()
.map(Id::from_string)
.collect::<BTreeSet<_>>();
if *ids != ids_expected {
unequal_warnings.push(Unequal {
path: path.as_str(),
entry,
expected: ids_expected,
actual: ids.clone(),
messages: messages.clone(),
});
}
}
let entries_unused = expected
.keys()
.filter(|glob| !reported.iter().any(|report| glob.matches(report.path)))
.collect::<Vec<_>>();
let mut problems = vec![];
if !elems_missing_from_expect.is_empty() {
let missing = elems_missing_from_expect
.iter()
.map(|report| (report.path.as_str(), &report.ids))
.collect::<BTreeMap<_, _>>();
let messages = elems_missing_from_expect
.iter()
.map(|report| (report.path.as_str(), &report.messages))
.collect::<BTreeMap<_, _>>();
problems.push(format!(
"Elements with warnings that no entry of {expectation} matches:\n{missing:#?}\n\
These warnings have the messages:\n{messages:#?}"
));
}
if !unequal_warnings.is_empty() {
problems.push(format!(
"Elements whose warnings are not the warnings listed by {expectation}:\n{unequal_warnings:#?}"
));
}
if !ambiguous_elems.is_empty() {
problems.push(format!(
"Elements matched by more than one entry of {expectation}. Narrow the entries so that \
each element is matched once:\n{ambiguous_elems:#?}"
));
}
if !entries_unused.is_empty() {
problems.push(format!(
"Entries of {expectation} that match no element with warnings:\n{entries_unused:#?}"
));
}
assert!(problems.is_empty(), "{}", problems.join("\n\n"));
}
#[derive(Debug)]
#[expect(
dead_code,
reason = "the fields are read by the derived `Debug` in the panic message"
)]
struct Unequal<'caller> {
path: &'caller str,
entry: &'caller PathGlob,
expected: BTreeSet<Id>,
actual: BTreeSet<Id>,
messages: Vec<String>,
}
#[derive(Debug)]
#[expect(
dead_code,
reason = "the fields are read by the derived `Debug` in the panic message"
)]
struct Ambiguous<'caller> {
path: &'caller str,
entries: Vec<&'caller PathGlob>,
}