Skip to main content

expectation_model/
spec.rs

1//! Expectations, features and stories: what a suite says, after its text has been resolved
2//! against the vocabulary.
3
4use crate::condition::{Condition, Constraint, Interval};
5use crate::diagnostic::{Sources, Span};
6use crate::vocabulary::{EventId, VarId, Variable, Vocabulary};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub struct ExpId(pub u32);
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct FeatureId(pub u32);
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct StoryId(pub u32);
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct SectionId(pub u32);
19
20/// A Then that's relative to what was there before: "the drawing goes up by 1 item", "the
21/// selected item stays the same".
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub enum Shift {
24    By(f64),
25    Same,
26}
27
28impl Shift {
29    /// What `var` can be afterwards, given what it could be before.
30    pub fn apply(self, var: &Variable, before: Option<&Constraint>) -> Constraint {
31        let before = before.copied().unwrap_or_else(|| var.full());
32        match (self, before, var.full()) {
33            (Shift::By(d), Constraint::Range(r), Constraint::Range(full)) => {
34                Constraint::Range(Interval { lo: r.lo + d, hi: r.hi + d, ..r }.intersect(&full))
35            }
36            _ => before,
37        }
38    }
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum Keyword {
43    Given,
44    When,
45    Then,
46    And,
47    But,
48}
49
50impl Keyword {
51    pub fn as_str(self) -> &'static str {
52        match self {
53            Keyword::Given => "Given",
54            Keyword::When => "When",
55            Keyword::Then => "Then",
56            Keyword::And => "And",
57            Keyword::But => "But",
58        }
59    }
60}
61
62/// A step as written, kept for docs and generated tests, with what it turned out to mean
63/// (none if it didn't resolve) so docs can say it in the reader's words.
64#[derive(Clone, Debug)]
65pub struct StepText {
66    pub keyword: Keyword,
67    pub text: String,
68    pub span: Span,
69    pub meaning: Option<Said>,
70}
71
72/// What a step says, once resolved.
73#[derive(Clone, Copy, Debug)]
74pub enum Said {
75    Fact(VarId, Constraint),
76    Event(EventId),
77    Phrase(PhraseId),
78    Shift(VarId, Shift),
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
82pub struct PhraseId(pub u32);
83
84/// A step of your own, built from others: a higher-level way to say a setup or a sequence of
85/// actions, like "the user draws a triangle" for PEN, three clicks on the canvas and DONE.
86/// Specs read better and stay put when the details change; docs say it with `say`.
87#[derive(Clone, Debug)]
88pub struct Phrase {
89    pub id: PhraseId,
90    pub name: String,
91    pub key: String,
92    /// How docs say it: an instruction for actions ("press NEW, draw three shapes on the
93    /// canvas, then press SAVE"), a clause for a setup ("a drawing saved").
94    pub say: Option<String>,
95    pub span: Span,
96    pub body: PhraseBody,
97}
98
99#[derive(Clone, Debug)]
100pub enum PhraseBody {
101    /// Facts, for Givens (and Thens).
102    Setup(Vec<Fact>),
103    /// Actions in order, for Whens: one for an expectation, any number in a story.
104    Actions(Vec<Trigger>),
105}
106
107/// One resolved step: "the dial is above 55 %".
108#[derive(Clone, Debug)]
109pub struct Fact {
110    pub var: VarId,
111    pub constraint: Constraint,
112    pub span: Span,
113}
114
115/// What sets an expectation off: an event, a change the outside world makes to some states,
116/// or both at once.
117#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
118pub struct Trigger {
119    pub event: Option<EventId>,
120    pub changes: Condition,
121}
122
123impl Trigger {
124    /// No event and no change: the expectation is an invariant that holds whenever its Given does.
125    pub fn is_none(&self) -> bool {
126        self.event.is_none() && self.changes.is_unconstrained()
127    }
128
129    /// Could a single action set off both?
130    pub fn compatible(&self, o: &Trigger) -> bool {
131        self.event == o.event && self.changes.overlaps(&o.changes)
132    }
133
134    /// Does performing this action certainly set off an expectation triggered by `o`?
135    pub fn fires(&self, o: &Trigger) -> bool {
136        !o.is_none() && self.event == o.event && self.changes.implies(&o.changes)
137    }
138
139    /// Might performing this action set off `o`, depending on which value it picks? Only if
140    /// it changes every state `o` is set off by.
141    pub fn may_fire(&self, o: &Trigger) -> bool {
142        !o.is_none() && self.compatible(o) && o.changes.vars().all(|v| self.changes.get(v).is_some())
143    }
144}
145
146/// Given some state, when something happens, then the system ends up in some state.
147#[derive(Clone, Debug)]
148pub struct Expectation {
149    pub id: ExpId,
150    pub feature: FeatureId,
151    pub name: String,
152    pub slug: String,
153    pub description: Vec<String>,
154    pub span: Span,
155    pub given: Condition,
156    pub trigger: Trigger,
157    pub then: Condition,
158    /// Relative Thens, applied to the state before.
159    pub shifts: Vec<(VarId, Shift)>,
160    /// Expectations this one is an exception to: where both apply, this one wins.
161    pub overrides: Vec<ExpId>,
162    pub steps: Vec<StepText>,
163    /// `Docs: hidden`: tested, but left out of the reading pages (it's only in the test report).
164    pub hidden: bool,
165}
166
167impl Expectation {
168    pub fn is_invariant(&self) -> bool {
169        self.trigger.is_none()
170    }
171
172    /// What its Then says the states it mentions become, from the state `before`.
173    pub fn effect(&self, vocab: &Vocabulary, before: &Condition) -> Condition {
174        let mut out = self.then.clone();
175        for &(v, shift) in &self.shifts {
176            let var = vocab.var(v);
177            out.set(var, shift.apply(var, before.get(v)));
178        }
179        out
180    }
181
182    /// The most general state it leaves the system in: its Given, with the trigger's changes
183    /// and then its Then written over the top. For an invariant, its Given and Then together.
184    pub fn outcome(&self, vocab: &Vocabulary) -> Condition {
185        if self.is_invariant() {
186            self.given.and(&self.then)
187        } else {
188            let before = self.given.overlay(&self.trigger.changes);
189            before.overlay(&self.effect(vocab, &before))
190        }
191    }
192
193    /// The states its Then mentions.
194    pub fn sets(&self) -> impl Iterator<Item = VarId> + '_ {
195        self.then.vars().chain(self.shifts.iter().map(|(v, _)| *v))
196    }
197
198    pub fn when_steps(&self) -> impl Iterator<Item = &StepText> {
199        let mut section = Keyword::Given;
200        self.steps.iter().filter(move |s| {
201            if !matches!(s.keyword, Keyword::And | Keyword::But) {
202                section = s.keyword;
203            }
204            section == Keyword::When
205        })
206    }
207}
208
209/// A group of features and stories that share a setup, nested like a book's chapters: one per
210/// directory of the suite. Everything in a section starts from its `start`.
211#[derive(Clone, Debug)]
212pub struct Section {
213    pub id: SectionId,
214    pub parent: Option<SectionId>,
215    pub name: String,
216    /// Unique in the suite: its ancestors' slugs and its own, joined by "--". Empty for the root.
217    pub slug: String,
218    /// Its directory, relative to the suite's. Empty for the root.
219    pub path: String,
220    pub description: Vec<String>,
221    /// What its Background adds to its parent's.
222    pub setup: Condition,
223    pub setup_steps: Vec<StepText>,
224    /// The state everything in it starts from: its parent's start with `setup` written over it.
225    pub start: Condition,
226    pub span: Option<Span>,
227    pub children: Vec<SectionId>,
228    pub features: Vec<FeatureId>,
229    pub stories: Vec<StoryId>,
230}
231
232#[derive(Clone, Debug)]
233pub struct Feature {
234    pub id: FeatureId,
235    pub section: SectionId,
236    pub name: String,
237    pub slug: String,
238    pub description: Vec<String>,
239    pub span: Span,
240    pub expectations: Vec<ExpId>,
241}
242
243/// "As a tea drinker, I want …, so that …", told through one or more scenarios.
244#[derive(Clone, Debug)]
245pub struct Story {
246    pub id: StoryId,
247    pub section: SectionId,
248    pub name: String,
249    pub slug: String,
250    /// The user story: its "As a …", "I want …" and "So that …" lines.
251    pub narrative: Vec<String>,
252    /// The rest of the story's text: its introduction in the docs.
253    pub description: Vec<String>,
254    pub span: Span,
255    pub scenarios: Vec<Scenario>,
256}
257
258#[derive(Clone, Debug)]
259pub struct Scenario {
260    pub name: String,
261    pub slug: String,
262    pub description: Vec<String>,
263    pub span: Span,
264    /// Arranged on top of the Background before the first step.
265    pub given: Condition,
266    pub given_steps: Vec<StepText>,
267    pub steps: Vec<StoryStep>,
268}
269
270/// One "When …" of a story and the "Then …"s the story checks after it.
271#[derive(Clone, Debug)]
272pub struct StoryStep {
273    pub action: Trigger,
274    pub checks: Vec<Fact>,
275    pub steps: Vec<StepText>,
276    pub span: Span,
277    /// Which written step it came from: a phrase of several actions makes several steps of
278    /// one group, which docs show as one step.
279    pub group: usize,
280    /// The group's lines as written (When, And, Then), on its first step only. `steps` has
281    /// what each step does, phrases spelt out, for the test report.
282    pub written: Vec<StepText>,
283}
284
285/// A whole suite: the vocabulary, its sections (the first is the root), and the expectations
286/// and stories written against them.
287#[derive(Clone, Debug, Default)]
288pub struct Suite {
289    pub name: String,
290    pub vocabulary: Vocabulary,
291    /// The root section's start, which every section builds on.
292    pub background: Condition,
293    pub background_steps: Vec<StepText>,
294    pub sections: Vec<Section>,
295    pub features: Vec<Feature>,
296    pub expectations: Vec<Expectation>,
297    pub stories: Vec<Story>,
298    /// `Term:`s: the glossary's definitions.
299    pub glossary: Vec<Term>,
300    pub sources: Sources,
301    /// `Platform:`s the suite is run on; the first is the default, which untagged lines are about.
302    pub platforms: Vec<String>,
303    /// The platform this suite was read for (a lens on one platform's variants), if the
304    /// suite declares platforms.
305    pub platform: Option<String>,
306}
307
308/// `Term: <name>` and the text under it: a definition for the glossary.
309#[derive(Clone, Debug)]
310pub struct Term {
311    pub name: String,
312    pub slug: String,
313    pub description: Vec<String>,
314    pub span: Span,
315}
316
317impl Suite {
318    pub fn exp(&self, id: ExpId) -> &Expectation {
319        &self.expectations[id.0 as usize]
320    }
321
322    pub fn feature(&self, id: FeatureId) -> &Feature {
323        &self.features[id.0 as usize]
324    }
325
326    pub fn story(&self, id: StoryId) -> &Story {
327        &self.stories[id.0 as usize]
328    }
329
330    pub fn section(&self, id: SectionId) -> &Section {
331        &self.sections[id.0 as usize]
332    }
333
334    /// The root first, down to `id`.
335    pub fn chain(&self, id: SectionId) -> Vec<&Section> {
336        let mut out = vec![self.section(id)];
337        while let Some(p) = out.last().unwrap().parent {
338            out.push(self.section(p));
339        }
340        out.reverse();
341        out
342    }
343
344    /// The section an expectation belongs to (its feature's).
345    pub fn section_of(&self, e: ExpId) -> &Section {
346        self.section(self.feature(self.exp(e).feature).section)
347    }
348
349    /// `id` and every section below it.
350    pub fn subtree(&self, id: SectionId) -> Vec<SectionId> {
351        let mut out = vec![id];
352        let mut i = 0;
353        while i < out.len() {
354            out.extend(self.section(out[i]).children.iter().copied());
355            i += 1;
356        }
357        out
358    }
359
360    pub fn describe(&self, c: &Condition) -> String {
361        self.vocabulary.describe(c)
362    }
363
364    /// "the user presses reset", "the dial is above 55 %", or both.
365    pub fn describe_trigger(&self, t: &Trigger) -> String {
366        let mut parts = vec![];
367        if !t.changes.is_unconstrained() {
368            parts.push(self.vocabulary.describe(&t.changes));
369        }
370        if let Some(e) = t.event {
371            parts.push(self.vocabulary.event(e).text.clone());
372        }
373        if parts.is_empty() { "always".into() } else { parts.join(" and ") }
374    }
375
376    /// Transitions (expectations with a trigger), in declaration order.
377    pub fn transitions(&self) -> impl Iterator<Item = &Expectation> {
378        self.expectations.iter().filter(|e| !e.is_invariant())
379    }
380
381    pub fn invariants(&self) -> impl Iterator<Item = &Expectation> {
382        self.expectations.iter().filter(|e| e.is_invariant())
383    }
384}