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