lean_ctx/core/patterns/
ruby.rs1pub fn compress(cmd_lower: &str, output: &str) -> Option<String> {
2 if cmd_lower.contains("rubocop") {
3 return compress_rubocop(output);
4 }
5 if cmd_lower.contains("bundle install") || cmd_lower.contains("bundle update") {
6 return compress_bundle(output);
7 }
8 if cmd_lower.contains("rake test") || cmd_lower.contains("rails test") {
9 return compress_minitest(output);
10 }
11 None
12}
13
14fn compress_rubocop(output: &str) -> Option<String> {
15 let mut offenses = Vec::new();
16 let mut files_inspected = 0u32;
17 let mut total_offenses = 0u32;
18
19 for line in output.lines() {
20 let trimmed = line.trim();
21 if trimmed.contains("files inspected") {
22 for word in trimmed.split_whitespace() {
23 if let Ok(n) = word.parse::<u32>() {
24 files_inspected = n;
25 break;
26 }
27 }
28 if let Some(pos) = trimmed.find("offense") {
29 let before = &trimmed[..pos];
30 for word in before.split(", ").last().unwrap_or("").split_whitespace() {
31 if let Ok(n) = word.parse::<u32>() {
32 total_offenses = n;
33 }
34 }
35 }
36 } else if trimmed.contains(": C:")
37 || trimmed.contains(": W:")
38 || trimmed.contains(": E:")
39 || trimmed.contains(": F:")
40 {
41 offenses.push(trimmed.to_string());
42 }
43 }
44
45 if files_inspected == 0 && offenses.is_empty() {
46 return None;
47 }
48
49 let mut result = format!("rubocop: {files_inspected} files, {total_offenses} offenses");
50
51 if total_offenses == 0 {
52 result.push_str(" (clean)");
53 return Some(result);
54 }
55
56 let grouped = group_by_cop(&offenses);
57 for (cop, count) in grouped.iter().take(10) {
58 result.push_str(&format!("\n {cop}: {count}x"));
59 }
60
61 if offenses.len() > 10 {
62 result.push_str(&format!("\n ... +{} more", offenses.len() - 10));
63 }
64
65 Some(result)
66}
67
68fn group_by_cop(offenses: &[String]) -> Vec<(String, usize)> {
69 let mut map = std::collections::HashMap::new();
70 for offense in offenses {
71 let cop = offense
72 .split('[')
73 .next_back()
74 .and_then(|s| s.strip_suffix(']'))
75 .unwrap_or("unknown")
76 .to_string();
77 *map.entry(cop).or_insert(0usize) += 1;
78 }
79 let mut sorted: Vec<_> = map.into_iter().collect();
80 sorted.sort_by(|a, b| b.1.cmp(&a.1));
81 sorted
82}
83
84fn compress_bundle(output: &str) -> Option<String> {
85 let mut installed = 0u32;
86 let mut using = 0u32;
87
88 for line in output.lines() {
89 let trimmed = line.trim();
90 if trimmed.starts_with("Installing ") {
91 installed += 1;
92 } else if trimmed.starts_with("Using ") {
93 using += 1;
94 }
95 }
96
97 if installed == 0 && using == 0 {
98 return None;
99 }
100
101 let mut result = String::from("bundle: ");
102 if installed > 0 {
103 result.push_str(&format!("{installed} installed"));
104 }
105 if using > 0 {
106 if installed > 0 {
107 result.push_str(", ");
108 }
109 result.push_str(&format!("{using} using (cached)"));
110 }
111
112 for line in output.lines().rev().take(3) {
113 let trimmed = line.trim();
114 if trimmed.starts_with("Bundle complete") || trimmed.starts_with("Bundled gems") {
115 result.push_str(&format!("\n {trimmed}"));
116 break;
117 }
118 }
119
120 Some(result)
121}
122
123fn compress_minitest(output: &str) -> Option<String> {
124 let mut total = 0u32;
125 let mut failures = 0u32;
126 let mut errors = 0u32;
127 let mut skips = 0u32;
128 let mut time = String::new();
129 let mut failure_details = Vec::new();
130
131 for line in output.lines() {
132 let trimmed = line.trim();
133 if trimmed.contains("runs,") && trimmed.contains("assertions") {
134 for part in trimmed.split(", ") {
135 let part = part.trim();
136 if part.ends_with("runs") {
137 if let Some(n) = part.split_whitespace().next().and_then(|w| w.parse().ok()) {
138 total = n;
139 }
140 } else if part.ends_with("failures") {
141 if let Some(n) = part.split_whitespace().next().and_then(|w| w.parse().ok()) {
142 failures = n;
143 }
144 } else if part.ends_with("errors") {
145 if let Some(n) = part.split_whitespace().next().and_then(|w| w.parse().ok()) {
146 errors = n;
147 }
148 } else if part.ends_with("skips") {
149 if let Some(n) = part.split_whitespace().next().and_then(|w| w.parse().ok()) {
150 skips = n;
151 }
152 }
153 }
154 if let Some(pos) = trimmed.find(" in ") {
155 time = trimmed[pos + 4..]
156 .split(',')
157 .next()
158 .unwrap_or("")
159 .trim()
160 .to_string();
161 }
162 }
163 if trimmed.starts_with("Failure:") || trimmed.starts_with("Error:") {
164 failure_details.push(trimmed.to_string());
165 }
166 }
167
168 if total == 0 {
169 return None;
170 }
171
172 let passed = total
173 .saturating_sub(failures)
174 .saturating_sub(errors)
175 .saturating_sub(skips);
176 let mut result = format!("minitest: {passed} passed");
177 if failures > 0 {
178 result.push_str(&format!(", {failures} failed"));
179 }
180 if errors > 0 {
181 result.push_str(&format!(", {errors} errors"));
182 }
183 if skips > 0 {
184 result.push_str(&format!(", {skips} skipped"));
185 }
186 if !time.is_empty() {
187 result.push_str(&format!(" ({time})"));
188 }
189
190 for detail in failure_details.iter().take(5) {
191 result.push_str(&format!("\n {detail}"));
192 }
193
194 Some(result)
195}