subplot 0.14.0

tools for specifying, documenting, and implementing automated acceptance tests for systems and software
Documentation
use std::collections::HashSet;

use crate::{html::Location, ScenarioStep};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};

/// An acceptance test scenario.
///
/// A scenario consists of a title, by which it can be identified, and
/// a sequence of steps. The Scenario struct assumes the steps are
/// valid and make sense; the struct does not try to validate the
/// sequence.
#[derive(Debug, Serialize, Deserialize)]
pub struct Scenario {
    title: String,
    origin: Location,
    steps: Vec<ScenarioStep>,
    labels: HashSet<String>,
}

impl Scenario {
    /// Construct a new scenario.
    ///
    /// The new scenario will have a title, but no steps.
    pub fn new(title: &str, origin: Location) -> Scenario {
        Scenario {
            title: title.to_string(),
            origin,
            steps: vec![],
            labels: Default::default(),
        }
    }

    /// Add a label to the set of labels for this scenario
    pub fn add_label(&mut self, label: &str) {
        self.labels.insert(String::from(label));
    }

    /// List the labels applied to this scenario
    pub fn labels(&self) -> impl Iterator<Item = &str> {
        self.labels.iter().map(String::as_str)
    }

    /// Check if a given label is present on this scenario
    pub fn has_label(&self, label: &str) -> bool {
        self.labels.contains(label)
    }

    /// Return the title of a scenario.
    pub fn title(&self) -> &str {
        &self.title
    }

    /// Does the scenario have steps?
    pub fn has_steps(&self) -> bool {
        !self.steps.is_empty()
    }

    /// Return slice with all the steps.
    pub fn steps(&self) -> &[ScenarioStep] {
        &self.steps
    }

    /// Add a step to a scenario.
    pub fn add(&mut self, step: &ScenarioStep) {
        self.steps.push(step.clone());
    }

    pub(crate) fn origin(&self) -> &Location {
        &self.origin
    }
}

#[cfg(test)]
mod test {
    use super::Scenario;
    use crate::html::Location;
    use crate::ScenarioStep;
    use crate::StepKind;

    #[test]
    fn has_title() {
        let scen = Scenario::new("title", Location::Unknown);
        assert_eq!(scen.title(), "title");
    }

    #[test]
    fn has_no_steps_initially() {
        let scen = Scenario::new("title", Location::Unknown);
        assert_eq!(scen.steps().len(), 0);
    }

    #[test]
    fn adds_step() {
        let mut scen = Scenario::new("title", Location::Unknown);
        let step = ScenarioStep::new(StepKind::Given, "and", "foo", Location::Unknown);
        scen.add(&step);
        assert_eq!(scen.steps(), &[step]);
    }
}

/// An element of a scenario filter for codegen
///
/// Each element of a filter must decide to "include" the scenario for the scenario to
/// end up in the codegen output.
#[derive(Clone)]
pub struct ScenarioFilterElement {
    has: Vec<String>,
    lacks: Vec<String>,
    include: bool,
}

impl ScenarioFilterElement {
    fn everything() -> Self {
        Self {
            has: Vec::new(),
            lacks: Vec::new(),
            include: true,
        }
    }

    /// Creates a new filter element from a filter string
    ///
    /// The filter may be to include, or exclude scenarios based on the filter string.
    ///
    /// A filter string is a comma-separated list of labels.  If a label is prefixed by `-`
    /// then it must *not* be present for the filter element to match.  All unprefixed
    /// labels must be present as well.
    pub fn new(include: bool, filter: &str) -> Self {
        let mut ret = Self {
            has: Vec::new(),
            lacks: Vec::new(),
            include,
        };

        for label in filter.split(',').map(str::trim) {
            if let Some(label) = label.strip_prefix('-') {
                ret.lacks.push(label.into());
            } else {
                ret.has.push(label.into());
            }
        }

        ret
    }

