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}