Skip to main content

aft/compress/
cargo.rs

1use crate::compress::generic::GenericCompressor;
2use crate::compress::Compressor;
3
4pub struct CargoCompressor;
5
6impl Compressor for CargoCompressor {
7    fn matches(&self, command: &str) -> bool {
8        command
9            .split_whitespace()
10            .next()
11            .is_some_and(|head| head == "cargo")
12    }
13
14    fn compress(&self, command: &str, output: &str) -> String {
15        match cargo_subcommand(command).as_deref() {
16            Some("build" | "check" | "clippy") => compress_build_like(output),
17            Some("test") => compress_test(output),
18            _ => GenericCompressor::compress_output(output),
19        }
20    }
21
22    fn matches_output(&self, output: &str) -> bool {
23        output.lines().any(is_cargo_test_signature_line)
24    }
25
26    fn compress_output_match(&self, output: &str) -> String {
27        compress_test(output)
28    }
29}
30
31fn is_cargo_test_signature_line(line: &str) -> bool {
32    line.starts_with("test result:")
33        || line.starts_with("failures:")
34        || (line.starts_with("---- ") && line.ends_with(" stdout ----"))
35}
36
37fn cargo_subcommand(command: &str) -> Option<String> {
38    let mut seen_cargo = false;
39    for token in command.split_whitespace() {
40        if !seen_cargo {
41            if token == "cargo" {
42                seen_cargo = true;
43            }
44            continue;
45        }
46        if token.starts_with('-') {
47            continue;
48        }
49        return Some(token.to_string());
50    }
51    None
52}
53
54fn compress_build_like(output: &str) -> String {
55    let lines: Vec<&str> = output.lines().collect();
56    let has_diagnostic = lines
57        .iter()
58        .any(|line| is_warning_or_error(line) || line.trim_start().starts_with("error["));
59
60    if !has_diagnostic {
61        return output.trim_end().to_string();
62    }
63
64    let mut result = Vec::new();
65    let mut index = 0usize;
66
67    while index < lines.len() {
68        let line = lines[index];
69        if is_ignored_progress(line) {
70            index += 1;
71            continue;
72        }
73
74        if is_warning_or_error(line) || line.trim_start().starts_with("error[") {
75            let start = index;
76            index += 1;
77            while index < lines.len() && !starts_next_build_message(lines[index]) {
78                index += 1;
79            }
80            result.extend(lines[start..index].iter().map(|line| (*line).to_string()));
81            continue;
82        }
83
84        if is_final_cargo_summary(line) {
85            result.push(line.to_string());
86        }
87        index += 1;
88    }
89
90    trim_trailing_lines(&result.join("\n"))
91}
92
93fn starts_next_build_message(line: &str) -> bool {
94    is_ignored_progress(line)
95        || is_warning_or_error(line)
96        || line.trim_start().starts_with("error[")
97        || is_final_cargo_summary(line)
98}
99
100fn is_warning_or_error(line: &str) -> bool {
101    let trimmed = line.trim_start();
102    trimmed.starts_with("warning:") || trimmed.starts_with("error:")
103}
104
105fn is_ignored_progress(line: &str) -> bool {
106    let trimmed = line.trim_start();
107    trimmed == "Updating crates.io index" || is_compiling_line(trimmed)
108}
109
110fn is_compiling_line(trimmed: &str) -> bool {
111    let Some(rest) = trimmed.strip_prefix("Compiling ") else {
112        return false;
113    };
114    let mut parts = rest.split_whitespace();
115    let _crate_name = parts.next();
116    parts.next().is_some_and(|part| {
117        part.strip_prefix('v').is_some_and(|version| {
118            version
119                .chars()
120                .all(|char| char.is_ascii_digit() || char == '.')
121        })
122    })
123}
124
125fn is_final_cargo_summary(line: &str) -> bool {
126    let trimmed = line.trim_start();
127    trimmed.starts_with("Finished ")
128        || trimmed.starts_with("error: could not compile")
129        || trimmed.starts_with("test result:")
130}
131
132fn compress_test(output: &str) -> String {
133    let lines: Vec<&str> = output.lines().collect();
134    let has_failures = lines.iter().any(|line| line.trim() == "failures:");
135    if !has_failures {
136        let result: Vec<String> = lines
137            .iter()
138            .filter(|line| {
139                let trimmed = line.trim_start();
140                trimmed.starts_with("running ")
141                    || trimmed.starts_with("test result:")
142                    || is_final_cargo_summary(trimmed)
143            })
144            .map(|line| (*line).to_string())
145            .collect();
146        return trim_trailing_lines(&result.join("\n"));
147    }
148
149    let mut result = Vec::new();
150    let mut index = 0usize;
151    while index < lines.len() {
152        let line = lines[index];
153        let trimmed = line.trim_start();
154        if trimmed.starts_with("running ") || trimmed.starts_with("test result:") {
155            result.push(line.to_string());
156            index += 1;
157            continue;
158        }
159
160        if trimmed == "failures:" {
161            result.extend(lines[index..].iter().map(|line| (*line).to_string()));
162            break;
163        }
164
165        if line.starts_with("---- ") {
166            while index < lines.len() {
167                result.push(lines[index].to_string());
168                index += 1;
169                if index < lines.len()
170                    && (lines[index].trim_start().starts_with("test result:")
171                        || lines[index].trim() == "failures:")
172                {
173                    break;
174                }
175            }
176            continue;
177        }
178
179        index += 1;
180    }
181
182    trim_trailing_lines(&result.join("\n"))
183}
184
185fn trim_trailing_lines(input: &str) -> String {
186    input
187        .lines()
188        .map(str::trim_end)
189        .collect::<Vec<_>>()
190        .join("\n")
191}