Skip to main content

bookspec_runner/
lib.rs

1//! bookspec's conformance runner: it drives any [`Engine`] through the
2//! examples and reports, per rule, pass, fail or not claimed
3//! (`plan/served.md` 8.2 P13 step 2).
4//!
5//! The runner is the only judge. An engine answers; the runner compares the
6//! answer with the example's expected values, and with the refusal the
7//! example names when the engine refuses. The engine is never asked
8//! whether its own answer is right.
9
10use std::collections::BTreeMap;
11
12use bookspec_checker::{Claims, Engine, Example, Expect, Query, Rule, RuleId, Values, rules};
13
14/// What the runner says about one rule.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Outcome {
17    /// The engine claims the rule and every example that demonstrates it
18    /// passed.
19    Pass,
20    /// The engine claims the rule and an example failed, or a claim has no
21    /// example behind it.
22    Fail,
23    /// The engine does not claim the rule.
24    NotClaimed,
25}
26
27impl Outcome {
28    /// The outcome as the words a report prints.
29    pub fn key(self) -> &'static str {
30        match self {
31            Outcome::Pass => "pass",
32            Outcome::Fail => "fail",
33            Outcome::NotClaimed => "not claimed",
34        }
35    }
36}
37
38/// One example's outcome.
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct ExampleReport {
41    /// The example's id.
42    pub id: String,
43    /// Why it failed, if it did.
44    pub failure: Option<String>,
45}
46
47/// The runner's report: what it found of each rule, and each example.
48#[derive(Clone, Debug, Default, PartialEq, Eq)]
49pub struct Report {
50    /// Every rule of the catalogue the runner was given, and its outcome.
51    pub rules: BTreeMap<RuleId, Outcome>,
52    /// Every example, in the order run.
53    pub examples: Vec<ExampleReport>,
54    /// Failures that belong to no rule: an example no rule lists, a rule
55    /// claimed but unknown to the catalogue.
56    pub unclaimed: Vec<String>,
57}
58
59impl Report {
60    /// True when every claimed rule passed and nothing else failed.
61    pub fn passed(&self) -> bool {
62        self.unclaimed.is_empty()
63            && self.rules.values().all(|o| *o != Outcome::Fail)
64            && self.examples.iter().all(|e| e.failure.is_none())
65    }
66
67    /// Every failure, with its rule's outcome where it has one.
68    pub fn failures(&self) -> Vec<String> {
69        let mut out = self.unclaimed.clone();
70        for (rule, outcome) in &self.rules {
71            if *outcome == Outcome::Fail {
72                let ids: Vec<&str> = rules()
73                    .iter()
74                    .find(|r| &r.id == rule)
75                    .map(|r| r.examples.to_vec())
76                    .unwrap_or_default();
77                out.push(format!("{rule} failed (examples: {})", ids.join(", ")));
78            }
79        }
80        for e in &self.examples {
81            if let Some(f) = &e.failure {
82                out.push(f.clone());
83            }
84        }
85        out
86    }
87}
88
89/// Run `examples` against engines made by `make`, one fresh engine per
90/// example, and report per rule against `catalog` (normally
91/// [`bookspec_checker::rules()`]).
92pub fn run<'a, E>(
93    examples: &'a [Example],
94    catalog: &[Rule],
95    mut make: impl FnMut(&'a Example) -> E,
96) -> Report
97where
98    E: Engine + 'a,
99{
100    let mut out = Report::default();
101    let mut claims = Claims::default();
102    for ex in examples {
103        let mut engine = make(ex);
104        let declared = engine.claims();
105        claims.rules.extend(declared.rules);
106        for (rule, choice) in declared.choices {
107            claims.choices.entry(rule).or_insert(choice);
108        }
109        let failure = run_example(ex, &mut engine).err();
110        out.examples.push(ExampleReport {
111            id: ex.id.clone(),
112            failure,
113        });
114    }
115    for rule in catalog {
116        let outcome = if !claims.claims(&rule.id) {
117            Outcome::NotClaimed
118        } else if !rule.has_examples() {
119            out.unclaimed
120                .push(format!("{} is claimed but has no example", rule.id));
121            Outcome::Fail
122        } else {
123            let failed = rule.examples.iter().any(|id| {
124                out.examples
125                    .iter()
126                    .find(|e| &e.id == id)
127                    .is_none_or(|e| e.failure.is_some())
128            });
129            if failed { Outcome::Fail } else { Outcome::Pass }
130        };
131        out.rules.insert(rule.id.clone(), outcome);
132    }
133    for rule in &claims.rules {
134        if !catalog.iter().any(|r| &r.id == rule) {
135            out.unclaimed
136                .push(format!("{rule} is claimed but is not in the catalogue"));
137        }
138    }
139    for ex in examples {
140        if !catalog.iter().any(|r| r.examples.contains(&ex.id.as_str())) {
141            out.unclaimed
142                .push(format!("example {} is listed under no rule", ex.id));
143        }
144    }
145    out
146}
147
148/// Run one example against one engine: load the setup, apply every event
149/// (accepted or refused as the example says), check the expectations named
150/// after each event, make the changes named after it, then check the rest.
151pub fn run_example(ex: &Example, engine: &mut dyn Engine) -> Result<(), String> {
152    engine
153        .load(&ex.setup)
154        .map_err(|r| format!("load refused: {} ({})", r.reason, r.rule))?;
155    for (i, event) in ex.events.iter().enumerate() {
156        let n = i + 1;
157        match (engine.apply(event), &event.refused) {
158            (Ok(()), None) => {}
159            (Err(r), Some(want)) if &r.reason == want => {}
160            (Err(r), Some(want)) => {
161                return Err(format!(
162                    "event {n}: refused {:?}, expected {want:?} ({})",
163                    r.reason, r.rule
164                ));
165            }
166            (Err(r), None) => {
167                return Err(format!(
168                    "event {n}: refused {:?}, expected accepted ({})",
169                    r.reason, r.rule
170                ));
171            }
172            (Ok(()), Some(want)) => {
173                return Err(format!("event {n}: accepted, expected refused: {want}"));
174            }
175        }
176        for x in ex.expect.iter().filter(|x| x.after == Some(n)) {
177            check(engine, x).map_err(|m| format!("after event {n}: {m}"))?;
178        }
179        for c in ex.changes.iter().filter(|c| c.after == n) {
180            engine
181                .change(c)
182                .map_err(|r| format!("change after {n}: {} ({})", r.reason, r.rule))?;
183        }
184    }
185    for x in ex.expect.iter().filter(|x| x.after.is_none()) {
186        check(engine, x)?;
187    }
188    Ok(())
189}
190
191/// Check one expectation against the engine's answer.
192fn check(engine: &dyn Engine, x: &Expect) -> Result<(), String> {
193    let q = x.query();
194    let answer = engine.query(&q);
195    match (answer, &x.refused, x.accepted) {
196        (Ok(v), None, Some(true)) => {
197            if v == serde_json::Value::Bool(true) {
198                Ok(())
199            } else {
200                Err(format!(
201                    "{}: accepted, but answered {v} (a command shows no values)",
202                    q.name
203                ))
204            }
205        }
206        (Ok(v), None, None) => compare(x, &v),
207        (Ok(v), Some(want), _) => Err(format!(
208            "{}: answered {v}, expected refused: {want}",
209            q.name
210        )),
211        (Err(r), Some(want), _) if &r.reason == want => Ok(()),
212        (Err(r), Some(want), _) => Err(format!(
213            "{}: refused {:?}, expected {want:?} ({})",
214            q.name, r.reason, r.rule
215        )),
216        (Err(r), None, _) => Err(format!("{}: refused {:?} ({})", q.name, r.reason, r.rule)),
217        (Ok(v), None, Some(false)) => Err(format!(
218            "{}: accepted = false is not the examples' vocabulary; answered {v}",
219            q.name
220        )),
221    }
222}
223
224/// Compare an answer with what the example expects.
225fn compare(x: &Expect, got: &serde_json::Value) -> Result<(), String> {
226    let q: Query = x.query();
227    let Some(want) = &x.values else {
228        return Err(format!("{}: expects values, none given", q.name));
229    };
230    match want {
231        Values::Figures(figures) => {
232            let serde_json::Value::Object(map) = got else {
233                return Err(format!("{}: answered {got}, expected a table", q.name));
234            };
235            for (key, want) in figures {
236                match map.get(key) {
237                    Some(serde_json::Value::String(got)) if got == want => {}
238                    Some(got) => {
239                        return Err(format!("{}: {key}: got {got}, expected {want:?}", q.name));
240                    }
241                    None => {
242                        return Err(format!("{}: {key} is missing (expected {want:?})", q.name));
243                    }
244                }
245            }
246            if q.is_complete() {
247                let extra: Vec<&String> =
248                    map.keys().filter(|k| !figures.contains_key(*k)).collect();
249                let missing: Vec<&String> =
250                    figures.keys().filter(|k| !map.contains_key(*k)).collect();
251                if !extra.is_empty() || !missing.is_empty() {
252                    return Err(format!(
253                        "{}: lists exactly what it covers; missing {missing:?}, extra {extra:?}",
254                        q.name
255                    ));
256                }
257            }
258            Ok(())
259        }
260        Values::Lines(lines) => {
261            let serde_json::Value::Array(got) = got else {
262                return Err(format!("{}: answered {got}, expected a list", q.name));
263            };
264            let got: Vec<&str> = got
265                .iter()
266                .map(|v| v.as_str().unwrap_or("<not text>"))
267                .collect();
268            let want: Vec<&str> = lines.iter().map(String::as_str).collect();
269            if got == want {
270                Ok(())
271            } else {
272                Err(format!("{}: got {got:?}, expected {want:?}", q.name))
273            }
274        }
275        Values::Rows(rows) => {
276            let serde_json::Value::Array(got) = got else {
277                return Err(format!("{}: answered {got}, expected rows", q.name));
278            };
279            let got: Vec<Vec<&str>> = got
280                .iter()
281                .map(|row| {
282                    row.as_array()
283                        .map(|r| {
284                            r.iter()
285                                .map(|v| v.as_str().unwrap_or("<not text>"))
286                                .collect()
287                        })
288                        .unwrap_or_default()
289                })
290                .collect();
291            let want: Vec<Vec<&str>> = rows
292                .iter()
293                .map(|r| r.iter().map(String::as_str).collect())
294                .collect();
295            if got == want {
296                Ok(())
297            } else {
298                Err(format!("{}: got {got:?}, expected {want:?}", q.name))
299            }
300        }
301    }
302}