1#[must_use]
9pub fn runs(toolchain: speccoder_core::collection::Toolchain, argv: &[String]) -> bool {
10 use speccoder_core::collection::Toolchain;
11 let has = |w: &str| argv.iter().any(|a| a == w || a.ends_with(&format!("/{w}")));
12 let word = |i: usize| argv.get(i).map(String::as_str);
13 match toolchain {
14 Toolchain::Rust => {
15 let cargo = argv
17 .iter()
18 .position(|a| a == "cargo" || a.ends_with("/cargo"));
19 match cargo {
20 Some(i) => {
21 matches!(word(i + 1), Some("test"))
22 || matches!((word(i + 1), word(i + 2)), (Some("nextest"), Some("run")))
23 }
24 None => has("cargo-nextest"),
25 }
26 }
27 Toolchain::Python => argv.iter().any(|a| a.contains("pytest")),
28 Toolchain::JavaScript => argv
29 .iter()
30 .any(|a| a.contains("vitest") || a.contains("jest")),
31 Toolchain::Go => {
32 has("gotestsum")
33 || argv
34 .iter()
35 .position(|a| a == "go" || a.ends_with("/go"))
36 .is_some_and(|i| word(i + 1) == Some("test"))
37 }
38 }
39}
40
41#[must_use]
44pub fn nextest_list(argv: &[String]) -> Option<Vec<String>> {
45 let i = argv.iter().position(|a| a == "nextest")?;
46 if argv.get(i + 1).map(String::as_str) != Some("run") {
47 return None;
48 }
49 const RUN_ONLY: &[&str] = &[
51 "--no-fail-fast",
52 "--fail-fast",
53 "--no-capture",
54 "--nocapture",
55 "--retries",
56 "--success-output",
57 "--failure-output",
58 "--status-level",
59 "--final-status-level",
60 "--no-tests",
61 "--test-threads",
62 "-j",
63 "--profile",
64 "-P",
65 "--max-fail",
66 ];
67 let mut out: Vec<String> = argv[..=i].to_vec();
68 out.push("list".to_owned());
69 let mut k = i + 2;
70 while k < argv.len() {
71 let a = &argv[k];
72 let flag = a.split('=').next().unwrap_or(a);
73 if RUN_ONLY.contains(&flag) {
74 if !a.contains('=')
76 && !matches!(
77 flag,
78 "--no-fail-fast" | "--fail-fast" | "--no-capture" | "--nocapture"
79 )
80 {
81 k += 1;
82 }
83 k += 1;
84 continue;
85 }
86 out.push(a.clone());
87 k += 1;
88 }
89 out.extend(["--message-format", "json"].map(str::to_owned));
90 Some(out)
91}
92
93fn split_cargo(argv: &[String]) -> (Vec<String>, Vec<String>) {
96 match argv.iter().position(|a| a == "--") {
97 Some(i) => (argv[..i].to_vec(), argv[i + 1..].to_vec()),
98 None => (argv.to_vec(), Vec::new()),
99 }
100}
101
102#[must_use]
105pub fn cargo_runs(argv: &[String], pkg: &str, root_package: bool, kind: &(String, String)) -> bool {
106 let (opts, _) = split_cargo(argv);
107 let value = |flag: &str| -> Vec<String> {
108 let mut v = Vec::new();
109 for (i, a) in opts.iter().enumerate() {
110 if a == flag {
111 if let Some(n) = opts.get(i + 1) {
112 v.push(n.clone());
113 }
114 } else if let Some(x) = a.strip_prefix(&format!("{flag}=")) {
115 v.push(x.to_owned());
116 }
117 }
118 v
119 };
120 let has = |f: &str| opts.iter().any(|a| a == f);
121 let packages: Vec<String> = value("-p").into_iter().chain(value("--package")).collect();
122 let package_ok = if !packages.is_empty() {
123 packages.iter().any(|p| p == pkg)
124 } else {
125 root_package || has("--workspace") || has("--all")
126 };
127 if !package_ok {
128 return false;
129 }
130 let tests = value("--test");
131 let bins = value("--bin");
132 let selected = has("--lib")
133 || has("--tests")
134 || has("--bins")
135 || has("--doc")
136 || has("--all-targets")
137 || !tests.is_empty()
138 || !bins.is_empty()
139 || has("--examples")
140 || has("--benches");
141 if !selected || has("--all-targets") {
142 return kind.0 != "bench" && kind.0 != "example";
143 }
144 match kind.0.as_str() {
145 "lib" => has("--lib"),
146 "bin" => has("--bins") || bins.contains(&kind.1),
147 "test" => has("--tests") || tests.contains(&kind.1),
148 _ => false,
149 }
150}
151
152#[must_use]
157pub fn cargo_list(
158 argv: &[String],
159 pkg: &str,
160 kind: &(String, String),
161) -> (Vec<String>, Option<Vec<String>>) {
162 let (opts, test_args) = split_cargo(argv);
163 let mut kept = Vec::new();
165 let mut i = 0;
166 while i < opts.len() {
167 let a = &opts[i];
168 let flag = a.split('=').next().unwrap_or(a);
169 match flag {
170 "--lib" | "--tests" | "--bins" | "--doc" | "--all-targets" | "--examples"
171 | "--benches" | "--no-fail-fast" => {}
172 "--test" | "--bin" | "-p" | "--package" => {
173 if !a.contains('=') {
174 i += 1;
175 }
176 }
177 _ => kept.push(a.clone()),
178 }
179 i += 1;
180 }
181 let mut base = kept;
182 base.extend(["-p".to_owned(), pkg.to_owned()]);
183 match kind.0.as_str() {
184 "lib" => base.push("--lib".to_owned()),
185 "bin" => base.extend(["--bin".to_owned(), kind.1.clone()]),
186 _ => base.extend(["--test".to_owned(), kind.1.clone()]),
187 }
188 base.push("--".to_owned());
189 let filters: Vec<String> = test_args
191 .iter()
192 .filter(|a| {
193 !matches!(
194 a.as_str(),
195 "--nocapture" | "--no-capture" | "--show-output" | "-q" | "--quiet"
196 )
197 })
198 .cloned()
199 .collect();
200 let runs_ignored = filters
201 .iter()
202 .any(|a| a == "--ignored" || a == "--include-ignored");
203 let mut list = base.clone();
204 list.extend(
205 filters
206 .iter()
207 .filter(|a| *a != "--include-ignored")
208 .cloned(),
209 );
210 list.extend(["--list", "--format", "terse"].map(str::to_owned));
211 let ignored = (!runs_ignored).then(|| {
212 let mut l = base;
213 l.extend(filters.iter().cloned());
214 l.extend(["--list", "--format", "terse", "--ignored"].map(str::to_owned));
215 l
216 });
217 (list, ignored)
218}
219
220fn drop_flags(argv: &[String], flags: &[&str]) -> Vec<String> {
221 let mut out = Vec::new();
222 let mut i = 0;
223 while i < argv.len() {
224 let a = &argv[i];
225 let flag = a.split('=').next().unwrap_or(a);
226 if flags.contains(&flag) {
227 if !a.contains('=') && argv.get(i + 1).is_some_and(|n| !n.starts_with('-')) {
228 i += 1;
229 }
230 i += 1;
231 continue;
232 }
233 out.push(a.clone());
234 i += 1;
235 }
236 out
237}
238
239#[must_use]
241pub fn pytest_list(argv: &[String]) -> Vec<String> {
242 let mut out = drop_flags(
243 argv,
244 &[
245 "--junitxml",
246 "--junit-xml",
247 "--cov",
248 "--cov-report",
249 "-x",
250 "--exitfirst",
251 "-v",
252 "--verbose",
253 ],
254 );
255 out.extend(["--collect-only", "-q"].map(str::to_owned));
256 out
257}
258
259#[must_use]
261pub fn vitest_list(argv: &[String]) -> Option<Vec<String>> {
262 let i = argv.iter().position(|a| a.ends_with("vitest"))?;
263 let mut out: Vec<String> = argv[..=i].to_vec();
264 out.push("list".to_owned());
265 let rest = &argv[i + 1..];
266 let rest = match rest.first().map(String::as_str) {
267 Some("run" | "watch" | "dev") => &rest[1..],
268 _ => rest,
269 };
270 out.extend(drop_flags(
271 rest,
272 &[
273 "--reporter",
274 "--outputFile",
275 "--coverage",
276 "--run",
277 "--silent",
278 ],
279 ));
280 out.push("--json".to_owned());
281 Some(out)
282}
283
284#[must_use]
286pub fn jest_list(argv: &[String]) -> Vec<String> {
287 let mut out = drop_flags(
288 argv,
289 &[
290 "--reporters",
291 "--ci",
292 "--coverage",
293 "--silent",
294 "--json",
295 "--outputFile",
296 ],
297 );
298 out.push("--listTests".to_owned());
299 out
300}
301
302#[must_use]
304pub fn go_list(argv: &[String]) -> Vec<String> {
305 let go_args: Vec<String> = if argv.iter().any(|a| a.ends_with("gotestsum")) {
307 argv.iter()
308 .skip_while(|a| *a != "--")
309 .skip(1)
310 .cloned()
311 .collect()
312 } else {
313 argv.iter()
314 .skip_while(|a| !(a.as_str() == "go" || a.ends_with("/go")))
315 .skip(2)
316 .cloned()
317 .collect()
318 };
319 let mut pattern = ".".to_owned();
320 let mut rest = Vec::new();
321 let mut i = 0;
322 while i < go_args.len() {
323 let a = &go_args[i];
324 if a == "-run" {
325 if let Some(p) = go_args.get(i + 1) {
326 pattern = p.clone();
327 }
328 i += 2;
329 continue;
330 }
331 if let Some(p) = a.strip_prefix("-run=") {
332 pattern = p.to_owned();
333 } else if matches!(a.as_str(), "-v" | "-json" | "-race" | "-short")
334 || a.starts_with("-count")
335 || a.starts_with("-cover")
336 || a.starts_with("-timeout")
337 || a.starts_with("-coverprofile")
338 {
339 } else {
340 rest.push(a.clone());
341 }
342 i += 1;
343 }
344 let mut out = vec![
345 "go".to_owned(),
346 "test".to_owned(),
347 "-list".to_owned(),
348 pattern,
349 ];
350 if rest.iter().all(|a| a.starts_with('-')) {
351 rest.push("./...".to_owned());
352 }
353 out.extend(rest);
354 out
355}
356
357#[must_use]
360pub fn skips_unseen_by_listing(
361 toolchain: speccoder_core::collection::Toolchain,
362 source: &str,
363) -> bool {
364 use speccoder_core::collection::Toolchain;
365 let code = speccoder_core::collection::strip_comments(source);
366 match toolchain {
367 Toolchain::Python => [
368 "pytest.mark.skip",
369 "pytest.skip(",
370 "pytest.mark.xfail",
371 "unittest.skip",
372 "pytest.importorskip",
373 ]
374 .iter()
375 .any(|m| code.contains(m)),
376 Toolchain::JavaScript => [
377 ".skip(",
378 ".todo(",
379 "xit(",
380 "xdescribe(",
381 "xtest(",
382 ".skipIf(",
383 ".runIf(",
384 ]
385 .iter()
386 .any(|m| code.contains(m)),
387 Toolchain::Go => ["t.Skip", "testing.Short()"]
388 .iter()
389 .any(|m| code.contains(m)),
390 Toolchain::Rust => false,
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398 use speccoder_core::collection::Toolchain;
399
400 fn v(s: &[&str]) -> Vec<String> {
401 s.iter().map(|x| (*x).to_owned()).collect()
402 }
403
404 #[test]
405 fn only_a_check_that_runs_the_tests_counts() {
406 assert!(runs(Toolchain::Rust, &v(&["cargo", "test", "--lib"])));
407 assert!(runs(Toolchain::Rust, &v(&["cargo", "nextest", "run"])));
408 assert!(!runs(Toolchain::Rust, &v(&["cargo", "fmt", "--check"])));
409 assert!(!runs(Toolchain::Rust, &v(&["cargo", "clippy"])));
410 assert!(!runs(Toolchain::Go, &v(&["go", "vet", "./..."])));
411 assert!(runs(Toolchain::Go, &v(&["go", "test", "./..."])));
412 }
413
414 #[test]
415 fn a_cargo_check_runs_only_what_it_selects() {
416 let lib = ("lib".to_owned(), String::new());
417 let greet = ("test".to_owned(), "greet".to_owned());
418 assert!(cargo_runs(&v(&["cargo", "test", "--lib"]), "g", true, &lib));
419 assert!(!cargo_runs(
420 &v(&["cargo", "test", "--lib"]),
421 "g",
422 true,
423 &greet
424 ));
425 assert!(cargo_runs(&v(&["cargo", "test"]), "g", true, &greet));
426 assert!(!cargo_runs(
427 &v(&["cargo", "test", "-p", "other"]),
428 "g",
429 true,
430 &greet
431 ));
432 assert!(cargo_runs(
433 &v(&["cargo", "test", "--test", "greet"]),
434 "g",
435 true,
436 &greet
437 ));
438 assert!(
439 !cargo_runs(&v(&["cargo", "test"]), "g", false, &greet),
440 "not the root package, no -p"
441 );
442 }
443
444 #[test]
445 fn listings_keep_the_check_s_selection() {
446 let (l, ignored) = cargo_list(
447 &v(&["cargo", "test", "--offline", "--", "greet", "--nocapture"]),
448 "g",
449 &("test".to_owned(), "greet".to_owned()),
450 );
451 assert_eq!(
452 l,
453 v(&[
454 "cargo",
455 "test",
456 "--offline",
457 "-p",
458 "g",
459 "--test",
460 "greet",
461 "--",
462 "greet",
463 "--list",
464 "--format",
465 "terse"
466 ])
467 );
468 assert!(ignored.unwrap().ends_with(&v(&["--ignored"])));
469 assert_eq!(
470 nextest_list(&v(&[
471 "cargo",
472 "nextest",
473 "run",
474 "--profile",
475 "speccoder",
476 "--no-tests=warn",
477 "-E",
478 "test(a)"
479 ]))
480 .unwrap(),
481 v(&[
482 "cargo",
483 "nextest",
484 "list",
485 "-E",
486 "test(a)",
487 "--message-format",
488 "json"
489 ])
490 );
491 assert_eq!(
492 pytest_list(&v(&[
493 "python3",
494 "-m",
495 "pytest",
496 "tests/unit",
497 "-k",
498 "greet",
499 "--junitxml=r.xml"
500 ])),
501 v(&[
502 "python3",
503 "-m",
504 "pytest",
505 "tests/unit",
506 "-k",
507 "greet",
508 "--collect-only",
509 "-q"
510 ])
511 );
512 assert_eq!(
513 vitest_list(&v(&["npx", "vitest", "run", "src/a", "--reporter=junit"])).unwrap(),
514 v(&["npx", "vitest", "list", "src/a", "--json"])
515 );
516 assert_eq!(
517 go_list(&v(&["go", "test", "-run", "TestA", "./pkg/..."])),
518 v(&["go", "test", "-list", "TestA", "./pkg/..."])
519 );
520 assert_eq!(
521 go_list(&v(&[
522 "gotestsum",
523 "--junitfile",
524 "r.xml",
525 "--",
526 "-tags=it",
527 "./..."
528 ])),
529 v(&["go", "test", "-list", ".", "-tags=it", "./..."])
530 );
531 }
532
533 #[test]
534 fn skips_a_listing_cannot_see_are_found() {
535 assert!(skips_unseen_by_listing(
536 Toolchain::Python,
537 "@pytest.mark.skip\ndef test_a(): ..."
538 ));
539 assert!(skips_unseen_by_listing(
540 Toolchain::JavaScript,
541 "it.skip('a', () => {})"
542 ));
543 assert!(skips_unseen_by_listing(
544 Toolchain::Go,
545 "func TestA(t *testing.T) { t.Skip() }"
546 ));
547 assert!(!skips_unseen_by_listing(
548 Toolchain::Python,
549 "def test_a(): ..."
550 ));
551 }
552}