Skip to main content

pixelsrc/build/
result.rs

1//! Build result types.
2//!
3//! Contains types for representing the outcome of build operations.
4
5use std::path::PathBuf;
6use std::time::Duration;
7
8/// Status of a single build target.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum BuildStatus {
11    /// Build succeeded
12    Success,
13    /// Build skipped (already up to date)
14    Skipped,
15    /// Build failed with error
16    Failed(String),
17}
18
19impl BuildStatus {
20    /// Check if the status indicates success.
21    pub fn is_success(&self) -> bool {
22        matches!(self, BuildStatus::Success | BuildStatus::Skipped)
23    }
24
25    /// Check if the status indicates failure.
26    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/// Result of building a single target.
42#[derive(Debug, Clone)]
43pub struct TargetResult {
44    /// Target ID that was built
45    pub target_id: String,
46    /// Build status
47    pub status: BuildStatus,
48    /// Output files produced
49    pub outputs: Vec<PathBuf>,
50    /// Build duration
51    pub duration: Duration,
52    /// Warning messages (if any)
53    pub warnings: Vec<String>,
54}
55
56impl TargetResult {
57    /// Create a successful result.
58    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    /// Create a skipped result.
63    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    /// Create a failed result.
74    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    /// Add warnings to the result.
85    pub fn with_warnings(mut self, warnings: Vec<String>) -> Self {
86        self.warnings = warnings;
87        self
88    }
89
90    /// Check if this result is successful.
91    pub fn is_success(&self) -> bool {
92        self.status.is_success()
93    }
94}
95
96/// Result of a complete build run.
97#[derive(Debug, Default)]
98pub struct BuildResult {
99    /// Results for each target
100    pub targets: Vec<TargetResult>,
101    /// Total build duration
102    pub total_duration: Duration,
103}
104
105impl BuildResult {
106    /// Create a new empty build result.
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    /// Add a target result.
112    pub fn add_result(&mut self, result: TargetResult) {
113        self.targets.push(result);
114    }
115
116    /// Set the total duration.
117    pub fn with_duration(mut self, duration: Duration) -> Self {
118        self.total_duration = duration;
119        self
120    }
121
122    /// Get the number of successful targets.
123    pub fn success_count(&self) -> usize {
124        self.targets.iter().filter(|r| matches!(r.status, BuildStatus::Success)).count()
125    }
126
127    /// Get the number of skipped targets.
128    pub fn skipped_count(&self) -> usize {
129        self.targets.iter().filter(|r| matches!(r.status, BuildStatus::Skipped)).count()
130    }
131
132    /// Get the number of failed targets.
133    pub fn failed_count(&self) -> usize {
134        self.targets.iter().filter(|r| r.status.is_failure()).count()
135    }
136
137    /// Check if the overall build succeeded (no failures).
138    pub fn is_success(&self) -> bool {
139        self.failed_count() == 0
140    }
141
142    /// Get all outputs produced.
143    pub fn all_outputs(&self) -> Vec<&PathBuf> {
144        self.targets.iter().flat_map(|r| r.outputs.iter()).collect()
145    }
146
147    /// Get all warnings.
148    pub fn all_warnings(&self) -> Vec<&String> {
149        self.targets.iter().flat_map(|r| r.warnings.iter()).collect()
150    }
151
152    /// Get failed target results.
153    pub fn failures(&self) -> Vec<&TargetResult> {
154        self.targets.iter().filter(|r| r.status.is_failure()).collect()
155    }
156
157    /// Format a summary of the build result.
158    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}