1use super::context::Ctx;
14use super::observe::StepState;
15use super::steps::StepSpec;
16
17#[derive(Debug, Clone)]
26pub enum Stance {
27 Applies,
29 NotApplicable(String),
32 Excluded(String),
34 Redundant {
36 reason: String,
38 inapplicable: String,
40 },
41}
42
43impl Stance {
44 #[must_use]
46 pub const fn word(&self) -> &'static str {
47 match self {
48 Self::Applies => "applicable",
49 Self::NotApplicable(_) => "not-applicable",
50 Self::Excluded(_) => "excluded",
51 Self::Redundant { .. } => "redundant",
52 }
53 }
54
55 #[must_use]
57 pub const fn acts(&self) -> bool {
58 matches!(self, Self::Applies)
59 }
60
61 #[must_use]
63 pub fn detail(&self) -> String {
64 match self {
65 Self::Applies => String::new(),
66 Self::NotApplicable(reason) | Self::Excluded(reason) => reason.clone(),
67 Self::Redundant {
68 reason,
69 inapplicable,
70 } => format!("{inapplicable}; the stated reason was {reason}"),
71 }
72 }
73
74 #[must_use]
77 pub fn framed(&self) -> String {
78 match self {
79 Self::Applies => String::new(),
80 Self::NotApplicable(reason) => format!("not applicable: {reason}"),
81 Self::Excluded(reason) => {
82 format!("excluded by {}: {reason}", crate::config::CONFIG_PATH)
83 }
84 Self::Redundant { .. } => format!(
85 "{} excludes a step that does not apply here: {}",
86 crate::config::CONFIG_PATH,
87 self.detail()
88 ),
89 }
90 }
91}
92
93#[must_use]
95pub fn stance(ctx: &Ctx, step: &StepSpec) -> Stance {
96 let inapplicable = (step.applies)(ctx);
97 match (ctx.excluded(step.name).map(str::to_owned), inapplicable) {
98 (Some(reason), Some(inapplicable)) => Stance::Redundant {
99 reason,
100 inapplicable,
101 },
102 (Some(reason), None) => Stance::Excluded(reason),
103 (None, Some(reason)) => Stance::NotApplicable(reason),
104 (None, None) => Stance::Applies,
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum Observed {
112 Satisfied {
115 limitation: Option<String>,
117 },
118 Skipped,
121 Unsatisfied,
123 Unknown,
125}
126
127impl Observed {
128 #[must_use]
130 pub fn of(state: &StepState) -> Self {
131 match state {
132 StepState::Satisfied { limitation, .. } => Self::Satisfied {
133 limitation: limitation.clone(),
134 },
135 StepState::Inapplicable { .. } => Self::Skipped,
136 StepState::Unsatisfied { .. } => Self::Unsatisfied,
137 StepState::Unknown { .. } => Self::Unknown,
138 }
139 }
140
141 #[must_use]
143 pub const fn label(&self) -> &'static str {
144 match self {
145 Self::Satisfied { .. } => "ok",
146 Self::Skipped => "skipped",
147 Self::Unsatisfied => "unsatisfied",
148 Self::Unknown => "unknown",
149 }
150 }
151
152 #[must_use]
154 pub const fn wire(&self) -> &'static str {
155 match self {
156 Self::Satisfied {
157 limitation: Some(_),
158 } => "satisfied-with-limitation",
159 Self::Satisfied { limitation: None } => "satisfied",
160 Self::Skipped => "skipped",
161 Self::Unsatisfied => "unsatisfied",
162 Self::Unknown => "unknown",
163 }
164 }
165}
166
167#[derive(Debug, Clone)]
169pub struct Row {
170 pub name: &'static str,
172 pub stance: Stance,
174 pub observed: Option<Observed>,
177 pub detail: String,
180}
181
182impl Row {
183 #[must_use]
185 pub const fn stated(step: &StepSpec, stance: Stance) -> Self {
186 Self {
187 name: step.name,
188 stance,
189 observed: None,
190 detail: String::new(),
191 }
192 }
193
194 #[must_use]
196 pub fn observed(step: &StepSpec, stance: Stance, state: &StepState) -> Self {
197 Self {
198 name: step.name,
199 stance,
200 observed: Some(Observed::of(state)),
201 detail: state.detail().to_owned(),
202 }
203 }
204
205 #[must_use]
207 pub fn line(&self) -> String {
208 let Some(observed) = &self.observed else {
209 return format!(
210 "{} {} — {}",
211 self.stance.word(),
212 self.name,
213 self.stance.detail()
214 );
215 };
216 let mut line = format!("{} {} — {}", observed.label(), self.name, self.detail);
217 if let Observed::Satisfied {
218 limitation: Some(limit),
219 } = observed
220 {
221 use std::fmt::Write as _;
222 let _ = write!(line, " (limitation: {limit})");
223 }
224 line
225 }
226
227 #[must_use]
229 pub fn event_fields(&self) -> (String, String) {
230 match &self.observed {
231 None => (self.stance.word().to_owned(), self.stance.detail()),
232 Some(Observed::Satisfied { .. }) => ("satisfied".to_owned(), self.detail.clone()),
233 Some(observed) => (observed.wire().to_owned(), self.detail.clone()),
234 }
235 }
236}
237
238#[derive(Debug, Clone, Default)]
240pub struct Report {
241 pub rows: Vec<Row>,
243}
244
245impl Report {
246 fn count(&self, wanted: &Observed) -> usize {
247 self.rows
248 .iter()
249 .filter(|row| row.observed.as_ref() == Some(wanted))
250 .count()
251 }
252
253 #[must_use]
255 pub fn unsatisfied(&self) -> usize {
256 self.count(&Observed::Unsatisfied)
257 }
258
259 #[must_use]
261 pub fn unknown(&self) -> usize {
262 self.count(&Observed::Unknown)
263 }
264
265 #[must_use]
267 pub fn judged(&self) -> usize {
268 self.rows.iter().filter(|row| row.stance.acts()).count()
269 }
270
271 #[must_use]
277 pub fn checkpointable(&self) -> bool {
278 self.unsatisfied() == 0 && self.unknown() == 0
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use crate::setup::steps::STEPS;
286
287 fn row(observed: Option<Observed>) -> Row {
288 let stance = if observed.is_some() {
289 Stance::Applies
290 } else {
291 Stance::Excluded("the target runs it elsewhere".into())
292 };
293 Row {
294 name: STEPS[0].name,
295 stance,
296 observed,
297 detail: String::new(),
298 }
299 }
300
301 #[test]
304 fn only_a_complete_observation_is_checkpointable() {
305 let accepted = Report {
306 rows: vec![
307 row(Some(Observed::Satisfied { limitation: None })),
308 row(Some(Observed::Satisfied {
309 limitation: Some("weaker".into()),
310 })),
311 row(Some(Observed::Skipped)),
312 row(None),
313 ],
314 };
315 assert!(accepted.checkpointable());
316 for refused in [Observed::Unknown, Observed::Unsatisfied] {
317 let mut report = accepted.clone();
318 report.rows.push(row(Some(refused.clone())));
319 assert!(!report.checkpointable(), "{refused:?} was accepted");
320 }
321 }
322
323 #[test]
326 fn an_observation_keeps_its_distinctions() {
327 let limited = StepState::Satisfied {
328 detail: "found".into(),
329 limitation: Some("weaker".into()),
330 };
331 assert_eq!(Observed::of(&limited).wire(), "satisfied-with-limitation");
332 let plain = StepState::Satisfied {
333 detail: "found".into(),
334 limitation: None,
335 };
336 assert_eq!(Observed::of(&plain).wire(), "satisfied");
337 let skipped = StepState::Inapplicable {
338 detail: "no line".into(),
339 };
340 assert_eq!(Observed::of(&skipped).wire(), "skipped");
341 let unknown = StepState::Unknown {
342 detail: "unreadable".into(),
343 };
344 assert_eq!(Observed::of(&unknown).wire(), "unknown");
345 let not = StepState::Unsatisfied {
346 detail: "absent".into(),
347 };
348 assert_eq!(Observed::of(¬).wire(), "unsatisfied");
349 }
350}