1use std::process::Command as ProcCommand;
11
12use serde_json::json;
13
14use crate::test_json::Unmatched;
15
16pub const FILTER_ENV: &str = "BYNK_TEST_FILTER";
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct TestFilter {
24 pub suites: Vec<String>,
25 pub cases: Vec<String>,
26 pub partial: bool,
30 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 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 pub fn partial(mut self, partial: bool) -> Self {
59 self.partial = partial;
60 self
61 }
62
63 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 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 pub fn is_empty(&self) -> bool {
86 self.suites.is_empty() && self.cases.is_empty()
87 }
88
89 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 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 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 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}