Skip to main content

aft/compress/
find.rs

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        // Letter-only basenames so digit masking cannot collapse distinct entries.
153        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}