1use crate::compress::generic::{strip_ansi, GenericCompressor};
2use crate::compress::listing_fold::{
3 finish_folded, fold_consecutive_runs, shape_key_for_basename, FoldEntry,
4};
5use crate::compress::{CompressionResult, Compressor};
6use std::path::Path;
7
8pub struct FindCompressor;
9
10impl Compressor for FindCompressor {
11 fn matches(&self, command: &str) -> bool {
12 command_tokens(command).any(|token| token == "find")
13 }
14
15 fn compress_with_exit_code(
16 &self,
17 command: &str,
18 output: &str,
19 exit_code: Option<i32>,
20 ) -> CompressionResult {
21 let stripped = strip_ansi(output);
22 if stripped.trim().is_empty() {
23 if matches!(exit_code, Some(code) if code != 0) {
24 return GenericCompressor::compress_output(output).into();
25 }
26 return CompressionResult::new("find: no matches");
27 }
28 let folded = compress_find_paths(command, &stripped);
29 CompressionResult::new(folded)
30 }
31}
32
33fn command_tokens(command: &str) -> impl Iterator<Item = String> + '_ {
34 command
35 .split_whitespace()
36 .map(|token| token.trim_matches(|ch| matches!(ch, '\'' | '"')))
37 .filter(|token| {
38 !matches!(
39 *token,
40 "npx" | "pnpm" | "yarn" | "bun" | "bunx" | "exec" | "-m"
41 )
42 })
43 .map(|token| {
44 token
45 .rsplit(['/', '\\'])
46 .next()
47 .unwrap_or(token)
48 .trim_end_matches(".cmd")
49 .to_string()
50 })
51}
52
53fn compress_find_paths(_command: &str, output: &str) -> String {
54 let mut entries = Vec::new();
55 for line in output.lines() {
56 let path = line.trim();
57 if path.is_empty() {
58 continue;
59 }
60 let path_obj = Path::new(path);
61 let basename = path_obj
62 .file_name()
63 .and_then(|s| s.to_str())
64 .unwrap_or(path)
65 .to_string();
66 let dir = path_obj
67 .parent()
68 .and_then(|p| p.to_str())
69 .unwrap_or("")
70 .to_string();
71 let shape_key = shape_key_for_basename(&dir, &basename);
72 entries.push(FoldEntry {
73 line: line.to_string(),
74 dir,
75 basename,
76 shape_key,
77 });
78 }
79
80 if entries.is_empty() {
81 return output.trim_end().to_string();
82 }
83
84 let folded = fold_consecutive_runs(entries);
85 finish_folded(folded)
86}
87
88pub fn build_lebench_find_fixture() -> String {
89 let mut paths = Vec::with_capacity(223);
90 for i in 1..=200u32 {
91 paths.push(format!("src/generated/client/module_{:03}.ts", i));
92 }
93 paths.insert(
94 100,
95 "src/generated/client/module_100_NEEDLE_FILE_marker.ts".to_string(),
96 );
97 for i in 1..=22u32 {
98 paths.push(format!("src/generated/client/extra_distinct_{i}.txt"));
99 }
100 paths.join("\n")
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 const NEEDLE: &str = "module_100_NEEDLE_FILE_marker.ts";
108
109 #[test]
110 fn matches_find_invocations() {
111 let c = FindCompressor;
112 assert!(c.matches("find src -name '*.ts'"));
113 assert!(!c.matches("findstr"));
114 }
115
116 #[test]
117 fn find_no_matches_shortcircuit() {
118 let c = FindCompressor;
119 let r = c.compress_with_exit_code("find . -name missing", "", None);
120 assert_eq!(r.text, "find: no matches");
121 }
122
123 #[test]
124 fn lebench_find_folds_and_preserves_needle() {
125 let input = build_lebench_find_fixture();
126 let line_count = input.lines().count();
127
128 let out = compress_find_paths("find", &input);
129 assert!(out.contains(NEEDLE), "needle must survive; got:\n{out}");
130 assert!(out.contains("module_*.ts"));
131 assert!(out.lines().count() < line_count / 2);
132 eprintln!(
133 "find fixture: {} -> {} lines",
134 line_count,
135 out.lines().count()
136 );
137 }
138
139 #[test]
140 fn small_find_listing_unchanged() {
141 let input = (1..5)
142 .map(|i| format!("/tmp/small/file_{i}.txt"))
143 .collect::<Vec<_>>()
144 .join("\n");
145 let out = compress_find_paths("find", &input);
146 assert_eq!(out.lines().count(), 4);
147 }
148
149 #[test]
150 fn distinct_outliers_over_max_lines_middle_caps_with_note() {
151 let marker = "MARKER_near_middle.txt";
152 let mut paths: Vec<String> = (0..421)
154 .map(|i| {
155 let mut suffix = String::new();
156 let mut n = i;
157 for _ in 0..8 {
158 suffix.push((b'a' + (n % 26) as u8) as char);
159 n /= 26;
160 }
161 format!("/proj/outlier_{suffix}.rs")
162 })
163 .collect();
164 paths.insert(410, format!("/proj/{marker}"));
165 let input = paths.join("\n");
166 let out = compress_find_paths("find", &input);
167 assert!(
168 out.contains("entries omitted"),
169 "last-resort middle cap must be noted: {out}"
170 );
171 assert!(
172 out.contains(marker),
173 "marker within kept head/tail should survive: {out}"
174 );
175 }
176}