1use std::collections::BTreeMap;
11
12use bookspec_checker::{Claims, Engine, Example, Expect, Query, Rule, RuleId, Values, rules};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Outcome {
17 Pass,
20 Fail,
23 NotClaimed,
25}
26
27impl Outcome {
28 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#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct ExampleReport {
41 pub id: String,
43 pub failure: Option<String>,
45}
46
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
49pub struct Report {
50 pub rules: BTreeMap<RuleId, Outcome>,
52 pub examples: Vec<ExampleReport>,
54 pub unclaimed: Vec<String>,
57}
58
59impl Report {
60 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 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
89pub 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
148pub 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
191fn 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
224fn 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}