1use expectation_graph::semantics::perform_among;
11use expectation_graph::{Analysis, Precedence};
12use expectation_model::text;
13use expectation_model::{
14 Condition, Constraint, Diagnostic, ExpId, Fact, Keyword, SectionId, Span, StepText, StoryId, Suite, Trigger, VarId,
15};
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum Origin {
19 Story { story: StoryId, scenario: usize },
21 Coverage { target: ExpId },
23}
24
25#[derive(Clone, Debug, PartialEq)]
27pub enum Because {
28 Arranged,
30 Expectation(ExpId),
32 Story(Span),
34}
35
36#[derive(Clone, Debug)]
37pub struct Check {
38 pub var: VarId,
39 pub expect: Constraint,
40 pub because: Because,
41}
42
43#[derive(Clone, Debug)]
44pub struct PlanStep {
45 pub action: Trigger,
46 pub text: Vec<StepText>,
47 pub group: usize,
49 pub written: Vec<StepText>,
51 pub fired: Vec<ExpId>,
53 pub invariants: Vec<ExpId>,
55 pub checks: Vec<Check>,
56 pub state: Condition,
58}
59
60impl PlanStep {
61 pub fn exercises(&self) -> impl Iterator<Item = ExpId> + '_ {
62 self.fired.iter().chain(&self.invariants).copied()
63 }
64}
65
66#[derive(Clone, Debug)]
68pub struct Plan {
69 pub origin: Origin,
70 pub section: SectionId,
72 pub title: String,
73 pub slug: String,
74 pub start: Condition,
75 pub given: Vec<StepText>,
76 pub start_checks: Vec<Check>,
77 pub start_invariants: Vec<ExpId>,
79 pub steps: Vec<PlanStep>,
80}
81
82pub struct Planned {
83 pub plans: Vec<Plan>,
84 pub diagnostics: Vec<Diagnostic>,
85}
86
87struct Input<'a> {
89 action: Trigger,
90 checks: &'a [Fact],
91 text: Vec<StepText>,
92 span: Span,
93 group: usize,
94 written: Vec<StepText>,
95}
96
97pub fn plan_stories(suite: &Suite, analysis: &Analysis) -> Planned {
99 let mut plans = vec![];
100 let mut diagnostics = vec![];
101 for story in &suite.stories {
102 for (i, sc) in story.scenarios.iter().enumerate() {
103 let title = if story.scenarios.len() == 1 && sc.name == story.name {
104 story.name.clone()
105 } else {
106 format!("{} › {}", story.name, sc.name)
107 };
108 let slug = if title == story.name { story.slug.clone() } else { format!("{}--{}", story.slug, sc.slug) };
109 let section = suite.section(story.section);
110 let slug = prefixed(§ion.slug, &slug);
111 let inputs = sc.steps.iter().map(|s| Input {
112 action: s.action.clone(),
113 checks: &s.checks,
114 text: s.steps.clone(),
115 span: s.span.clone(),
116 group: s.group,
117 written: s.written.clone(),
118 });
119 let start = section.start.overlay(&sc.given);
120 let mut given: Vec<StepText> =
121 suite.chain(section.id).iter().flat_map(|s| s.setup_steps.iter().cloned()).collect();
122 given.extend(sc.given_steps.iter().cloned());
123 let origin = Origin::Story { story: story.id, scenario: i };
124 let head = Head { origin, section: section.id, title, slug, start, given };
125 plans.push(build(suite, &analysis.precedence, head, inputs, &mut diagnostics));
126 }
127 }
128 Planned { plans, diagnostics }
129}
130
131fn prefixed(section: &str, slug: &str) -> String {
133 if section.is_empty() { slug.to_string() } else { format!("{section}--{slug}") }
134}
135
136struct Head {
138 origin: Origin,
139 section: SectionId,
140 title: String,
141 slug: String,
142 start: Condition,
143 given: Vec<StepText>,
144}
145
146fn build<'a>(
147 suite: &Suite,
148 prec: &Precedence,
149 head: Head,
150 inputs: impl Iterator<Item = Input<'a>>,
151 diags: &mut Vec<Diagnostic>,
152) -> Plan {
153 let Head { origin, section, title, slug, start, given } = head;
154 let v = &suite.vocabulary;
155 let (mut state, start_invariants) = expectation_graph::semantics::settle(suite, prec, &start, &[]);
156 let mut start_checks: Vec<Check> =
157 start.iter().map(|(var, c)| Check { var, expect: *c, because: Because::Arranged }).collect();
158 for &inv in &start_invariants {
159 promise(&mut start_checks, suite, inv, &start);
160 }
161
162 let scope = expectation_graph::flow::in_scope(suite, section);
164 let mut steps = vec![];
165 for input in inputs {
166 let errors = diags.iter().filter(|d| d.is_error()).count();
167 let candidates = suite.transitions().filter(|e| scope[e.id.0 as usize]);
168 let out = perform_among(suite, prec, &state, &input.action, candidates);
169 if let Some(&(a, b, var)) = out.clashes.first() {
170 let (a, b) = (suite.exp(a), suite.exp(b));
171 diags.push(
172 Diagnostic::error(
173 "clash",
174 format!("«{}» and «{}» both apply here and disagree about {}", a.name, b.name, v.var(var).subject),
175 )
176 .at(&input.span, "")
177 .help("settle the conflict between them first"),
178 );
179 break;
180 }
181 for &u in &out.uncertain {
182 let e = suite.exp(u);
183 let mut d = Diagnostic::error("unclear-step", format!("it isn't clear whether «{}» applies here", e.name));
184 let given = expectation_graph::semantics::seen_by(&state, &input.action, &e.trigger).unproven(&e.given);
185 if !given.is_unconstrained() {
186 d = d.at(&input.span, format!("nothing establishes that {}", v.describe(&given)));
187 }
188 let trig = input.action.changes.unproven(&e.trigger.changes);
189 if !trig.is_unconstrained() {
190 d = d.at(&input.span, format!("the step doesn't say whether {}", v.describe(&trig)));
191 }
192 diags.push(
193 d.at(&e.span, "").help("pin it down: arrange it with a `Given`, or give the When an exact value"),
194 );
195 }
196 if let (true, true, Some(e)) = (out.fired.is_empty(), out.uncertain.is_empty(), input.action.event) {
197 let event = v.event(e);
198 let line = input.text.iter().find(|t| text::key(&t.text) == event.key).map_or(&input.span, |t| &t.span);
199 diags.push(
200 Diagnostic::error("unspecified", format!("nothing is expected to happen when {}", event.text))
201 .at(line, format!("at this point {}", v.describe(&state)))
202 .help("no expectation covers this; write one, or the story relies on unspecified behaviour"),
203 );
204 }
205
206 let mut checks = vec![];
207 let before = state.overlay(&input.action.changes);
208 for &f in out.fired.iter().chain(&out.invariants) {
209 promise(&mut checks, suite, f, &before);
210 }
211 let mut after = out.after;
212 for fact in input.checks {
213 let mut want = Condition::new();
214 want.constrain(v.var(fact.var), fact.constraint);
215 let stated = v.var(fact.var).describe(&fact.constraint);
216 if !after.overlaps(&want) {
217 let said = v.describe(&after.restrict([fact.var]));
218 let mut d = Diagnostic::error(
219 "contradicts",
220 format!("the story expects {stated}, but the expectations say {said}"),
221 )
222 .at(&fact.span, "");
223 if let Some(&by) = out.fired.iter().find(|&&f| suite.exp(f).then.get(fact.var).is_some()) {
224 d = d.at(&suite.exp(by).span, format!("«{}» says so", suite.exp(by).name));
225 }
226 diags.push(d);
227 continue;
228 }
229 if !after.implies(&want) {
230 diags.push(
231 Diagnostic::warning("unpromised", format!("no expectation promises that {stated}"))
232 .at(&fact.span, "")
233 .help("it's checked anyway; write an expectation so it's specified"),
234 );
235 }
236 after = after.and(&want);
237 if !checks.iter().any(|c: &Check| c.var == fact.var && c.expect == fact.constraint) {
238 checks.push(Check {
239 var: fact.var,
240 expect: fact.constraint,
241 because: Because::Story(fact.span.clone()),
242 });
243 }
244 }
245 steps.push(PlanStep {
246 action: input.action,
247 text: input.text,
248 group: input.group,
249 written: input.written,
250 fired: out.fired,
251 invariants: out.invariants,
252 checks,
253 state: after.clone(),
254 });
255 state = after;
256 if diags.iter().filter(|d| d.is_error()).count() > errors {
258 break;
259 }
260 }
261 Plan { origin, section, title, slug, start, given, start_checks, start_invariants, steps }
262}
263
264fn promise(checks: &mut Vec<Check>, suite: &Suite, e: ExpId, before: &Condition) {
266 for (var, c) in suite.exp(e).effect(&suite.vocabulary, before).iter() {
267 if !checks.iter().any(|k| k.var == var && k.expect == *c) {
268 checks.push(Check { var, expect: *c, because: Because::Expectation(e) });
269 }
270 }
271}
272
273pub fn coverage(suite: &Suite, plans: &[Plan]) -> Vec<Vec<(usize, Option<usize>)>> {
275 let mut out = vec![vec![]; suite.expectations.len()];
276 for (p, plan) in plans.iter().enumerate() {
277 for &inv in &plan.start_invariants {
278 out[inv.0 as usize].push((p, None));
279 }
280 for (s, step) in plan.steps.iter().enumerate() {
281 for e in step.exercises() {
282 out[e.0 as usize].push((p, Some(s)));
283 }
284 }
285 }
286 out
287}
288
289pub fn untested(suite: &Suite, analysis: &Analysis, plans: &[Plan]) -> Vec<Diagnostic> {
291 let cov = coverage(suite, plans);
292 let mut diags = vec![];
293 for e in &suite.expectations {
294 if !cov[e.id.0 as usize].is_empty() {
295 continue;
296 }
297 let Some(route) = &analysis.flow.routes[e.id.0 as usize] else { continue };
298 let mut lines: Vec<String> = route
299 .start
300 .iter()
301 .filter(|(v, c)| suite.background.get(*v) != Some(c))
302 .map(|(v, c)| format!("Given {}", suite.vocabulary.var(v).describe(c)))
303 .collect();
304 lines.extend(route.steps.iter().map(|s| format!("When {}", suite.describe_trigger(&suite.exp(*s).trigger))));
305 diags.push(Diagnostic::warning("untested", format!("no story exercises «{}»", e.name)).at(&e.span, "").help(
306 if lines.is_empty() {
307 "it holds from the start; any story checks it".into()
308 } else {
309 format!("a story that would: {}", lines.join(" / "))
310 },
311 ));
312 }
313 diags
314}
315
316pub fn coverage_plans(suite: &Suite, analysis: &Analysis, plans: &[Plan]) -> Vec<Plan> {
319 let cov = coverage(suite, plans);
320 let mut covered: Vec<bool> = cov.iter().map(|c| !c.is_empty()).collect();
321 let mut out = vec![];
322 let mut flows: Vec<Option<expectation_graph::Flow>> = suite.sections.iter().map(|_| None).collect();
324 for e in &suite.expectations {
325 if covered[e.id.0 as usize] {
326 continue;
327 }
328 let section = suite.section_of(e.id);
329 let flow = flows[section.id.0 as usize].get_or_insert_with(|| {
330 let roots = [(section.start.clone(), section.id)];
331 expectation_graph::flow::explore(suite, &analysis.precedence, &roots, expectation_graph::Limits::default())
332 });
333 let Some(route) = flow.routes[e.id.0 as usize].as_ref().or(analysis.flow.routes[e.id.0 as usize].as_ref())
334 else {
335 continue;
336 };
337 if route.steps.is_empty() {
338 continue;
339 }
340 let inputs = route.steps.iter().enumerate().map(|(n, &s)| {
341 let x = suite.exp(s);
342 let text: Vec<StepText> = x
343 .when_steps()
344 .cloned()
345 .enumerate()
346 .map(|(i, mut t)| {
347 t.keyword = if i == 0 { Keyword::When } else { Keyword::And };
348 t
349 })
350 .collect();
351 Input {
352 action: x.trigger.clone(),
353 checks: &[],
354 written: text.clone(),
355 text,
356 span: x.span.clone(),
357 group: n + 1,
358 }
359 });
360 let mut given: Vec<StepText> =
361 suite.chain(section.id).iter().flat_map(|s| s.setup_steps.iter().cloned()).collect();
362 for (var, c) in route.start.iter().filter(|(v, c)| section.start.get(*v) != Some(c)) {
363 let span = e.span.clone();
364 let (text, meaning) = (suite.vocabulary.var(var).describe(c), Some(expectation_model::Said::Fact(var, *c)));
365 given.push(StepText { keyword: Keyword::Given, text, span, meaning });
366 }
367 let mut diags = vec![];
368 let title = format!("Reaching «{}»", e.name);
369 let head = Head {
370 origin: Origin::Coverage { target: e.id },
371 section: section.id,
372 title,
373 slug: prefixed(§ion.slug, &format!("coverage--{}", text::slug(&e.name))),
374 start: route.start.clone(),
375 given,
376 };
377 let plan = build(suite, &analysis.precedence, head, inputs, &mut diags);
378 if diags.iter().any(|d| d.is_error()) {
379 continue;
380 }
381 for step in &plan.steps {
382 for x in step.exercises() {
383 covered[x.0 as usize] = true;
384 }
385 }
386 out.push(plan);
387 }
388 out
389}