    fn includes(&self, scenario: &Scenario) -> bool {
        let has = self.has.iter().all(|s| scenario.labels.contains(s));
        let lacks = self.lacks.iter().any(|s| scenario.labels.contains(s));
        // We match if we have the labels we want, and none of the labels we should lack
        let matched = has & !lacks;

        // matched  include output
        // false    false   true
        // false    true    false
        // true     false   false
        // true     true    true

        !(matched ^ self.include)
    }
}

/// A scenario filter
///
/// A scenario filter consists of multiple [`ScenarioFilterElement`]s, each of
/// which must match for a scenario to be included in the codegen output.
#[derive(Clone)]
pub struct ScenarioFilter {
    elements: Vec<ScenarioFilterElement>,
}

lazy_static! {
    /// A [`ScenarioFilter`] set to include everything
    pub static ref SCENARIO_FILTER_EVERYTHING: ScenarioFilter = ScenarioFilter::everything();
    /// A [`ScenarioFilter`] set to include nothing
    pub static ref SCENARIO_FILTER_NOTHING: ScenarioFilter = ScenarioFilter::nothing();
}

impl ScenarioFilter {
    fn everything() -> Self {
        Self {
            elements: vec![ScenarioFilterElement::everything()],
        }
    }

    fn nothing() -> Self {
        Self { elements: vec![] }
    }

    /// Add a [`ScenarioFilterElement`] to this filter
    pub fn push(&mut self, element: ScenarioFilterElement) {
        self.elements.push(element);
    }

    /// Check if the given scenario would be included by this filter
    pub fn includes(&self, scenario: &Scenario) -> bool {
        !self.elements.is_empty() && self.elements.iter().all(|e| e.includes(scenario))
    }
}

#[cfg(test)]
mod filtertest {
    use crate::html::Location;

    use super::{Scenario, ScenarioFilter, ScenarioFilterElement};

    fn scenario_none() -> Scenario {
        Scenario::new("whatever", Location::unknown())
    }

    fn scenario_slow() -> Scenario {
        let mut ret = Scenario::new("slow", Location::unknown());
        ret.add_label("slow");
        ret
    }

    fn scenario_slow_important() -> Scenario {
        let mut ret = Scenario::new("slow-important", Location::unknown());
        ret.add_label("slow");
        ret.add_label("important");
        ret
    }

    fn scenario_fast() -> Scenario {
        let mut ret = Scenario::new("fast", Location::unknown());
        ret.add_label("fast");
        ret
    }

    fn scenario_fast_pointless() -> Scenario {
        let mut ret = Scenario::new("fast-pointless", Location::unknown());
        ret.add_label("fast");
        ret.add_label("pointless");
        ret
    }

    fn all_scenarios() -> Vec<Scenario> {
        vec![
            scenario_none(),
            scenario_slow(),
            scenario_fast(),
            scenario_slow_important(),
            scenario_fast_pointless(),
        ]
    }

    #[test]
    fn include_all() {
        let filter = ScenarioFilter::everything();
        let scenarios = all_scenarios();
        assert_eq!(
            scenarios.len(),
            scenarios.iter().filter(|s| filter.includes(s)).count()
        );
    }

    #[test]
    fn include_none() {
        let filter = ScenarioFilter::nothing();
        assert!(!all_scenarios().into_iter().any(|s| filter.includes(&s)))
    }

    #[test]
    fn include_fast() {
        let mut filter = ScenarioFilter::nothing();
        filter.push(ScenarioFilterElement::new(true, "fast"));
        assert_eq!(
            all_scenarios()
                .into_iter()
                .filter(|s| filter.includes(s))
                .count(),
            2
        );
    }

    #[test]
    fn exclude_slow() {
        let mut filter = ScenarioFilter::everything();
        filter.push(ScenarioFilterElement::new(false, "slow"));
        assert_eq!(
            all_scenarios()
                .into_iter()
                .filter(|s| filter.includes(s))
                .count(),
            3
        );
    }

    #[test]
    fn exclude_unimportant_slow() {
        let mut filter = ScenarioFilter::everything();
        filter.push(ScenarioFilterElement::new(false, "slow, -important"));
        assert_eq!(
            all_scenarios()
                .into_iter()
                .filter(|s| filter.includes(s))
                .count(),
            4
        );
    }
}