use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BuildStatus {
Success,
Skipped,
Failed(String),
}
impl BuildStatus {
pub fn is_success(&self) -> bool {
matches!(self, BuildStatus::Success | BuildStatus::Skipped)
}
pub fn is_failure(&self) -> bool {
matches!(self, BuildStatus::Failed(_))
}
}
impl std::fmt::Display for BuildStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuildStatus::Success => write!(f, "success"),
BuildStatus::Skipped => write!(f, "skipped"),
BuildStatus::Failed(err) => write!(f, "failed: {}", err),
}
}
}
#[derive(Debug, Clone)]
pub struct TargetResult {
pub target_id: String,
pub status: BuildStatus,
pub outputs: Vec<PathBuf>,
pub duration: Duration,
pub warnings: Vec<String>,
}
impl TargetResult {
pub fn success(target_id: String, outputs: Vec<PathBuf>, duration: Duration) -> Self {
Self { target_id, status: BuildStatus::Success, outputs, duration, warnings: vec![] }
}
pub fn skipped(target_id: String) -> Self {
Self {
target_id,
status: BuildStatus::Skipped,
outputs: vec![],
duration: Duration::ZERO,
warnings: vec![],
}
}
pub fn failed(target_id: String, error: String, duration: Duration) -> Self {
Self {
target_id,
status: BuildStatus::Failed(error),
outputs: vec![],
duration,
warnings: vec![],
}
}
pub fn with_warnings(mut self, warnings: Vec<String>) -> Self {
self.warnings = warnings;
self
}
pub fn is_success(&self) -> bool {
self.status.is_success()
}
}
#[derive(Debug, Default)]
pub struct BuildResult {
pub targets: Vec<TargetResult>,
pub total_duration: Duration,
}
impl BuildResult {
pub fn new() -> Self {
Self::default()
}
pub fn add_result(&mut self, result: TargetResult) {
self.targets.push(result);
}
pub fn with_duration(mut self, duration: Duration) -> Self {
self.total_duration = duration;
self
}
pub fn success_count(&self) -> usize {
self.targets.iter().filter(|r| matches!(r.status, BuildStatus::Success)).count()
}
pub fn skipped_count(&self) -> usize {
self.targets.iter().filter(|r| matches!(r.status, BuildStatus::Skipped)).count()
}
pub fn failed_count(&self) -> usize {
self.targets.iter().filter(|r| r.status.is_failure()).count()
}
pub fn is_success(&self) -> bool {
self.failed_count() == 0
}
pub fn all_outputs(&self) -> Vec<&PathBuf> {
self.targets.iter().flat_map(|r| r.outputs.iter()).collect()
}
pub fn all_warnings(&self) -> Vec<&String> {
self.targets.iter().flat_map(|r| r.warnings.iter()).collect()
}
pub fn failures(&self) -> Vec<&TargetResult> {
self.targets.iter().filter(|r| r.status.is_failure()).collect()
}
pub fn summary(&self) -> String {
let mut lines = Vec::new();
let success = self.success_count();
let skipped = self.skipped_count();
let failed = self.failed_count();
let total = self.targets.len();
if failed > 0 {
lines.push(format!(
"Build failed: {} succeeded, {} skipped, {} failed ({} total)",
success, skipped, failed, total
));
for target in self.failures() {
lines.push(format!(" - {}: {}", target.target_id, target.status));
}
} else {
lines.push(format!(
"Build succeeded: {} built, {} skipped ({} total) in {:?}",
success, skipped, total, self.total_duration
));
}
let warnings = self.all_warnings();
if !warnings.is_empty() {
lines.push(format!("Warnings ({}): ", warnings.len()));
for warning in warnings.iter().take(5) {
lines.push(format!(" - {}", warning));
}
if warnings.len() > 5 {
lines.push(format!(" ... and {} more", warnings.len() - 5));
}
}
lines.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_status_display() {
assert_eq!(BuildStatus::Success.to_string(), "success");
assert_eq!(BuildStatus::Skipped.to_string(), "skipped");
assert_eq!(BuildStatus::Failed("error".to_string()).to_string(), "failed: error");
}
#[test]
fn test_build_status_is_success() {
assert!(BuildStatus::Success.is_success());
assert!(BuildStatus::Skipped.is_success());
assert!(!BuildStatus::Failed("error".to_string()).is_success());
}
#[test]
fn test_target_result_success() {
let result = TargetResult::success(
"atlas:main".to_string(),
vec![PathBuf::from("main.png")],
Duration::from_millis(100),
);
assert!(result.is_success());
assert_eq!(result.outputs.len(), 1);
}
#[test]
fn test_target_result_failed() {
let result = TargetResult::failed(
"atlas:main".to_string(),
"File not found".to_string(),
Duration::from_millis(50),
);
assert!(!result.is_success());
assert!(result.outputs.is_empty());
}
#[test]
fn test_target_result_with_warnings() {
let result =
TargetResult::success("atlas:main".to_string(), vec![], Duration::from_millis(100))
.with_warnings(vec!["Warning 1".to_string(), "Warning 2".to_string()]);
assert_eq!(result.warnings.len(), 2);
}
#[test]
fn test_build_result_counts() {
let mut result = BuildResult::new();
result.add_result(TargetResult::success("a".to_string(), vec![], Duration::ZERO));
result.add_result(TargetResult::skipped("b".to_string()));
result.add_result(TargetResult::failed(
"c".to_string(),
"error".to_string(),
Duration::ZERO,
));
assert_eq!(result.success_count(), 1);
assert_eq!(result.skipped_count(), 1);
assert_eq!(result.failed_count(), 1);
assert!(!result.is_success());
}
#[test]
fn test_build_result_is_success() {
let mut result = BuildResult::new();
result.add_result(TargetResult::success("a".to_string(), vec![], Duration::ZERO));
result.add_result(TargetResult::skipped("b".to_string()));
assert!(result.is_success());
}
#[test]
fn test_build_result_all_outputs() {
let mut result = BuildResult::new();
result.add_result(TargetResult::success(
"a".to_string(),
vec![PathBuf::from("a.png")],
Duration::ZERO,
));
result.add_result(TargetResult::success(
"b".to_string(),
vec![PathBuf::from("b.png"), PathBuf::from("b.json")],
Duration::ZERO,
));
let outputs = result.all_outputs();
assert_eq!(outputs.len(), 3);
}
#[test]
fn test_build_result_summary() {
let mut result = BuildResult::new();
result.add_result(TargetResult::success(
"atlas:main".to_string(),
vec![],
Duration::from_millis(100),
));
let summary = result.with_duration(Duration::from_millis(100)).summary();
assert!(summary.contains("Build succeeded"));
assert!(summary.contains("1 built"));
}
}