Skip to main content

henad_explore/testing/
report.rs

1//! Reports of the checks over one model.
2
3use std::fmt;
4
5use super::ModelCheck;
6
7/// Failures and skipped checks of one model.
8#[derive(Debug)]
9pub struct ModelReport {
10    model_id: String,
11    failures: Vec<CheckFailure>,
12    skipped: Vec<SkippedCheck>,
13    thread_count_jobs: Option<usize>,
14}
15
16impl ModelReport {
17    pub(super) fn new(model_id: &str) -> Self {
18        Self {
19            model_id: model_id.to_owned(),
20            failures: Vec::new(),
21            skipped: Vec::new(),
22            thread_count_jobs: None,
23        }
24    }
25
26    pub(super) fn fail(&mut self, check: ModelCheck, message: String) {
27        self.failures.push(CheckFailure { check, message });
28    }
29
30    pub(super) fn skip(&mut self, check: ModelCheck, reason: SkipReason) {
31        self.skipped.push(SkippedCheck { check, reason });
32    }
33
34    pub(super) fn set_thread_count_jobs(&mut self, jobs: usize) {
35        self.thread_count_jobs = Some(jobs);
36    }
37
38    /// Id of the model checked.
39    pub fn model_id(&self) -> &str {
40        &self.model_id
41    }
42
43    /// Checks that failed, each with its message.
44    pub fn failures(&self) -> &[CheckFailure] {
45        &self.failures
46    }
47
48    /// Checks skipped, each with its reason: an exemption, a declared property, or a missing device.
49    pub fn skipped(&self) -> &[SkippedCheck] {
50        &self.skipped
51    }
52
53    /// Number of jobs a step split into when [`ModelCheck::ThreadCount`] passed, `None` when it did not run to a pass.
54    pub fn thread_count_jobs(&self) -> Option<usize> {
55        self.thread_count_jobs
56    }
57
58    /// Returns whether no check failed.
59    pub fn passed(&self) -> bool {
60        self.failures.is_empty()
61    }
62
63    /// Returns the reason `check` was skipped, or `None` when it ran.
64    pub fn skip_reason(&self, check: ModelCheck) -> Option<&SkipReason> {
65        self.skipped
66            .iter()
67            .find(|skipped| skipped.check == check)
68            .map(|skipped| &skipped.reason)
69    }
70
71    /// Asserts that no check failed.
72    ///
73    /// # Panics
74    ///
75    /// Panics with a summary line naming the failed checks, then every failure.
76    pub fn assert_passed(&self) {
77        assert!(self.passed(), "{self}");
78    }
79
80    /// Returns one line naming the model and its failed or skipped checks.
81    fn summary(&self) -> String {
82        let names = |checks: &mut dyn Iterator<Item = ModelCheck>| {
83            checks.map(|check| check.to_string()).collect::<Vec<_>>().join(", ")
84        };
85        if self.failures.is_empty() {
86            format!(
87                "Model '{}' passed its checks, with {} skipped.",
88                self.model_id,
89                self.skipped.len()
90            )
91        } else {
92            format!(
93                "Model '{}' failed {} of its checks: {}.",
94                self.model_id,
95                self.failures.len(),
96                names(&mut self.failures.iter().map(CheckFailure::check))
97            )
98        }
99    }
100}
101
102/// Writes a summary line, then every failure and every skipped check, one to a line.
103impl fmt::Display for ModelReport {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        writeln!(f, "{}", self.summary())?;
106        for failure in &self.failures {
107            writeln!(f, "  {failure}")?;
108        }
109        for skipped in &self.skipped {
110            writeln!(f, "  {skipped}")?;
111        }
112        Ok(())
113    }
114}
115
116/// One check that failed, with what it found.
117#[derive(Debug, Clone)]
118pub struct CheckFailure {
119    check: ModelCheck,
120    message: String,
121}
122
123impl CheckFailure {
124    /// Check that failed.
125    pub fn check(&self) -> ModelCheck {
126        self.check
127    }
128
129    /// Message saying what the check found.
130    pub fn message(&self) -> &str {
131        &self.message
132    }
133}
134
135impl fmt::Display for CheckFailure {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        write!(f, "{} failed: {}", self.check, self.message)
138    }
139}
140
141/// One check that did not run, with the reason.
142#[derive(Debug, Clone)]
143pub struct SkippedCheck {
144    check: ModelCheck,
145    reason: SkipReason,
146}
147
148impl SkippedCheck {
149    /// Check that did not run.
150    pub fn check(&self) -> ModelCheck {
151        self.check
152    }
153
154    /// Reason the check did not run.
155    pub fn reason(&self) -> &SkipReason {
156        &self.reason
157    }
158}
159
160impl fmt::Display for SkippedCheck {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        write!(f, "{} skipped: {}", self.check, self.reason)
163    }
164}
165
166/// Reason a check did not run.
167#[derive(Debug, Clone, PartialEq, Eq)]
168#[non_exhaustive]
169pub enum SkipReason {
170    /// The check applies to the other backend.
171    OtherBackend,
172    /// The model declares that its runs do not replay exactly, and the check compares two runs.
173    InexactReplay,
174    /// [`super::CheckSettings::exempt`] exempted the model.
175    Exempt(String),
176    /// The check needs a GPU device, and the settings provide no device.
177    NoDevice,
178    /// The check runs on native targets only. These are the checks that build a GPU model, and
179    /// [`super::ModelCheck::ThreadCount`].
180    NativeOnly,
181    /// A step of the model is one job at every size within its parameters' bounds.
182    OneJob,
183    /// A step of the model is one job at the size an override sets for the named parameter.
184    OneJobAtOverride(String),
185}
186
187impl fmt::Display for SkipReason {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        match self {
190            Self::OtherBackend => f.write_str("does not apply to this backend"),
191            Self::InexactReplay => f.write_str("the model declares that its runs do not replay exactly"),
192            Self::Exempt(reason) => write!(f, "exempt: {reason}"),
193            Self::NoDevice => f.write_str("no GPU device"),
194            Self::NativeOnly => f.write_str("runs on native targets only"),
195            Self::OneJob => f.write_str("one job at every size within bounds"),
196            Self::OneJobAtOverride(param_id) => write!(f, "one job at the {param_id} an override sets"),
197        }
198    }
199}
200
201/// Reports of every model of a set, and the model ids that the settings specify and that are missing from the set.
202#[derive(Debug)]
203pub struct SetReport {
204    reports: Vec<ModelReport>,
205    unknown_models: Vec<String>,
206}
207
208impl SetReport {
209    pub(super) fn new(reports: Vec<ModelReport>, unknown_models: Vec<String>) -> Self {
210        Self {
211            reports,
212            unknown_models,
213        }
214    }
215
216    /// Report of each model, in the set's order.
217    pub fn reports(&self) -> &[ModelReport] {
218        &self.reports
219    }
220
221    /// Model ids that an override or an exemption specifies and that are missing from the set, each once.
222    pub fn unknown_models(&self) -> &[String] {
223        &self.unknown_models
224    }
225
226    /// Returns whether every model passed and the settings specify no model missing from the set.
227    pub fn passed(&self) -> bool {
228        self.unknown_models.is_empty() && self.reports.iter().all(ModelReport::passed)
229    }
230
231    /// Asserts that every model passed and the settings specify no model missing from the set.
232    ///
233    /// # Panics
234    ///
235    /// Panics with a summary line, then every model's report and every model id missing from the set.
236    pub fn assert_passed(&self) {
237        assert!(self.passed(), "{self}");
238    }
239}
240
241/// Writes a summary line, every model id that the settings specify and that is missing from the set, then each
242/// model's report.
243impl fmt::Display for SetReport {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        let failed = self.reports.iter().filter(|report| !report.passed()).count();
246        let checked = models(self.reports.len());
247        if failed == 0 {
248            write!(f, "{checked} checked, and none failed.")?;
249        } else {
250            write!(f, "{checked} checked, and {failed} failed.")?;
251        }
252        if !self.unknown_models.is_empty() {
253            write!(
254                f,
255                " The settings name {} the set lacks.",
256                models(self.unknown_models.len())
257            )?;
258        }
259        writeln!(f)?;
260        for id in &self.unknown_models {
261            writeln!(f, "The settings name model '{id}', which the set lacks.")?;
262        }
263        for report in &self.reports {
264            write!(f, "{report}")?;
265        }
266        Ok(())
267    }
268}
269
270/// Returns `count` with the noun "model" in agreement, as in "1 model" or "3 models".
271fn models(count: usize) -> String {
272    if count == 1 {
273        "1 model".to_owned()
274    } else {
275        format!("{count} models")
276    }
277}