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
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}