ifc_validate/report/
summary.rs1use std::fmt;
4
5use super::finding::{Finding, Severity};
6use super::path::path_key;
7
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
10pub struct Summary {
11 pub errors: usize,
13 pub warnings: usize,
15 pub unsupported: usize,
17}
18
19impl Summary {
20 #[must_use]
27 pub const fn is_conformant(&self) -> bool {
28 self.errors == 0
29 }
30}
31
32impl fmt::Display for Summary {
33 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34 write!(
35 formatter,
36 "{} errors, {} warnings, {} unsupported",
37 self.errors, self.warnings, self.unsupported
38 )
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct Report {
45 findings: Vec<Finding>,
46 truncated: bool,
47 max_findings: usize,
48}
49
50impl Default for Report {
51 fn default() -> Self {
52 Self {
53 findings: Vec::new(),
54 truncated: false,
55 max_findings: usize::MAX,
56 }
57 }
58}
59
60impl PartialEq for Report {
61 fn eq(&self, other: &Self) -> bool {
62 self.findings == other.findings && self.truncated == other.truncated
63 }
64}
65
66impl Eq for Report {}
67
68impl Report {
69 #[must_use]
71 pub fn new() -> Self {
72 Self::default()
73 }
74
75 pub(crate) fn with_max_findings(max_findings: usize) -> Self {
77 Self {
78 max_findings,
79 ..Self::default()
80 }
81 }
82
83 pub fn push(&mut self, finding: Finding) {
85 if self.findings.len() >= self.max_findings {
86 self.truncated = true;
87 return;
88 }
89 self.findings.push(finding);
90 }
91
92 pub fn mark_truncated(&mut self) {
97 self.truncated = true;
98 }
99
100 #[must_use]
102 pub const fn is_truncated(&self) -> bool {
103 self.truncated
104 }
105
106 #[must_use]
112 pub fn findings(&self) -> &[Finding] {
113 &self.findings
114 }
115
116 #[must_use]
118 pub fn summary(&self) -> Summary {
119 let mut summary = Summary::default();
120 for finding in &self.findings {
121 match finding.severity {
122 Severity::Error => summary.errors += 1,
123 Severity::Warning => summary.warnings += 1,
124 Severity::Unsupported => summary.unsupported += 1,
125 }
126 }
127 summary
128 }
129
130 #[must_use]
136 pub fn sorted(&self) -> Vec<&Finding> {
137 let mut sorted: Vec<&Finding> = self.findings.iter().collect();
138 sorted.sort_by(|left, right| {
139 left.severity
140 .cmp(&right.severity)
141 .then_with(|| left.rule.cmp(&right.rule))
142 .then_with(|| path_key(&left.path).cmp(&path_key(&right.path)))
143 .then_with(|| left.message.cmp(&right.message))
144 });
145 sorted
146 }
147
148 pub fn extend(&mut self, other: Self) {
150 let other_truncated = other.truncated;
151 for finding in other.findings {
152 self.push(finding);
153 }
154 self.truncated |= other_truncated;
155 }
156
157 #[must_use]
159 pub fn is_conformant(&self) -> bool {
160 self.summary().is_conformant()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::report::Path;
168 use ifc_model::EntityId;
169
170 #[test]
171 fn unsupported_rules_do_not_make_a_file_non_conformant() {
172 let mut report = Report::new();
173 report.push(Finding::unsupported(
174 "where.IfcRoot.WR1",
175 Path::File,
176 "not implemented",
177 ));
178 assert!(
179 report.is_conformant(),
180 "an unchecked rule is a fact about the validator"
181 );
182 assert_eq!(report.summary().unsupported, 1);
183 }
184
185 #[test]
186 fn sorting_is_total_and_stable() {
187 let mut report = Report::new();
188 report.push(Finding::warning("b.rule", Path::Entity(EntityId(9)), "w"));
189 report.push(Finding::error("a.rule", Path::Entity(EntityId(2)), "e"));
190 report.push(Finding::error("a.rule", Path::Entity(EntityId(1)), "e"));
191 let order: Vec<String> = report
192 .sorted()
193 .iter()
194 .map(|finding| finding.path.to_string())
195 .collect();
196 assert_eq!(order, ["#1", "#2", "#9"], "errors first, then by entity");
197 }
198
199 #[test]
201 fn extend_honors_the_storage_cap() {
202 let mut target = Report::with_max_findings(1);
203 let mut source = Report::new();
204 source.push(Finding::error("a", Path::File, "first"));
205 source.push(Finding::error("b", Path::File, "second"));
206
207 target.extend(source);
208
209 assert_eq!(target.findings().len(), 1);
210 assert!(target.is_truncated());
211 }
212
213 #[test]
215 fn truncation_survives_a_merge() {
216 let mut left = Report::new();
217 let mut right = Report::new();
218 right.mark_truncated();
219 left.extend(right);
220 assert!(left.is_truncated());
221 }
222}