subplot/
scenarios.rs

1use std::collections::HashSet;
2
3use crate::{html::Location, ScenarioStep};
4use lazy_static::lazy_static;
5use serde::{Deserialize, Serialize};
6
7/// An acceptance test scenario.
8///
9/// A scenario consists of a title, by which it can be identified, and
10/// a sequence of steps. The Scenario struct assumes the steps are
11/// valid and make sense; the struct does not try to validate the
12/// sequence.
13#[derive(Debug, Serialize, Deserialize)]
14pub struct Scenario {
15    title: String,
16    origin: Location,
17    steps: Vec<ScenarioStep>,
18    labels: HashSet<String>,
19}
20
21impl Scenario {
22    /// Construct a new scenario.
23    ///
24    /// The new scenario will have a title, but no steps.
25    pub fn new(title: &str, origin: Location) -> Scenario {
26        Scenario {
27            title: title.to_string(),
28            origin,
29            steps: vec![],
30            labels: Default::default(),
31        }
32    }
33
34    /// Add a label to the set of labels for this scenario
35    pub fn add_label(&mut self, label: &str) {
36        self.labels.insert(String::from(label));
37    }
38
39    /// List the labels applied to this scenario
40    pub fn labels(&self) -> impl Iterator<Item = &str> {
41        self.labels.iter().map(String::as_str)
42    }
43
44    /// Check if a given label is present on this scenario
45    pub fn has_label(&self, label: &str) -> bool {
46        self.labels.contains(label)
47    }
48
49    /// Return the title of a scenario.
50    pub fn title(&self) -> &str {
51        &self.title
52    }
53
54    /// Does the scenario have steps?
55    pub fn has_steps(&self) -> bool {
56        !self.steps.is_empty()
57    }
58
59    /// Return slice with all the steps.
60    pub fn steps(&self) -> &[ScenarioStep] {
61        &self.steps
62    }
63
64    /// Add a step to a scenario.
65    pub fn add(&mut self, step: &ScenarioStep) {
66        self.steps.push(step.clone());
67    }
68
69    pub(crate) fn origin(&self) -> &Location {
70        &self.origin
71    }
72}
73
74#[cfg(test)]
75mod test {
76    use super::Scenario;
77    use crate::html::Location;
78    use crate::ScenarioStep;
79    use crate::StepKind;
80
81    #[test]
82    fn has_title() {
83        let scen = Scenario::new("title", Location::Unknown);
84        assert_eq!(scen.title(), "title");
85    }
86
87    #[test]
88    fn has_no_steps_initially() {
89        let scen = Scenario::new("title", Location::Unknown);
90        assert_eq!(scen.steps().len(), 0);
91    }
92
93    #[test]
94    fn adds_step() {
95        let mut scen = Scenario::new("title", Location::Unknown);
96        let step = ScenarioStep::new(StepKind::Given, "and", "foo", Location::Unknown);
97        scen.add(&step);
98        assert_eq!(scen.steps(), &[step]);
99    }
100}
101
102/// An element of a scenario filter for codegen
103///
104/// Each element of a filter must decide to "include" the scenario for the scenario to
105/// end up in the codegen output.
106#[derive(Clone)]
107pub struct ScenarioFilterElement {
108    has: Vec<String>,
109    lacks: Vec<String>,
110    include: bool,
111}
112
113impl ScenarioFilterElement {
114    fn everything() -> Self {
115        Self {
116            has: Vec::new(),
117            lacks: Vec::new(),
118            include: true,
119        }
120    }
121
122    /// Creates a new filter element from a filter string
123    ///
124    /// The filter may be to include, or exclude scenarios based on the filter string.
125    ///
126    /// A filter string is a comma-separated list of labels.  If a label is prefixed by `-`
127    /// then it must *not* be present for the filter element to match.  All unprefixed
128    /// labels must be present as well.
129    pub fn new(include: bool, filter: &str) -> Self {
130        let mut ret = Self {
131            has: Vec::new(),
132            lacks: Vec::new(),
133            include,
134        };
135
136        for label in filter.split(',').map(str::trim) {
137            if let Some(label) = label.strip_prefix('-') {
138                ret.lacks.push(label.into());
139            } else {
140                ret.has.push(label.into());
141            }
142        }
143
144        ret
145    }
146
147    fn includes(&self, scenario: &Scenario) -> bool {
148        let has = self.has.iter().all(|s| scenario.labels.contains(s));
149        let lacks = self.lacks.iter().any(|s| scenario.labels.contains(s));
150        // We match if we have the labels we want, and none of the labels we should lack
151        let matched = has & !lacks;
152
153        // matched  include output
154        // false    false   true
155        // false    true    false
156        // true     false   false
157        // true     true    true
158
159        !(matched ^ self.include)
160    }
161}
162
163/// A scenario filter
164///
165/// A scenario filter consists of multiple [`ScenarioFilterElement`]s, each of
166/// which must match for a scenario to be included in the codegen output.
167#[derive(Clone)]
168pub struct ScenarioFilter {
169    elements: Vec<ScenarioFilterElement>,
170}
171
172lazy_static! {
173    /// A [`ScenarioFilter`] set to include everything
174    pub static ref SCENARIO_FILTER_EVERYTHING: ScenarioFilter = ScenarioFilter::everything();
175    /// A [`ScenarioFilter`] set to include nothing
176    pub static ref SCENARIO_FILTER_NOTHING: ScenarioFilter = ScenarioFilter::nothing();
177}
178
179impl ScenarioFilter {
180    fn everything() -> Self {
181        Self {
182            elements: vec![ScenarioFilterElement::everything()],
183        }
184    }
185
186    fn nothing() -> Self {
187        Self { elements: vec![] }
188    }
189
190    /// Add a [`ScenarioFilterElement`] to this filter
191    pub fn push(&mut self, element: ScenarioFilterElement) {
192        self.elements.push(element);
193    }
194
195    /// Check if the given scenario would be included by this filter
196    pub fn includes(&self, scenario: &Scenario) -> bool {
197        !self.elements.is_empty() && self.elements.iter().all(|e| e.includes(scenario))
198    }
199}
200
201#[cfg(test)]
202mod filtertest {
203    use crate::html::Location;
204
205    use super::{Scenario, ScenarioFilter, ScenarioFilterElement};
206
207    fn scenario_none() -> Scenario {
208        Scenario::new("whatever", Location::unknown())
209    }
210
211    fn scenario_slow() -> Scenario {
212        let mut ret = Scenario::new("slow", Location::unknown());
213        ret.add_label("slow");
214        ret
215    }
216
217    fn scenario_slow_important() -> Scenario {
218        let mut ret = Scenario::new("slow-important", Location::unknown());
219        ret.add_label("slow");
220        ret.add_label("important");
221        ret
222    }
223
224    fn scenario_fast() -> Scenario {
225        let mut ret = Scenario::new("fast", Location::unknown());
226        ret.add_label("fast");
227        ret
228    }
229
230    fn scenario_fast_pointless() -> Scenario {
231        let mut ret = Scenario::new("fast-pointless", Location::unknown());
232        ret.add_label("fast");
233        ret.add_label("pointless");
234        ret
235    }
236
237    fn all_scenarios() -> Vec<Scenario> {
238        vec![
239            scenario_none(),
240            scenario_slow(),
241            scenario_fast(),
242            scenario_slow_important(),
243            scenario_fast_pointless(),
244        ]
245    }
246
247    #[test]
248    fn include_all() {
249        let filter = ScenarioFilter::everything();
250        let scenarios = all_scenarios();
251        assert_eq!(
252            scenarios.len(),
253            scenarios.iter().filter(|s| filter.includes(s)).count()
254        );
255    }
256
257    #[test]
258    fn include_none() {
259        let filter = ScenarioFilter::nothing();
260        assert!(!all_scenarios().into_iter().any(|s| filter.includes(&s)))
261    }
262
263    #[test]
264    fn include_fast() {
265        let mut filter = ScenarioFilter::nothing();
266        filter.push(ScenarioFilterElement::new(true, "fast"));
267        assert_eq!(
268            all_scenarios()
269                .into_iter()
270                .filter(|s| filter.includes(s))
271                .count(),
272            2
273        );
274    }
275
276    #[test]
277    fn exclude_slow() {
278        let mut filter = ScenarioFilter::everything();
279        filter.push(ScenarioFilterElement::new(false, "slow"));
280        assert_eq!(
281            all_scenarios()
282                .into_iter()
283                .filter(|s| filter.includes(s))
284                .count(),
285            3
286        );
287    }
288
289    #[test]
290    fn exclude_unimportant_slow() {
291        let mut filter = ScenarioFilter::everything();
292        filter.push(ScenarioFilterElement::new(false, "slow, -important"));
293        assert_eq!(
294            all_scenarios()
295                .into_iter()
296                .filter(|s| filter.includes(s))
297                .count(),
298            4
299        );
300    }
301}