lean_ctx_patterns/
dotnet.rs1macro_rules! static_regex {
4 ($pattern:expr_2021) => {{
5 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
6 RE.get_or_init(|| {
7 regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
8 })
9 }};
10}
11
12fn build_summary_err_re() -> &'static regex::Regex {
13 static_regex!(r"(?i)^\s*(\d+)\s+Error\(s\)\s*$")
14}
15fn build_summary_warn_re() -> &'static regex::Regex {
16 static_regex!(r"(?i)^\s*(\d+)\s+Warning\(s\)\s*$")
17}
18fn build_result_re() -> &'static regex::Regex {
19 static_regex!(r"(?i)^(Build succeeded\.|Build FAILED\.)")
20}
21fn restored_proj_re() -> &'static regex::Regex {
22 static_regex!(r"(?i)^\s*Restored\s+(.+\.csproj[^(\n]*)(?:\s*\([^)]*\))?\s*\.?\s*$")
23}
24fn restored_pkg_re() -> &'static regex::Regex {
25 static_regex!(r"(?i)Restored\s+(\d+)\s+package")
26}
27fn test_total_re() -> &'static regex::Regex {
28 static_regex!(r"(?i)^\s*Total tests:\s*(\d+)\s*$")
29}
30fn publish_arrow_re() -> &'static regex::Regex {
31 static_regex!(r"\s+->\s+")
32}
33
34fn is_msbuild_noise(line: &str) -> bool {
35 let t = line.trim_start();
36 let tl = t.to_ascii_lowercase();
37 if tl.starts_with("microsoft (r) build engine")
38 || tl.starts_with("copyright (c) microsoft")
39 || tl.contains("version ") && tl.contains("msbuild")
40 {
41 return true;
42 }
43 if tl.starts_with("verbosity:") || tl == "build started." {
44 return true;
45 }
46 if tl.starts_with("time elapsed") && tl.contains("00:00:") {
48 return true;
49 }
50 false
51}
52
53fn is_dotnet_restore_noise(line: &str) -> bool {
54 let tl = line.trim().to_ascii_lowercase();
55 tl.starts_with("determining projects to restore")
56 || tl.contains("assets file has not changed")
57 || tl.starts_with("writing assets file")
58}
59
60fn looks_like_build_error_line(line: &str) -> bool {
61 let t = line.trim();
62 let tl = t.to_ascii_lowercase();
63 if tl.contains(": error ") || tl.starts_with("error ") {
64 return true;
65 }
66 if tl.contains("msbuild : error") || tl.contains("error msb") {
67 return true;
68 }
69 if tl.contains(": error cs") || tl.contains(": error mc") {
70 return true;
71 }
72 false
73}
74
75fn looks_like_build_warning_line(line: &str) -> bool {
76 let t = line.trim();
77 let tl = t.to_ascii_lowercase();
78 (tl.contains(": warning ") || tl.starts_with("warning ")) && !tl.contains("warning(s)")
79}
80
81pub fn compress(command: &str, output: &str) -> Option<String> {
82 let cl = command.trim().to_ascii_lowercase();
83 if !cl.starts_with("dotnet ") {
84 return None;
85 }
86
87 let sub = cl
88 .split_whitespace()
89 .nth(1)
90 .unwrap_or("")
91 .trim_start_matches('-');
92 match sub {
93 "build" | "msbuild" => return Some(compress_build(output)),
94 "test" | "vstest" => return Some(compress_test(output)),
95 "restore" => return Some(compress_restore(output)),
96 "publish" => return Some(compress_publish(output)),
97 _ => {}
98 }
99
100 None
101}
102
103fn compress_build(output: &str) -> String {
104 let mut parts = Vec::new();
105 let mut errors = Vec::new();
106 let mut warnings = Vec::new();
107 let mut result_line: Option<String> = None;
108 let mut summary_errors: Option<String> = None;
109 let mut summary_warnings: Option<String> = None;
110
111 for line in output.lines() {
112 let t = line.trim_end();
113 if t.trim().is_empty() || is_msbuild_noise(t) {
114 continue;
115 }
116 let trim = t.trim();
117 if build_result_re().is_match(trim) {
118 result_line = Some(trim.to_string());
119 continue;
120 }
121 if let Some(caps) = build_summary_err_re().captures(trim) {
122 summary_errors = Some(format!("{} Error(s)", &caps[1]));
123 continue;
124 }
125 if let Some(caps) = build_summary_warn_re().captures(trim) {
126 summary_warnings = Some(format!("{} Warning(s)", &caps[1]));
127 continue;
128 }
129 if looks_like_build_error_line(trim) {
130 errors.push(trim.to_string());
131 continue;
132 }
133 if looks_like_build_warning_line(trim) {
134 warnings.push(trim.to_string());
135 }
136 }
137
138 if let Some(r) = result_line {
139 parts.push(r);
140 }
141 if let Some(s) = summary_warnings {
142 parts.push(s);
143 }
144 if let Some(s) = summary_errors {
145 parts.push(s);
146 }
147 if !errors.is_empty() {
148 parts.push(format!("{} error lines:", errors.len()));
149 parts.extend(errors.into_iter().map(|e| format!(" {e}")));
150 }
151 if !warnings.is_empty() && warnings.len() <= 20 {
152 parts.push(format!("{} warning lines:", warnings.len()));
153 parts.extend(warnings.into_iter().map(|w| format!(" {w}")));
154 } else if !warnings.is_empty() {
155 parts.push(format!("{} warnings (omitted detail)", warnings.len()));
156 }
157
158 if parts.is_empty() {
159 compact_or_ok(output, 8)
160 } else {
161 parts.join("\n")
162 }
163}
164
165fn compress_test(output: &str) -> String {
166 let mut parts = Vec::new();
167 let mut in_failure = false;
168 let mut failure_block: Vec<String> = Vec::new();
169
170 for line in output.lines() {
171 let t = line.trim_end();
172 let trim = t.trim();
173 let tl = trim.to_ascii_lowercase();
174
175 if tl.contains("test run failed") || tl == "failed!" {
176 parts.push(trim.to_string());
177 }
178 if tl.starts_with("passed!") || tl.starts_with("failed!") {
179 parts.push(trim.to_string());
180 }
181 if test_total_re().is_match(trim) {
182 parts.push(trim.to_string());
183 }
184 if tl.starts_with("passed:")
185 || tl.starts_with("failed:")
186 || tl.starts_with("skipped:")
187 || tl.starts_with("total:")
188 {
189 parts.push(trim.to_string());
190 }
191 if tl.contains("error message:") || tl.contains("stack trace:") {
192 in_failure = true;
193 }
194 if looks_like_build_error_line(trim) && tl.contains("error") {
195 parts.push(trim.to_string());
196 }
197
198 if in_failure && !trim.is_empty() {
199 failure_block.push(trim.to_string());
200 if failure_block.len() > 40 {
201 in_failure = false;
202 }
203 }
204 }
205
206 if !failure_block.is_empty() {
207 parts.push("failure detail:".to_string());
208 parts.extend(failure_block.into_iter().take(25).map(|l| format!(" {l}")));
209 }
210
211 if parts.is_empty() {
212 compact_or_ok(output, 12)
213 } else {
214 parts.join("\n")
215 }
216}
217
218fn compress_restore(output: &str) -> String {
219 let mut restored_projects = Vec::new();
220 let mut pkg_summary: Option<String> = None;
221
222 for line in output.lines() {
223 let t = line.trim_end();
224 if t.trim().is_empty() || is_dotnet_restore_noise(t) {
225 continue;
226 }
227 let trim = t.trim();
228 if let Some(caps) = restored_proj_re().captures(trim) {
229 restored_projects.push(caps[1].trim().to_string());
230 continue;
231 }
232 if let Some(caps) = restored_pkg_re().captures(trim) {
233 pkg_summary = Some(format!("Restored {} packages (summary line)", &caps[1]));
234 }
235 if looks_like_build_error_line(trim) {
236 restored_projects.push(format!("ERR: {trim}"));
237 }
238 }
239
240 let mut parts = Vec::new();
241 if !restored_projects.is_empty() {
242 parts.push(format!("Restored {} project(s):", restored_projects.len()));
243 for p in restored_projects {
244 parts.push(format!(" {p}"));
245 }
246 }
247 if let Some(s) = pkg_summary {
248 parts.push(s);
249 }
250
251 if parts.is_empty() {
252 compact_or_ok(output, 10)
253 } else {
254 parts.join("\n")
255 }
256}
257
258fn compress_publish(output: &str) -> String {
259 let mut parts = Vec::new();
260
261 for line in output.lines() {
262 let t = line.trim_end();
263 if t.trim().is_empty() || is_msbuild_noise(t) {
264 continue;
265 }
266 let trim = t.trim();
267 if publish_arrow_re().is_match(trim) {
268 parts.push(trim.to_string());
269 continue;
270 }
271 if trim.to_ascii_lowercase().contains("published to")
272 || trim.to_ascii_lowercase().contains("output path")
273 {
274 parts.push(trim.to_string());
275 }
276 if build_result_re().is_match(trim) {
277 parts.push(trim.to_string());
278 }
279 if looks_like_build_error_line(trim) {
280 parts.push(trim.to_string());
281 }
282 }
283
284 if parts.is_empty() {
285 compact_or_ok(output, 10)
286 } else {
287 parts.join("\n")
288 }
289}
290
291fn compact_or_ok(output: &str, max: usize) -> String {
292 let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
293 if lines.is_empty() {
294 return "ok".to_string();
295 }
296 if lines.len() <= max {
297 return lines.join("\n");
298 }
299 format!(
300 "{}\n{}",
301 lines[..max].join("\n"),
302 super::elision_marker(lines.len() - max)
303 )
304}