use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CIStatus {
Pending,
Running,
Passed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CIStageResult {
pub stage: String,
pub command: String,
pub status: CIStatus,
pub duration_ms: u64,
pub cache_hit: bool,
pub diagnostics_count: u32,
}
impl CIStageResult {
pub fn new(stage: String, command: String, status: CIStatus, duration_ms: u64) -> Self {
Self {
stage,
command,
status,
duration_ms,
cache_hit: false,
diagnostics_count: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CIResult {
pub run_id: Uuid,
pub overall_status: CIStatus,
pub stages: Vec<CIStageResult>,
pub started_at: DateTime<Utc>,
pub finished_at: Option<DateTime<Utc>>,
pub total_duration_ms: u64,
pub passed: u32,
pub failed: u32,
}
impl CIResult {
pub fn new(
run_id: Uuid,
stages: Vec<CIStageResult>,
started_at: DateTime<Utc>,
finished_at: Option<DateTime<Utc>>,
) -> Self {
let passed = stages
.iter()
.filter(|s| s.status == CIStatus::Passed)
.count() as u32;
let failed = stages
.iter()
.filter(|s| s.status == CIStatus::Failed)
.count() as u32;
let total_duration_ms = finished_at
.as_ref()
.map(|finished| {
let delta_ms = finished
.signed_duration_since(started_at)
.num_milliseconds();
delta_ms.max(0) as u64
})
.unwrap_or(0);
let overall_status = if failed > 0 {
CIStatus::Failed
} else {
CIStatus::Passed
};
Self {
run_id,
overall_status,
stages,
started_at,
finished_at,
total_duration_ms,
passed,
failed,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ci_status_serde() {
let statuses = [
CIStatus::Pending,
CIStatus::Running,
CIStatus::Passed,
CIStatus::Failed,
CIStatus::Cancelled,
];
for status in &statuses {
let json = serde_json::to_string(status).expect("serialize");
let deserialized: CIStatus = serde_json::from_str(&json).expect("deserialize");
assert_eq!(*status, deserialized);
}
assert_eq!(
serde_json::to_string(&CIStatus::Passed).expect("serialize"),
"\"passed\""
);
}
#[test]
fn test_ci_stage_result_serde_roundtrip() {
let result = CIStageResult::new(
"clippy".to_string(),
"cargo clippy --workspace".to_string(),
CIStatus::Passed,
4500,
);
let json = serde_json::to_string(&result).expect("serialize");
let deserialized: CIStageResult = serde_json::from_str(&json).expect("deserialize");
assert_eq!(result, deserialized);
}
#[test]
fn test_ci_stage_result_defaults() {
let result = CIStageResult::new(
"fmt".to_string(),
"cargo fmt --check".to_string(),
CIStatus::Passed,
200,
);
assert!(!result.cache_hit);
assert_eq!(result.diagnostics_count, 0);
}
#[test]
fn test_ci_result_computes_aggregates() {
let run_id = Uuid::new_v4();
let stages = vec![
CIStageResult::new("fmt".into(), "cargo fmt".into(), CIStatus::Passed, 100),
CIStageResult::new(
"clippy".into(),
"cargo clippy".into(),
CIStatus::Failed,
500,
),
CIStageResult::new("test".into(), "cargo test".into(), CIStatus::Passed, 2000),
];
let started_at = Utc::now();
let finished_at = started_at + chrono::Duration::milliseconds(2600);
let result = CIResult::new(run_id, stages, started_at, Some(finished_at));
assert_eq!(result.passed, 2);
assert_eq!(result.failed, 1);
assert_eq!(result.overall_status, CIStatus::Failed);
assert_eq!(result.total_duration_ms, 2600);
}
#[test]
fn test_ci_result_all_pass() {
let run_id = Uuid::new_v4();
let stages = vec![
CIStageResult::new("fmt".into(), "cargo fmt".into(), CIStatus::Passed, 100),
CIStageResult::new("test".into(), "cargo test".into(), CIStatus::Passed, 1000),
];
let started_at = Utc::now();
let finished_at = started_at + chrono::Duration::milliseconds(1100);
let result = CIResult::new(run_id, stages, started_at, Some(finished_at));
assert_eq!(result.overall_status, CIStatus::Passed);
assert_eq!(result.passed, 2);
assert_eq!(result.failed, 0);
}
#[test]
fn test_ci_result_serde_roundtrip() {
let run_id = Uuid::new_v4();
let stages = vec![CIStageResult::new(
"test".into(),
"cargo test".into(),
CIStatus::Passed,
3000,
)];
let started_at = Utc::now();
let finished_at = started_at + chrono::Duration::milliseconds(3000);
let result = CIResult::new(run_id, stages, started_at, Some(finished_at));
let json = serde_json::to_string(&result).expect("serialize");
let deserialized: CIResult = serde_json::from_str(&json).expect("deserialize");
assert_eq!(result, deserialized);
}
}