#![allow(dead_code)]
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum StepStatus {
Pending,
Running,
Success,
Failed,
Skipped,
}
impl StepStatus {
#[must_use]
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Success | Self::Failed | Self::Skipped)
}
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Running => "running",
Self::Success => "success",
Self::Failed => "failed",
Self::Skipped => "skipped",
}
}
}
#[derive(Debug, Clone)]
pub struct StepResult {
pub step_id: String,
pub status: StepStatus,
elapsed_ms: u64,
pub message: Option<String>,
}
impl StepResult {
#[must_use]
pub fn new(step_id: impl Into<String>, status: StepStatus, elapsed_ms: u64) -> Self {
Self {
step_id: step_id.into(),
status,
elapsed_ms,
message: None,
}
}
#[must_use]
pub fn with_message(mut self, msg: impl Into<String>) -> Self {
self.message = Some(msg.into());
self
}
#[must_use]
pub fn elapsed_ms(&self) -> u64 {
self.elapsed_ms
}
#[must_use]
pub fn is_success(&self) -> bool {
self.status == StepStatus::Success
}
#[must_use]
pub fn is_failed(&self) -> bool {
self.status == StepStatus::Failed
}
}
#[derive(Debug, Default)]
pub struct StepResultLog {
entries: HashMap<String, Vec<StepResult>>,
}
impl StepResultLog {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, run_id: impl Into<String>, result: StepResult) {
self.entries.entry(run_id.into()).or_default().push(result);
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn success_rate(&self, run_id: &str) -> Option<f64> {
let results = self.entries.get(run_id)?;
let terminal: Vec<_> = results.iter().filter(|r| r.status.is_terminal()).collect();
if terminal.is_empty() {
return None;
}
let successes = terminal.iter().filter(|r| r.is_success()).count();
Some(successes as f64 / terminal.len() as f64)
}
#[must_use]
pub fn failed_steps(&self, run_id: &str) -> Vec<&StepResult> {
self.entries
.get(run_id)
.map(|v| v.iter().filter(|r| r.is_failed()).collect())
.unwrap_or_default()
}
#[must_use]
pub fn total_entries(&self) -> usize {
self.entries.values().map(Vec::len).sum()
}
#[must_use]
pub fn run_ids(&self) -> Vec<&str> {
self.entries.keys().map(String::as_str).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_status_pending_not_terminal() {
assert!(!StepStatus::Pending.is_terminal());
}
#[test]
fn test_status_running_not_terminal() {
assert!(!StepStatus::Running.is_terminal());
}
#[test]
fn test_status_success_is_terminal() {
assert!(StepStatus::Success.is_terminal());
}
#[test]
fn test_status_failed_is_terminal() {
assert!(StepStatus::Failed.is_terminal());
}
#[test]
fn test_status_skipped_is_terminal() {
assert!(StepStatus::Skipped.is_terminal());
}
#[test]
fn test_status_labels() {
assert_eq!(StepStatus::Pending.label(), "pending");
assert_eq!(StepStatus::Running.label(), "running");
assert_eq!(StepStatus::Success.label(), "success");
assert_eq!(StepStatus::Failed.label(), "failed");
assert_eq!(StepStatus::Skipped.label(), "skipped");
}
#[test]
fn test_step_result_elapsed_ms() {
let r = StepResult::new("step1", StepStatus::Success, 250);
assert_eq!(r.elapsed_ms(), 250);
}
#[test]
fn test_step_result_is_success() {
let r = StepResult::new("step1", StepStatus::Success, 10);
assert!(r.is_success());
assert!(!r.is_failed());
}
#[test]
fn test_step_result_is_failed() {
let r = StepResult::new("step2", StepStatus::Failed, 5);
assert!(r.is_failed());
assert!(!r.is_success());
}
#[test]
fn test_step_result_with_message() {
let r = StepResult::new("s", StepStatus::Failed, 0).with_message("oops");
assert_eq!(r.message.as_deref(), Some("oops"));
}
#[test]
fn test_log_add_and_total_entries() {
let mut log = StepResultLog::new();
log.add("run1", StepResult::new("a", StepStatus::Success, 1));
log.add("run1", StepResult::new("b", StepStatus::Failed, 2));
log.add("run2", StepResult::new("c", StepStatus::Skipped, 0));
assert_eq!(log.total_entries(), 3);
}
#[test]
fn test_success_rate_all_success() {
let mut log = StepResultLog::new();
log.add("r", StepResult::new("a", StepStatus::Success, 1));
log.add("r", StepResult::new("b", StepStatus::Success, 2));
let rate = log.success_rate("r").expect("should succeed in test");
assert!((rate - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_success_rate_mixed() {
let mut log = StepResultLog::new();
log.add("r", StepResult::new("a", StepStatus::Success, 1));
log.add("r", StepResult::new("b", StepStatus::Failed, 2));
let rate = log.success_rate("r").expect("should succeed in test");
assert!((rate - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_success_rate_unknown_run() {
let log = StepResultLog::new();
assert!(log.success_rate("unknown").is_none());
}
#[test]
fn test_failed_steps() {
let mut log = StepResultLog::new();
log.add("r", StepResult::new("ok", StepStatus::Success, 1));
log.add("r", StepResult::new("bad", StepStatus::Failed, 2));
let failed = log.failed_steps("r");
assert_eq!(failed.len(), 1);
assert_eq!(failed[0].step_id, "bad");
}
#[test]
fn test_run_ids() {
let mut log = StepResultLog::new();
log.add("alpha", StepResult::new("x", StepStatus::Success, 0));
log.add("beta", StepResult::new("y", StepStatus::Success, 0));
let mut ids = log.run_ids();
ids.sort_unstable();
assert_eq!(ids, vec!["alpha", "beta"]);
}
#[test]
fn test_success_rate_no_terminal_steps() {
let mut log = StepResultLog::new();
log.add("r", StepResult::new("x", StepStatus::Running, 0));
assert!(log.success_rate("r").is_none());
}
}