1#![allow(dead_code)]
7
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub enum StepStatus {
17 Pending,
19 Running,
21 Success,
23 Failed,
25 Skipped,
27}
28
29impl StepStatus {
30 #[must_use]
32 pub fn is_terminal(&self) -> bool {
33 matches!(self, Self::Success | Self::Failed | Self::Skipped)
34 }
35
36 #[must_use]
38 pub fn label(&self) -> &'static str {
39 match self {
40 Self::Pending => "pending",
41 Self::Running => "running",
42 Self::Success => "success",
43 Self::Failed => "failed",
44 Self::Skipped => "skipped",
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
55pub struct StepResult {
56 pub step_id: String,
58 pub status: StepStatus,
60 elapsed_ms: u64,
62 pub message: Option<String>,
64}
65
66impl StepResult {
67 #[must_use]
69 pub fn new(step_id: impl Into<String>, status: StepStatus, elapsed_ms: u64) -> Self {
70 Self {
71 step_id: step_id.into(),
72 status,
73 elapsed_ms,
74 message: None,
75 }
76 }
77
78 #[must_use]
80 pub fn with_message(mut self, msg: impl Into<String>) -> Self {
81 self.message = Some(msg.into());
82 self
83 }
84
85 #[must_use]
87 pub fn elapsed_ms(&self) -> u64 {
88 self.elapsed_ms
89 }
90
91 #[must_use]
93 pub fn is_success(&self) -> bool {
94 self.status == StepStatus::Success
95 }
96
97 #[must_use]
99 pub fn is_failed(&self) -> bool {
100 self.status == StepStatus::Failed
101 }
102}
103
104#[derive(Debug, Default)]
110pub struct StepResultLog {
111 entries: HashMap<String, Vec<StepResult>>,
112}
113
114impl StepResultLog {
115 #[must_use]
117 pub fn new() -> Self {
118 Self::default()
119 }
120
121 pub fn add(&mut self, run_id: impl Into<String>, result: StepResult) {
123 self.entries.entry(run_id.into()).or_default().push(result);
124 }
125
126 #[allow(clippy::cast_precision_loss)]
130 #[must_use]
131 pub fn success_rate(&self, run_id: &str) -> Option<f64> {
132 let results = self.entries.get(run_id)?;
133 let terminal: Vec<_> = results.iter().filter(|r| r.status.is_terminal()).collect();
134 if terminal.is_empty() {
135 return None;
136 }
137 let successes = terminal.iter().filter(|r| r.is_success()).count();
138 Some(successes as f64 / terminal.len() as f64)
139 }
140
141 #[must_use]
143 pub fn failed_steps(&self, run_id: &str) -> Vec<&StepResult> {
144 self.entries
145 .get(run_id)
146 .map(|v| v.iter().filter(|r| r.is_failed()).collect())
147 .unwrap_or_default()
148 }
149
150 #[must_use]
152 pub fn total_entries(&self) -> usize {
153 self.entries.values().map(Vec::len).sum()
154 }
155
156 #[must_use]
158 pub fn run_ids(&self) -> Vec<&str> {
159 self.entries.keys().map(String::as_str).collect()
160 }
161}
162
163#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_status_pending_not_terminal() {
173 assert!(!StepStatus::Pending.is_terminal());
174 }
175
176 #[test]
177 fn test_status_running_not_terminal() {
178 assert!(!StepStatus::Running.is_terminal());
179 }
180
181 #[test]
182 fn test_status_success_is_terminal() {
183 assert!(StepStatus::Success.is_terminal());
184 }
185
186 #[test]
187 fn test_status_failed_is_terminal() {
188 assert!(StepStatus::Failed.is_terminal());
189 }
190
191 #[test]
192 fn test_status_skipped_is_terminal() {
193 assert!(StepStatus::Skipped.is_terminal());
194 }
195
196 #[test]
197 fn test_status_labels() {
198 assert_eq!(StepStatus::Pending.label(), "pending");
199 assert_eq!(StepStatus::Running.label(), "running");
200 assert_eq!(StepStatus::Success.label(), "success");
201 assert_eq!(StepStatus::Failed.label(), "failed");
202 assert_eq!(StepStatus::Skipped.label(), "skipped");
203 }
204
205 #[test]
206 fn test_step_result_elapsed_ms() {
207 let r = StepResult::new("step1", StepStatus::Success, 250);
208 assert_eq!(r.elapsed_ms(), 250);
209 }
210
211 #[test]
212 fn test_step_result_is_success() {
213 let r = StepResult::new("step1", StepStatus::Success, 10);
214 assert!(r.is_success());
215 assert!(!r.is_failed());
216 }
217
218 #[test]
219 fn test_step_result_is_failed() {
220 let r = StepResult::new("step2", StepStatus::Failed, 5);
221 assert!(r.is_failed());
222 assert!(!r.is_success());
223 }
224
225 #[test]
226 fn test_step_result_with_message() {
227 let r = StepResult::new("s", StepStatus::Failed, 0).with_message("oops");
228 assert_eq!(r.message.as_deref(), Some("oops"));
229 }
230
231 #[test]
232 fn test_log_add_and_total_entries() {
233 let mut log = StepResultLog::new();
234 log.add("run1", StepResult::new("a", StepStatus::Success, 1));
235 log.add("run1", StepResult::new("b", StepStatus::Failed, 2));
236 log.add("run2", StepResult::new("c", StepStatus::Skipped, 0));
237 assert_eq!(log.total_entries(), 3);
238 }
239
240 #[test]
241 fn test_success_rate_all_success() {
242 let mut log = StepResultLog::new();
243 log.add("r", StepResult::new("a", StepStatus::Success, 1));
244 log.add("r", StepResult::new("b", StepStatus::Success, 2));
245 let rate = log.success_rate("r").expect("should succeed in test");
246 assert!((rate - 1.0).abs() < f64::EPSILON);
247 }
248
249 #[test]
250 fn test_success_rate_mixed() {
251 let mut log = StepResultLog::new();
252 log.add("r", StepResult::new("a", StepStatus::Success, 1));
253 log.add("r", StepResult::new("b", StepStatus::Failed, 2));
254 let rate = log.success_rate("r").expect("should succeed in test");
255 assert!((rate - 0.5).abs() < f64::EPSILON);
256 }
257
258 #[test]
259 fn test_success_rate_unknown_run() {
260 let log = StepResultLog::new();
261 assert!(log.success_rate("unknown").is_none());
262 }
263
264 #[test]
265 fn test_failed_steps() {
266 let mut log = StepResultLog::new();
267 log.add("r", StepResult::new("ok", StepStatus::Success, 1));
268 log.add("r", StepResult::new("bad", StepStatus::Failed, 2));
269 let failed = log.failed_steps("r");
270 assert_eq!(failed.len(), 1);
271 assert_eq!(failed[0].step_id, "bad");
272 }
273
274 #[test]
275 fn test_run_ids() {
276 let mut log = StepResultLog::new();
277 log.add("alpha", StepResult::new("x", StepStatus::Success, 0));
278 log.add("beta", StepResult::new("y", StepStatus::Success, 0));
279 let mut ids = log.run_ids();
280 ids.sort_unstable();
281 assert_eq!(ids, vec!["alpha", "beta"]);
282 }
283
284 #[test]
285 fn test_success_rate_no_terminal_steps() {
286 let mut log = StepResultLog::new();
287 log.add("r", StepResult::new("x", StepStatus::Running, 0));
288 assert!(log.success_rate("r").is_none());
289 }
290}