use std::fmt;
use super::finding::{Finding, Severity};
use super::path::path_key;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Summary {
pub errors: usize,
pub evaluation_errors: usize,
pub warnings: usize,
pub unsupported: usize,
}
impl Summary {
#[must_use]
pub const fn is_conformant(&self) -> bool {
self.errors == 0 && self.evaluation_errors == 0
}
}
impl fmt::Display for Summary {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{} errors, {} evaluation errors, {} warnings, {} unsupported",
self.errors, self.evaluation_errors, self.warnings, self.unsupported
)
}
}
#[derive(Debug, Clone)]
pub struct Report {
findings: Vec<Finding>,
truncated: bool,
max_findings: usize,
}
impl Default for Report {
fn default() -> Self {
Self {
findings: Vec::new(),
truncated: false,
max_findings: usize::MAX,
}
}
}
impl PartialEq for Report {
fn eq(&self, other: &Self) -> bool {
self.findings == other.findings && self.truncated == other.truncated
}
}
impl Eq for Report {}
impl Report {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub(crate) fn with_max_findings(max_findings: usize) -> Self {
Self {
max_findings,
..Self::default()
}
}
pub fn push(&mut self, finding: Finding) {
if self.findings.len() >= self.max_findings {
self.truncated = true;
return;
}
self.findings.push(finding);
}
pub fn mark_truncated(&mut self) {
self.truncated = true;
}
#[must_use]
pub const fn is_truncated(&self) -> bool {
self.truncated
}
#[must_use]
pub fn findings(&self) -> &[Finding] {
&self.findings
}
#[must_use]
pub fn summary(&self) -> Summary {
let mut summary = Summary::default();
for finding in &self.findings {
match finding.severity {
Severity::Error => summary.errors += 1,
Severity::EvaluationError => summary.evaluation_errors += 1,
Severity::Warning => summary.warnings += 1,
Severity::Unsupported => summary.unsupported += 1,
}
}
summary
}
#[must_use]
pub fn sorted(&self) -> Vec<&Finding> {
let mut sorted: Vec<&Finding> = self.findings.iter().collect();
sorted.sort_by(|left, right| {
left.severity
.cmp(&right.severity)
.then_with(|| left.rule.cmp(&right.rule))
.then_with(|| path_key(&left.path).cmp(&path_key(&right.path)))
.then_with(|| left.message.cmp(&right.message))
});
sorted
}
pub fn extend(&mut self, other: Self) {
let other_truncated = other.truncated;
for finding in other.findings {
self.push(finding);
}
self.truncated |= other_truncated;
}
#[must_use]
pub fn is_conformant(&self) -> bool {
self.summary().is_conformant()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::report::Path;
use ifc_model::EntityId;
#[test]
fn unsupported_rules_do_not_make_a_file_non_conformant() {
let mut report = Report::new();
report.push(Finding::unsupported(
"where.IfcRoot.WR1",
Path::File,
"not implemented",
));
assert!(
report.is_conformant(),
"an unchecked rule is a fact about the validator"
);
assert_eq!(report.summary().unsupported, 1);
}
#[test]
fn an_evaluation_error_makes_a_file_non_conformant() {
let mut report = Report::new();
report.push(Finding::evaluation_error(
"IfcMaterialLayer.NormalizedPriority",
Path::Entity(EntityId(1)),
"Priority is a string, not an integer",
));
let summary = report.summary();
assert_eq!(summary.evaluation_errors, 1);
assert_eq!(summary.errors, 0, "counted separately from violations");
assert!(!report.is_conformant());
assert!(summary.to_string().contains("1 evaluation errors"));
}
#[test]
fn sorting_is_total_and_stable() {
let mut report = Report::new();
report.push(Finding::warning("b.rule", Path::Entity(EntityId(9)), "w"));
report.push(Finding::error("a.rule", Path::Entity(EntityId(2)), "e"));
report.push(Finding::error("a.rule", Path::Entity(EntityId(1)), "e"));
let order: Vec<String> = report
.sorted()
.iter()
.map(|finding| finding.path.to_string())
.collect();
assert_eq!(order, ["#1", "#2", "#9"], "errors first, then by entity");
}
#[test]
fn extend_honors_the_storage_cap() {
let mut target = Report::with_max_findings(1);
let mut source = Report::new();
source.push(Finding::error("a", Path::File, "first"));
source.push(Finding::error("b", Path::File, "second"));
target.extend(source);
assert_eq!(target.findings().len(), 1);
assert!(target.is_truncated());
}
#[test]
fn truncation_survives_a_merge() {
let mut left = Report::new();
let mut right = Report::new();
right.mark_truncated();
left.extend(right);
assert!(left.is_truncated());
}
}