1use std::path::PathBuf;
6use std::time::Duration;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum BuildStatus {
11 Success,
13 Skipped,
15 Failed(String),
17}
18
19impl BuildStatus {
20 pub fn is_success(&self) -> bool {
22 matches!(self, BuildStatus::Success | BuildStatus::Skipped)
23 }
24
25 pub fn is_failure(&self) -> bool {
27 matches!(self, BuildStatus::Failed(_))
28 }
29}
30
31impl std::fmt::Display for BuildStatus {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 BuildStatus::Success => write!(f, "success"),
35 BuildStatus::Skipped => write!(f, "skipped"),
36 BuildStatus::Failed(err) => write!(f, "failed: {}", err),
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct TargetResult {
44 pub target_id: String,
46 pub status: BuildStatus,
48 pub outputs: Vec<PathBuf>,
50 pub duration: Duration,
52 pub warnings: Vec<String>,
54}
55
56impl TargetResult {
57 pub fn success(target_id: String, outputs: Vec<PathBuf>, duration: Duration) -> Self {
59 Self { target_id, status: BuildStatus::Success, outputs, duration, warnings: vec![] }
60 }
61
62 pub fn skipped(target_id: String) -> Self {
64 Self {
65 target_id,
66 status: BuildStatus::Skipped,
67 outputs: vec![],
68 duration: Duration::ZERO,
69 warnings: vec![],
70 }
71 }
72
73 pub fn failed(target_id: String, error: String, duration: Duration) -> Self {
75 Self {
76 target_id,
77 status: BuildStatus::Failed(error),
78 outputs: vec![],
79 duration,
80 warnings: vec![],
81 }
82 }
83
84 pub fn with_warnings(mut self, warnings: Vec<String>) -> Self {
86 self.warnings = warnings;
87 self
88 }
89
90 pub fn is_success(&self) -> bool {
92 self.status.is_success()
93 }
94}
95
96#[derive(Debug, Default)]
98pub struct BuildResult {
99 pub targets: Vec<TargetResult>,
101 pub total_duration: Duration,
103}
104
105impl BuildResult {
106 pub fn new() -> Self {
108 Self::default()
109 }
110
111 pub fn add_result(&mut self, result: TargetResult) {
113 self.targets.push(result);
114 }
115
116 pub fn with_duration(mut self, duration: Duration) -> Self {
118 self.total_duration = duration;
119 self
120 }
121
122 pub fn success_count(&self) -> usize {
124 self.targets.iter().filter(|r| matches!(r.status, BuildStatus::Success)).count()
125 }
126
127 pub fn skipped_count(&self) -> usize {
129 self.targets.iter().filter(|r| matches!(r.status, BuildStatus::Skipped)).count()
130 }
131
132 pub fn failed_count(&self) -> usize {
134 self.targets.iter().filter(|r| r.status.is_failure()).count()
135 }
136
137 pub fn is_success(&self) -> bool {
139 self.failed_count() == 0
140 }
141
142 pub fn all_outputs(&self) -> Vec<&PathBuf> {
144 self.targets.iter().flat_map(|r| r.outputs.iter()).collect()
145 }
146
147 pub fn all_warnings(&self) -> Vec<&String> {
149 self.targets.iter().flat_map(|r| r.warnings.iter()).collect()
150 }
151
152 pub fn failures(&self) -> Vec<&TargetResult> {
154 self.targets.iter().filter(|r| r.status.is_failure()).collect()
155 }
156
157 pub fn summary(&self) -> String {
159 let mut lines = Vec::new();
160
161 let success = self.success_count();
162 let skipped = self.skipped_count();
163 let failed = self.failed_count();
164 let total = self.targets.len();
165
166 if failed > 0 {
167 lines.push(format!(
168 "Build failed: {} succeeded, {} skipped, {} failed ({} total)",
169 success, skipped, failed, total
170 ));
171 for target in self.failures() {
172 lines.push(format!(" - {}: {}", target.target_id, target.status));
173 }
174 } else {
175 lines.push(format!(
176 "Build succeeded: {} built, {} skipped ({} total) in {:?}",
177 success, skipped, total, self.total_duration
178 ));
179 }
180
181 let warnings = self.all_warnings();
182 if !warnings.is_empty() {
183 lines.push(format!("Warnings ({}): ", warnings.len()));
184 for warning in warnings.iter().take(5) {
185 lines.push(format!(" - {}", warning));
186 }
187 if warnings.len() > 5 {
188 lines.push(format!(" ... and {} more", warnings.len() - 5));
189 }
190 }
191
192 lines.join("\n")
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_build_status_display() {
202 assert_eq!(BuildStatus::Success.to_string(), "success");
203 assert_eq!(BuildStatus::Skipped.to_string(), "skipped");
204 assert_eq!(BuildStatus::Failed("error".to_string()).to_string(), "failed: error");
205 }
206
207 #[test]
208 fn test_build_status_is_success() {
209 assert!(BuildStatus::Success.is_success());
210 assert!(BuildStatus::Skipped.is_success());
211 assert!(!BuildStatus::Failed("error".to_string()).is_success());
212 }
213
214 #[test]
215 fn test_target_result_success() {
216 let result = TargetResult::success(
217 "atlas:main".to_string(),
218 vec![PathBuf::from("main.png")],
219 Duration::from_millis(100),
220 );
221
222 assert!(result.is_success());
223 assert_eq!(result.outputs.len(), 1);
224 }
225
226 #[test]
227 fn test_target_result_failed() {
228 let result = TargetResult::failed(
229 "atlas:main".to_string(),
230 "File not found".to_string(),
231 Duration::from_millis(50),
232 );
233
234 assert!(!result.is_success());
235 assert!(result.outputs.is_empty());
236 }
237
238 #[test]
239 fn test_target_result_with_warnings() {
240 let result =
241 TargetResult::success("atlas:main".to_string(), vec![], Duration::from_millis(100))
242 .with_warnings(vec!["Warning 1".to_string(), "Warning 2".to_string()]);
243
244 assert_eq!(result.warnings.len(), 2);
245 }
246
247 #[test]
248 fn test_build_result_counts() {
249 let mut result = BuildResult::new();
250 result.add_result(TargetResult::success("a".to_string(), vec![], Duration::ZERO));
251 result.add_result(TargetResult::skipped("b".to_string()));
252 result.add_result(TargetResult::failed(
253 "c".to_string(),
254 "error".to_string(),
255 Duration::ZERO,
256 ));
257
258 assert_eq!(result.success_count(), 1);
259 assert_eq!(result.skipped_count(), 1);
260 assert_eq!(result.failed_count(), 1);
261 assert!(!result.is_success());
262 }
263
264 #[test]
265 fn test_build_result_is_success() {
266 let mut result = BuildResult::new();
267 result.add_result(TargetResult::success("a".to_string(), vec![], Duration::ZERO));
268 result.add_result(TargetResult::skipped("b".to_string()));
269
270 assert!(result.is_success());
271 }
272
273 #[test]
274 fn test_build_result_all_outputs() {
275 let mut result = BuildResult::new();
276 result.add_result(TargetResult::success(
277 "a".to_string(),
278 vec![PathBuf::from("a.png")],
279 Duration::ZERO,
280 ));
281 result.add_result(TargetResult::success(
282 "b".to_string(),
283 vec![PathBuf::from("b.png"), PathBuf::from("b.json")],
284 Duration::ZERO,
285 ));
286
287 let outputs = result.all_outputs();
288 assert_eq!(outputs.len(), 3);
289 }
290
291 #[test]
292 fn test_build_result_summary() {
293 let mut result = BuildResult::new();
294 result.add_result(TargetResult::success(
295 "atlas:main".to_string(),
296 vec![],
297 Duration::from_millis(100),
298 ));
299
300 let summary = result.with_duration(Duration::from_millis(100)).summary();
301 assert!(summary.contains("Build succeeded"));
302 assert!(summary.contains("1 built"));
303 }
304}