1pub mod biome;
19pub mod builtin_filters;
20pub mod bun;
21pub mod cargo;
22pub mod eslint;
23pub mod generic;
24pub mod git;
25pub mod go;
26pub mod mypy;
27pub mod next;
28pub mod npm;
29pub mod playwright;
30pub mod pnpm;
31pub mod prettier;
32pub mod pytest;
33pub mod ruff;
34pub mod toml_filter;
35pub mod trust;
36pub mod tsc;
37pub mod vitest;
38
39use crate::context::AppContext;
40use crate::harness::Harness;
41use biome::BiomeCompressor;
42use bun::BunCompressor;
43use cargo::CargoCompressor;
44use eslint::EslintCompressor;
45use generic::{strip_ansi, GenericCompressor};
46use git::GitCompressor;
47use go::{GoCompressor, GolangciLintCompressor};
48use mypy::MypyCompressor;
49use next::NextCompressor;
50use npm::NpmCompressor;
51use playwright::PlaywrightCompressor;
52use pnpm::PnpmCompressor;
53use prettier::PrettierCompressor;
54use pytest::PytestCompressor;
55use ruff::RuffCompressor;
56use std::fs;
57use std::path::{Path, PathBuf};
58use std::sync::{Arc, RwLock};
59use toml_filter::{apply_filter, FilterRegistry};
60use tsc::TscCompressor;
61use vitest::VitestCompressor;
62
63pub type SharedFilterRegistry = Arc<RwLock<FilterRegistry>>;
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum Specificity {
86 Specific,
87 PackageManager,
88}
89
90pub trait Compressor {
93 fn matches(&self, command: &str) -> bool;
96
97 fn compress(&self, command: &str, output: &str) -> String;
99
100 fn specificity(&self) -> Specificity {
101 Specificity::Specific
102 }
103}
104
105pub fn compress(command: &str, output: String, ctx: &AppContext) -> String {
111 if !ctx.config().experimental_bash_compress {
112 return output;
113 }
114 let registry_handle = ctx.shared_filter_registry();
115 let guard = match registry_handle.read() {
116 Ok(g) => g,
117 Err(poisoned) => poisoned.into_inner(),
118 };
119 compress_with_registry(command, &output, &guard)
120}
121
122pub fn compress_with_registry(command: &str, output: &str, registry: &FilterRegistry) -> String {
128 let stripped_for_generic = strip_ansi(output);
129
130 let normalized = normalize_command_for_dispatch(command);
136 let dispatch_cmd = normalized.as_deref().unwrap_or(command);
137
138 let compressors: [&dyn Compressor; 17] = [
139 &GitCompressor,
140 &CargoCompressor,
141 &TscCompressor,
142 &NpmCompressor,
143 &BunCompressor,
144 &PnpmCompressor,
145 &PytestCompressor,
146 &EslintCompressor,
147 &VitestCompressor,
148 &BiomeCompressor,
149 &PrettierCompressor,
150 &RuffCompressor,
151 &MypyCompressor,
152 &GoCompressor,
153 &GolangciLintCompressor,
154 &PlaywrightCompressor,
155 &NextCompressor,
156 ];
157
158 for compressor in compressors
160 .iter()
161 .filter(|c| c.specificity() == Specificity::Specific)
162 {
163 if compressor.matches(dispatch_cmd) {
164 return compressor.compress(dispatch_cmd, &stripped_for_generic);
165 }
166 }
167
168 for compressor in compressors
170 .iter()
171 .filter(|c| c.specificity() == Specificity::PackageManager)
172 {
173 if compressor.matches(dispatch_cmd) {
174 return compressor.compress(dispatch_cmd, &stripped_for_generic);
175 }
176 }
177
178 if let Some(filter) = registry.lookup(dispatch_cmd) {
181 return apply_filter(filter, output);
182 }
183
184 GenericCompressor.compress(command, &stripped_for_generic)
186}
187
188pub fn build_registry_for_context(ctx: &AppContext) -> FilterRegistry {
197 let harness = ctx.harness.borrow().unwrap_or(Harness::Opencode);
198 let config = ctx.config();
199 let storage_dir = config.storage_dir.clone();
200 let project_root = config.project_root.clone();
201 drop(config);
202
203 let user_dir = storage_dir.as_ref().map(|dir| {
204 repair_legacy_user_filter_dir(dir, harness);
205 user_filter_dir(dir, harness)
206 });
207 let project_dir = match (project_root.as_ref(), storage_dir.as_ref()) {
208 (Some(root), Some(storage)) => {
209 if trust::is_project_trusted(Some(storage), root) {
210 Some(root.join(".aft").join("filters"))
211 } else {
212 None
213 }
214 }
215 _ => None,
216 };
217
218 toml_filter::build_registry(
219 builtin_filters::ALL,
220 user_dir.as_deref(),
221 project_dir.as_deref(),
222 )
223}
224
225pub fn normalize_command_for_dispatch(command: &str) -> Option<String> {
249 let trimmed = command.trim_start();
250 if trimmed.is_empty() {
251 return None;
252 }
253
254 let (open_paren, after_paren) = if let Some(rest) = trimmed.strip_prefix('(') {
256 (true, rest.trim_start())
257 } else {
258 (false, trimmed)
259 };
260
261 let mut current = after_paren.to_string();
262 let mut changed = open_paren;
263
264 loop {
266 if let Some(stripped) = strip_leading_assignment_prefix(¤t) {
270 current = stripped;
271 changed = true;
272 continue;
273 }
274
275 let head: String = current.split_whitespace().next().unwrap_or("").to_string();
276
277 if head == "cd" {
279 if let Some(stripped) = strip_cd_prefix(¤t) {
282 current = stripped;
283 changed = true;
284 continue;
285 }
286 }
287
288 if head == "env" {
290 if let Some(stripped) = strip_env_prefix(¤t) {
291 current = stripped;
292 changed = true;
293 continue;
294 }
295 }
296
297 if head == "timeout" {
299 if let Some(stripped) = strip_timeout_prefix(¤t) {
300 current = stripped;
301 changed = true;
302 continue;
303 }
304 }
305
306 if head == "nohup" {
308 if let Some(rest) = current.strip_prefix("nohup").and_then(|s| {
309 let trimmed = s.trim_start();
310 if trimmed.is_empty() {
311 None
312 } else {
313 Some(trimmed.to_string())
314 }
315 }) {
316 current = rest;
317 changed = true;
318 continue;
319 }
320 }
321
322 break;
323 }
324
325 if changed {
326 Some(current)
327 } else {
328 None
329 }
330}
331
332fn strip_cd_prefix(command: &str) -> Option<String> {
333 let bytes = command.as_bytes();
335 let mut in_single = false;
336 let mut in_double = false;
337 let mut i = 0;
338 while i < bytes.len() {
339 let ch = bytes[i] as char;
340 if !in_double && ch == '\'' {
341 in_single = !in_single;
342 } else if !in_single && ch == '"' {
343 in_double = !in_double;
344 } else if !in_single && !in_double {
345 if ch == '&' && i + 1 < bytes.len() && bytes[i + 1] as char == '&' {
346 let rest = command[i + 2..].trim_start();
347 if rest.is_empty() {
348 return None;
349 }
350 return Some(rest.to_string());
351 }
352 if ch == ';' {
353 let rest = command[i + 1..].trim_start();
354 if rest.is_empty() {
355 return None;
356 }
357 return Some(rest.to_string());
358 }
359 }
360 i += 1;
361 }
362 None
363}
364
365fn strip_env_prefix(command: &str) -> Option<String> {
366 let rest = command.strip_prefix("env")?.trim_start();
368 strip_leading_assignment_prefix(rest)
369}
370
371fn strip_leading_assignment_prefix(command: &str) -> Option<String> {
372 let mut index = 0usize;
373 let mut consumed_assignment = false;
374
375 loop {
376 index = skip_whitespace(command, index);
377 if index >= command.len() {
378 break;
379 }
380
381 let word_end = shell_word_end(command, index)?;
382 if word_end == index {
383 break;
384 }
385
386 let word = &command[index..word_end];
387 if !is_env_assignment(word) {
388 break;
389 }
390
391 consumed_assignment = true;
392 index = word_end;
393 }
394
395 if !consumed_assignment {
396 return None;
397 }
398
399 let after = command[index..].trim_start();
400 if after.is_empty() {
401 None
402 } else {
403 Some(after.to_string())
404 }
405}
406
407fn skip_whitespace(input: &str, mut index: usize) -> usize {
408 while index < input.len() {
409 let Some(ch) = input[index..].chars().next() else {
410 break;
411 };
412 if !ch.is_whitespace() {
413 break;
414 }
415 index += ch.len_utf8();
416 }
417 index
418}
419
420fn shell_word_end(command: &str, start: usize) -> Option<usize> {
421 let mut in_single = false;
422 let mut in_double = false;
423 let mut escaped = false;
424
425 for (offset, ch) in command[start..].char_indices() {
426 let index = start + offset;
427
428 if escaped {
429 escaped = false;
430 continue;
431 }
432
433 if ch == '\\' && !in_single {
434 escaped = true;
435 continue;
436 }
437
438 if ch == '\'' && !in_double {
439 in_single = !in_single;
440 continue;
441 }
442
443 if ch == '"' && !in_single {
444 in_double = !in_double;
445 continue;
446 }
447
448 if !in_single && !in_double && (ch.is_whitespace() || matches!(ch, ';' | '&' | '|')) {
449 return Some(index);
450 }
451 }
452
453 if in_single || in_double || escaped {
454 None
455 } else {
456 Some(command.len())
457 }
458}
459
460fn is_env_assignment(token: &str) -> bool {
461 if token.starts_with('-') {
462 return false;
463 }
464 let Some((name, _value)) = token.split_once('=') else {
465 return false;
466 };
467 let mut chars = name.chars();
468 let Some(first) = chars.next() else {
469 return false;
470 };
471 (first.is_ascii_alphabetic() || first == '_')
472 && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
473}
474
475fn strip_timeout_prefix(command: &str) -> Option<String> {
476 let rest = command.strip_prefix("timeout")?.trim_start();
477 let mut iter = rest.splitn(2, char::is_whitespace);
479 let duration = iter.next()?;
480 let after = iter.next()?.trim_start();
481 if after.is_empty() || !looks_like_duration(duration) {
482 return None;
483 }
484 Some(after.to_string())
485}
486
487fn looks_like_duration(token: &str) -> bool {
488 if token.is_empty() {
489 return false;
490 }
491 let mut chars = token.chars().peekable();
492 let mut saw_digit = false;
493 while let Some(&ch) = chars.peek() {
494 if ch.is_ascii_digit() {
495 saw_digit = true;
496 chars.next();
497 } else {
498 break;
499 }
500 }
501 if !saw_digit {
502 return false;
503 }
504 match chars.next() {
505 None => true,
506 Some(unit) => matches!(unit, 's' | 'm' | 'h' | 'd') && chars.next().is_none(),
507 }
508}
509
510pub fn user_filter_dir(storage_dir: &Path, harness: Harness) -> PathBuf {
513 storage_dir.join(harness.as_str()).join("filters")
514}
515
516fn legacy_user_filter_dir(storage_dir: &Path) -> PathBuf {
517 storage_dir.join("filters")
518}
519
520pub(crate) fn repair_legacy_user_filter_dir(storage_dir: &Path, harness: Harness) {
524 let legacy_dir = legacy_user_filter_dir(storage_dir);
525 if !legacy_dir.exists() {
526 return;
527 }
528
529 let entries = match fs::read_dir(&legacy_dir) {
530 Ok(entries) => entries.filter_map(Result::ok).collect::<Vec<_>>(),
531 Err(_) => return,
532 };
533 if entries.is_empty() {
534 let _ = fs::remove_dir(&legacy_dir);
535 return;
536 }
537
538 let harness_dir = user_filter_dir(storage_dir, harness);
539 if fs::create_dir_all(&harness_dir).is_err() {
540 return;
541 }
542
543 for entry in entries {
544 let target = harness_dir.join(entry.file_name());
545 if target.exists() {
546 continue;
547 }
548 let _ = fs::rename(entry.path(), target);
549 }
550
551 if fs::read_dir(&legacy_dir)
552 .map(|mut entries| entries.next().is_none())
553 .unwrap_or(false)
554 {
555 let _ = fs::remove_dir(&legacy_dir);
556 }
557}
558
559pub fn project_filter_dir(project_root: &Path) -> PathBuf {
563 project_root.join(".aft").join("filters")
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569
570 #[test]
571 fn user_and_project_filter_dir_helpers() {
572 let storage = Path::new("/tmp/aft-storage");
573 assert_eq!(
574 user_filter_dir(storage, Harness::Opencode),
575 Path::new("/tmp/aft-storage/opencode/filters")
576 );
577
578 let project = Path::new("/repo");
579 assert_eq!(project_filter_dir(project), Path::new("/repo/.aft/filters"));
580 }
581
582 #[test]
583 fn repair_legacy_user_filter_dir_moves_root_filters_without_overwrite() {
584 let temp = tempfile::tempdir().unwrap();
585 let storage = temp.path();
586 fs::create_dir_all(storage.join("filters")).unwrap();
587 fs::create_dir_all(storage.join("opencode/filters")).unwrap();
588 fs::write(storage.join("filters/root-only.toml"), "root").unwrap();
589 fs::write(storage.join("filters/collides.toml"), "root").unwrap();
590 fs::write(storage.join("opencode/filters/collides.toml"), "harness").unwrap();
591
592 repair_legacy_user_filter_dir(storage, Harness::Opencode);
593
594 assert_eq!(
595 fs::read_to_string(storage.join("opencode/filters/root-only.toml")).unwrap(),
596 "root"
597 );
598 assert_eq!(
599 fs::read_to_string(storage.join("opencode/filters/collides.toml")).unwrap(),
600 "harness"
601 );
602 assert_eq!(
603 fs::read_to_string(storage.join("filters/collides.toml")).unwrap(),
604 "root"
605 );
606 assert!(!storage.join("filters/root-only.toml").exists());
607 }
608}
609
610#[cfg(test)]
611mod dispatch_specificity_tests {
612 use super::*;
613 use crate::compress::toml_filter::FilterRegistry;
614
615 fn empty_registry() -> FilterRegistry {
616 FilterRegistry::default()
617 }
618
619 fn dispatch(cmd: &str, output: &str) -> String {
624 compress_with_registry(cmd, output, &empty_registry())
625 }
626
627 #[test]
628 fn bun_run_vitest_routes_to_vitest_not_generic() {
629 let output = "Test Files 1 passed (1)\n Tests 4 passed (4)\n Start at 10:00:00\n Duration 120ms\n";
634 let compressed = dispatch("bun run vitest", output);
635 assert!(compressed.contains("Tests") || compressed.contains("Test Files"));
637 }
638
639 #[test]
640 fn npm_test_routes_to_vitest_when_output_is_vitest_shaped() {
641 let output = "added 100 packages, removed 2 packages\n";
648 let _compressed = dispatch("npm test", output);
649 }
653
654 #[test]
655 fn bun_run_vitest_token_match_wins_over_bun_head_match() {
656 let output = "PASS src/a.test.ts (1)\n PASS src/b.test.ts (1)\nTest Files 2 passed (2)\n Tests 4 passed (4)\n";
659 let compressed = dispatch("bun run vitest run", output);
660 assert!(compressed.contains("Test Files") || compressed.contains("PASS"));
662 }
663
664 #[test]
665 fn bunx_jest_routes_to_vitest_module() {
666 let output = "PASS src/foo.test.js (1.2s)\nTest Suites: 1 passed, 1 total\nTests: 3 passed, 3 total\n";
667 let compressed = dispatch("bunx jest --json", output);
668 assert!(compressed.contains("Tests:") && compressed.contains("Test Suites"));
669 }
670
671 #[test]
672 fn pnpm_run_vitest_routes_to_vitest() {
673 let output = "Test Files 1 passed (1)\n Tests 10 passed (10)\n";
674 let compressed = dispatch("pnpm run vitest", output);
675 assert!(compressed.contains("Tests") || compressed.contains("Test Files"));
676 }
677
678 #[test]
679 fn npx_eslint_routes_to_eslint_not_generic() {
680 let output = "\n/tmp/a.js\n 1:1 error 'foo' is defined but never used no-unused-vars\n\n✖ 1 problem (1 error, 0 warnings)\n";
681 let compressed = dispatch("npx eslint .", output);
682 assert!(compressed.contains("no-unused-vars") || compressed.contains("✖"));
684 }
685
686 #[test]
687 fn npm_run_lint_with_eslint_token_routes_to_eslint() {
688 let output = "> my-project@1.0.0 lint\n> eslint .\n\nAll good.\n";
693 let _compressed = dispatch("npm run lint", output);
694 }
698
699 #[test]
700 fn bun_test_still_routes_to_bun_test_compressor() {
701 let output = "bun test v1.3.14\n\nsrc/foo.test.ts:\n(pass) my test [0.5ms]\n\n 1 pass\n 0 fail\n 1 expect() calls\nRan 1 tests across 1 files. [1.00ms]\n";
709 let compressed = dispatch("bun test", output);
710 assert!(compressed.contains("(pass)") || compressed.contains("1 pass"));
711 }
712
713 #[test]
714 fn bunx_vitest_routes_to_vitest() {
715 let output = "Test Files 1 passed (1)\n Tests 3 passed (3)\n";
716 let compressed = dispatch("bunx vitest run", output);
717 assert!(compressed.contains("Tests") || compressed.contains("Test Files"));
718 }
719
720 #[test]
721 fn cargo_test_still_routes_to_cargo() {
722 let output = "running 5 tests\ntest foo ... ok\ntest bar ... FAILED\n\nfailures:\n\ntest result: FAILED. 4 passed; 1 failed\n";
725 let compressed = dispatch("cargo test", output);
726 assert!(compressed.contains("failed") || compressed.contains("FAILED"));
728 }
729
730 #[test]
731 fn git_status_still_routes_to_git() {
732 let output =
734 "On branch main\nYour branch is up to date.\n\nnothing to commit, working tree clean\n";
735 let compressed = dispatch("git status", output);
736 assert!(compressed.contains("branch") || compressed.contains("clean"));
737 }
738
739 #[test]
740 fn pnpm_install_still_routes_to_pnpm() {
741 let output = "Progress: resolved 100, downloaded 50, added 50\nAdded 50 packages\n";
743 let compressed = dispatch("pnpm install", output);
744 assert!(compressed.contains("Added") || compressed.contains("Progress"));
746 }
747}
748
749#[cfg(test)]
750mod normalize_command_tests {
751 use super::*;
752
753 #[test]
754 fn passes_bare_commands_unchanged() {
755 assert_eq!(normalize_command_for_dispatch("bun test"), None);
756 assert_eq!(normalize_command_for_dispatch("cargo build"), None);
757 assert_eq!(normalize_command_for_dispatch("git status"), None);
758 }
759
760 #[test]
761 fn strips_cd_and_amp_prefix() {
762 assert_eq!(
763 normalize_command_for_dispatch("cd /repo && bun test").as_deref(),
764 Some("bun test")
765 );
766 assert_eq!(
767 normalize_command_for_dispatch("cd /repo/packages/aft && cargo test --release")
768 .as_deref(),
769 Some("cargo test --release")
770 );
771 }
772
773 #[test]
774 fn strips_cd_and_semicolon_prefix() {
775 assert_eq!(
776 normalize_command_for_dispatch("cd /repo; bun test").as_deref(),
777 Some("bun test")
778 );
779 }
780
781 #[test]
782 fn strips_cd_with_quoted_path() {
783 assert_eq!(
784 normalize_command_for_dispatch("cd \"/path with space\" && npm install").as_deref(),
785 Some("npm install")
786 );
787 }
788
789 #[test]
790 fn strips_env_assignments() {
791 assert_eq!(
792 normalize_command_for_dispatch("env FOO=bar npm install").as_deref(),
793 Some("npm install")
794 );
795 assert_eq!(
796 normalize_command_for_dispatch("env FOO=bar BAZ=qux RUST_LOG=info cargo test")
797 .as_deref(),
798 Some("cargo test")
799 );
800 }
801
802 #[test]
803 fn strips_bare_assignment_prefixes() {
804 assert_eq!(
805 normalize_command_for_dispatch("NODE_ENV=production npm install").as_deref(),
806 Some("npm install")
807 );
808 assert_eq!(
809 normalize_command_for_dispatch("FOO=1 BAR=2 cargo test").as_deref(),
810 Some("cargo test")
811 );
812 assert_eq!(
813 normalize_command_for_dispatch("RUSTFLAGS='-C debug' cargo build").as_deref(),
814 Some("cargo build")
815 );
816 }
817
818 #[test]
819 fn does_not_strip_later_assignment_arguments() {
820 assert_eq!(normalize_command_for_dispatch("npm install foo=bar"), None);
821 }
822
823 #[test]
824 fn env_without_assignments_returns_none() {
825 assert_eq!(
827 normalize_command_for_dispatch("env npm install").as_deref(),
828 None
829 );
830 }
831
832 #[test]
833 fn strips_timeout_prefix() {
834 assert_eq!(
835 normalize_command_for_dispatch("timeout 30 cargo test").as_deref(),
836 Some("cargo test")
837 );
838 assert_eq!(
839 normalize_command_for_dispatch("timeout 5m bun test").as_deref(),
840 Some("bun test")
841 );
842 }
843
844 #[test]
845 fn strips_nohup_prefix() {
846 assert_eq!(
847 normalize_command_for_dispatch("nohup ./long-running-script.sh").as_deref(),
848 Some("./long-running-script.sh")
849 );
850 }
851
852 #[test]
853 fn strips_paren_then_cd_and_amp() {
854 assert_eq!(
855 normalize_command_for_dispatch("(cd /repo && bun test").as_deref(),
856 Some("bun test")
857 );
858 }
859
860 #[test]
861 fn chains_multiple_prefixes() {
862 assert_eq!(
864 normalize_command_for_dispatch("env FOO=bar timeout 30 cargo test").as_deref(),
865 Some("cargo test")
866 );
867 assert_eq!(
869 normalize_command_for_dispatch("cd /repo && env FOO=bar npm install").as_deref(),
870 Some("npm install")
871 );
872 }
873
874 fn empty_registry() -> FilterRegistry {
877 FilterRegistry::default()
878 }
879
880 #[test]
881 fn cd_prefix_bun_test_still_routes_to_bun_test() {
882 let output = "bun test v1.3.14\n\nsrc/a.test.ts:\n(pass) ok [0.1ms]\n\n 1 pass\n 0 fail\n 1 expect() calls\nRan 1 tests across 1 files. [1.00ms]\n";
883 let compressed = compress_with_registry("cd /repo && bun test", output, &empty_registry());
884 assert!(compressed.contains("(pass)") || compressed.contains("1 pass"));
889 }
890
891 #[test]
892 fn cd_prefix_cargo_test_still_routes_to_cargo() {
893 let output = "running 5 tests\ntest foo ... ok\ntest bar ... FAILED\n\nfailures:\n\ntest result: FAILED. 4 passed; 1 failed\n";
894 let compressed =
895 compress_with_registry("cd /repo && cargo test", output, &empty_registry());
896 assert!(compressed.contains("FAILED") || compressed.contains("failed"));
897 }
898
899 #[test]
900 fn env_prefix_npm_install_still_routes_to_npm() {
901 let output = "added 50 packages, and audited 100 packages in 3s\n";
902 let compressed = compress_with_registry(
903 "env NODE_ENV=production npm install",
904 output,
905 &empty_registry(),
906 );
907 assert!(compressed.contains("added") || compressed.contains("audited"));
909 }
910
911 #[test]
912 fn bare_assignment_prefix_npm_install_routes_to_npm() {
913 let output = "npm http fetch GET 200 https://registry.npmjs.org/foo 123ms\nnpm WARN deprecated old-pkg@1.0.0: use new-pkg instead\n\nadded 42 packages in 2s\n\naudited 100 packages in 2s\n\nfound 0 vulnerabilities\n";
914 let compressed =
915 compress_with_registry("NODE_ENV=production npm install", output, &empty_registry());
916 assert!(!compressed.contains("npm http fetch"));
917 assert!(compressed.contains("audited 100 packages"));
918 }
919
920 #[test]
921 fn bare_assignment_prefix_cargo_test_routes_to_cargo() {
922 let output = "running 1 test\ntest foo ... ok\n\ntest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n";
923 let compressed =
924 compress_with_registry("FOO=1 BAR=2 cargo test", output, &empty_registry());
925 assert!(compressed.contains("running 1 test"));
926 assert!(compressed.contains("test result: ok"));
927 assert!(!compressed.contains("test foo ... ok"));
928 }
929
930 #[test]
931 fn quoted_assignment_prefix_cargo_build_routes_to_cargo() {
932 let output = " Compiling foo v0.1.0\nwarning: unused variable: `x`\n --> src/lib.rs:1:9\n |\n1 | let x = 1;\n | ^ help: if this is intentional, prefix it with an underscore: `_x`\n\n Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s\n";
933 let compressed = compress_with_registry(
934 "RUSTFLAGS='-C debug' cargo build",
935 output,
936 &empty_registry(),
937 );
938 assert!(!compressed.contains("Compiling foo"));
939 assert!(compressed.contains("warning: unused variable"));
940 assert!(compressed.contains("Finished `dev` profile"));
941 }
942
943 #[test]
944 fn timeout_prefix_cargo_build_still_routes_to_cargo() {
945 let output =
946 " Compiling foo v0.1.0\n Finished `dev` profile [unoptimized] target(s) in 5s\n";
947 let compressed =
948 compress_with_registry("timeout 30 cargo build", output, &empty_registry());
949 assert!(compressed.contains("Compiling") || compressed.contains("Finished"));
951 }
952}