Skip to main content

bynk_driver/
test_filter.rs

1//! `bynk test --case` / `--suite` (#2273): the set of tests one run executes.
2//!
3//! Both flags repeat. Values of one flag combine as **or**, the two flags as
4//! **and**: a case runs when its suite is among the `--suite` values (when any
5//! are given) and its name is among the `--case` values (when any are given).
6//! The set reaches the emitted test entry as one JSON value in
7//! `BYNK_TEST_FILTER`, so one process runs the whole selection, with one seed
8//! and one coverage report.
9
10use std::process::Command as ProcCommand;
11
12use serde_json::json;
13
14use crate::test_json::Unmatched;
15
16/// The environment variable carrying the filter to the emitted test entry.
17pub const FILTER_ENV: &str = "BYNK_TEST_FILTER";
18
19/// The `--suite` and `--case` values of one run. Empty values are dropped and
20/// duplicates removed, keeping the first occurrence, so an empty flag is unset
21/// as `--case ""` always was.
22#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct TestFilter {
24    pub suites: Vec<String>,
25    pub cases: Vec<String>,
26    /// The run sees only part of the selection (`--partial-selection`, which an
27    /// orchestrator that runs the selection as several processes passes): it
28    /// judges nothing, and leaves the unmatched values to its caller.
29    pub partial: bool,
30    /// The (suite, case) pairs the build skipped that this filter selects
31    /// (`--against … --skip-unsupported`). They are not in the build, so the
32    /// runner is told they exist: a selected skipped case is not unmatched.
33    pub skipped: Vec<(String, String)>,
34}
35
36fn normalised(values: &[String]) -> Vec<String> {
37    let mut out: Vec<String> = Vec::new();
38    for value in values {
39        if !value.is_empty() && !out.contains(value) {
40            out.push(value.clone());
41        }
42    }
43    out
44}
45
46impl TestFilter {
47    /// The filter for the `--suite` and `--case` values as given.
48    pub fn new(suites: &[String], cases: &[String]) -> Self {
49        Self {
50            suites: normalised(suites),
51            cases: normalised(cases),
52            partial: false,
53            skipped: Vec::new(),
54        }
55    }
56
57    /// Mark this run as seeing only part of a selection.
58    pub fn partial(mut self, partial: bool) -> Self {
59        self.partial = partial;
60        self
61    }
62
63    /// Whether `case` of `suite` is selected: its suite is among the `--suite`
64    /// values (if any) and its name among the `--case` values (if any).
65    pub fn selects(&self, suite: &str, case: &str) -> bool {
66        (self.suites.is_empty() || self.suites.iter().any(|s| s == suite))
67            && (self.cases.is_empty() || self.cases.iter().any(|c| c == case))
68    }
69
70    /// The same filter, told which of the build's skipped (suite, case) pairs it
71    /// selects.
72    pub fn with_skipped<'a>(
73        mut self,
74        skipped: impl IntoIterator<Item = (&'a str, &'a str)>,
75    ) -> Self {
76        self.skipped = skipped
77            .into_iter()
78            .filter(|(suite, case)| self.selects(suite, case))
79            .map(|(suite, case)| (suite.to_string(), case.to_string()))
80            .collect();
81        self
82    }
83
84    /// No value is left, so every test runs.
85    pub fn is_empty(&self) -> bool {
86        self.suites.is_empty() && self.cases.is_empty()
87    }
88
89    /// The values that select nothing among `available` (suite, case) pairs: a
90    /// suite no pair has, and a case no pair in a selected suite has. The
91    /// runner judges the same way; `--against dev` asks it before starting
92    /// anything, because it runs the selection in several processes.
93    pub fn unmatched_in(&self, available: &[(String, String)]) -> Unmatched {
94        let selected =
95            |suite: &str| self.suites.is_empty() || self.suites.iter().any(|s| s == suite);
96        Unmatched {
97            suites: self
98                .suites
99                .iter()
100                .filter(|s| !available.iter().any(|(suite, _)| suite == *s))
101                .cloned()
102                .collect(),
103            cases: self
104                .cases
105                .iter()
106                .filter(|c| {
107                    !available
108                        .iter()
109                        .any(|(suite, case)| selected(suite) && case == *c)
110                })
111                .cloned()
112                .collect(),
113        }
114    }
115
116    /// The filter as the arguments that select it again, for a child `bynkc test`.
117    pub fn args(&self) -> Vec<String> {
118        let mut args = Vec::new();
119        for suite in &self.suites {
120            args.push("--suite".to_string());
121            args.push(suite.clone());
122        }
123        for case in &self.cases {
124            args.push("--case".to_string());
125            args.push(case.clone());
126        }
127        args
128    }
129
130    /// Hand the filter to the runner `cmd` launches. `program` (`bynkc` or
131    /// `bynk`) is carried so a rich run, whose output the driver cannot read,
132    /// names the same program in its error as a JSON run does.
133    pub fn apply(&self, program: &str, cmd: &mut ProcCommand) {
134        if self.is_empty() {
135            return;
136        }
137        let mut value = serde_json::Map::new();
138        value.insert("program".to_string(), json!(program));
139        if !self.suites.is_empty() {
140            value.insert("suites".to_string(), json!(self.suites));
141        }
142        if !self.cases.is_empty() {
143            value.insert("cases".to_string(), json!(self.cases));
144        }
145        if self.partial {
146            value.insert("partial".to_string(), json!(true));
147        }
148        if !self.skipped.is_empty() {
149            let skipped: Vec<_> = self
150                .skipped
151                .iter()
152                .map(|(suite, case)| json!({"suite": suite, "case": case}))
153                .collect();
154            value.insert("skipped".to_string(), json!(skipped));
155        }
156        cmd.env(FILTER_ENV, serde_json::Value::Object(value).to_string());
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    fn strings(values: &[&str]) -> Vec<String> {
165        values.iter().map(|s| s.to_string()).collect()
166    }
167
168    #[test]
169    fn drops_empty_values_and_duplicates_keeping_order() {
170        let filter = TestFilter::new(&strings(&["b", "", "a", "b"]), &strings(&["x", "x", ""]));
171        assert_eq!(filter.suites, strings(&["b", "a"]));
172        assert_eq!(filter.cases, strings(&["x"]));
173    }
174
175    #[test]
176    fn an_empty_flag_is_unset() {
177        assert!(TestFilter::new(&strings(&[""]), &strings(&[""])).is_empty());
178        assert!(TestFilter::new(&[], &[]).is_empty());
179        assert!(!TestFilter::new(&[], &strings(&["x"])).is_empty());
180    }
181
182    #[test]
183    fn a_name_with_a_comma_is_one_value() {
184        let filter = TestFilter::new(&[], &strings(&["joins, groups, and orphans"]));
185        assert_eq!(filter.cases, strings(&["joins, groups, and orphans"]));
186    }
187
188    #[test]
189    fn judges_each_value_against_the_available_cases() {
190        let available = vec![
191            ("orders".to_string(), "places".to_string()),
192            ("billing".to_string(), "charges".to_string()),
193        ];
194        let judge = |suites: &[&str], cases: &[&str]| {
195            TestFilter::new(&strings(suites), &strings(cases)).unmatched_in(&available)
196        };
197        assert!(judge(&[], &[]).is_empty());
198        assert!(judge(&["orders"], &["places"]).is_empty());
199        assert_eq!(judge(&[], &["places", "typo"]).cases, strings(&["typo"]));
200        assert_eq!(judge(&["nope"], &[]).suites, strings(&["nope"]));
201        // A case that exists only outside the selected suites selects nothing.
202        assert_eq!(
203            judge(&["orders"], &["charges"]).cases,
204            strings(&["charges"])
205        );
206    }
207
208    #[test]
209    fn selects_by_suite_and_case_together() {
210        let filter = TestFilter::new(&strings(&["a"]), &strings(&["x"]));
211        assert!(filter.selects("a", "x"));
212        assert!(!filter.selects("b", "x"));
213        assert!(!filter.selects("a", "y"));
214        assert!(TestFilter::default().selects("anything", "at all"));
215    }
216
217    #[test]
218    fn tells_the_runner_which_skipped_cases_it_selects() {
219        let filter = TestFilter::new(&strings(&["a"]), &[]).with_skipped([("a", "x"), ("b", "y")]);
220        assert_eq!(filter.skipped, vec![("a".to_string(), "x".to_string())]);
221        let mut cmd = ProcCommand::new("node");
222        filter.apply("bynkc", &mut cmd);
223        let value = cmd
224            .get_envs()
225            .find(|(k, _)| *k == FILTER_ENV)
226            .and_then(|(_, v)| v.map(|v| v.to_string_lossy().into_owned()))
227            .unwrap();
228        let parsed: serde_json::Value = serde_json::from_str(&value).unwrap();
229        assert_eq!(parsed["skipped"], json!([{"suite": "a", "case": "x"}]));
230    }
231
232    #[test]
233    fn args_repeat_each_flag() {
234        let filter = TestFilter::new(&strings(&["s"]), &strings(&["a", "b"]));
235        assert_eq!(
236            filter.args(),
237            strings(&["--suite", "s", "--case", "a", "--case", "b"])
238        );
239    }
240
241    #[test]
242    fn sets_the_environment_only_when_a_filter_was_given() {
243        let envs = |filter: &TestFilter| {
244            let mut cmd = ProcCommand::new("node");
245            filter.apply("bynkc", &mut cmd);
246            cmd.get_envs()
247                .find(|(k, _)| *k == FILTER_ENV)
248                .and_then(|(_, v)| v.map(|v| v.to_string_lossy().into_owned()))
249        };
250        assert_eq!(envs(&TestFilter::default()), None);
251        let value = envs(&TestFilter::new(&strings(&["s"]), &strings(&["a \"b\""]))).unwrap();
252        let parsed: serde_json::Value = serde_json::from_str(&value).unwrap();
253        assert_eq!(
254            parsed,
255            json!({"program": "bynkc", "suites": ["s"], "cases": ["a \"b\""]})
256        );
257    }
258}