actionqueue_engine/index/
terminal.rs1use actionqueue_core::run::run_instance::RunInstance;
8use actionqueue_core::run::state::RunState;
9
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
16pub struct TerminalIndex {
17 runs: Vec<RunInstance>,
19}
20
21impl TerminalIndex {
22 pub fn new() -> Self {
24 Self { runs: Vec::new() }
25 }
26
27 pub fn from_runs(runs: Vec<RunInstance>) -> Self {
32 debug_assert!(
33 runs.iter().all(|r| r.state().is_terminal()),
34 "TerminalIndex::from_runs called with non-terminal run"
35 );
36 Self { runs }
37 }
38
39 pub fn runs(&self) -> &[RunInstance] {
41 &self.runs
42 }
43
44 pub fn len(&self) -> usize {
46 self.runs.len()
47 }
48
49 pub fn is_empty(&self) -> bool {
51 self.runs.is_empty()
52 }
53
54 pub fn completed(&self) -> Vec<&RunInstance> {
56 self.runs.iter().filter(|run| run.state() == RunState::Completed).collect()
57 }
58
59 pub fn failed(&self) -> Vec<&RunInstance> {
61 self.runs.iter().filter(|run| run.state() == RunState::Failed).collect()
62 }
63
64 pub fn canceled(&self) -> Vec<&RunInstance> {
66 self.runs.iter().filter(|run| run.state() == RunState::Canceled).collect()
67 }
68}
69
70impl From<&[RunInstance]> for TerminalIndex {
71 fn from(runs: &[RunInstance]) -> Self {
72 let terminal_runs: Vec<RunInstance> =
73 runs.iter().filter(|run| run.state().is_terminal()).cloned().collect();
74 Self::from_runs(terminal_runs)
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use actionqueue_core::ids::TaskId;
81 use actionqueue_core::run::state::RunState;
82
83 use super::*;
84 use crate::index::test_util::build_run;
85
86 #[test]
87 fn terminal_index_filters_correctly() {
88 let now = 1000;
89 let task_id = TaskId::new();
90
91 let runs = vec![
93 build_run(task_id, RunState::Ready, 900, now, 0, None),
94 build_run(task_id, RunState::Completed, 900, now, 0, None),
95 build_run(task_id, RunState::Failed, 950, now, 0, None),
96 build_run(task_id, RunState::Canceled, 800, now, 0, None),
97 ];
98
99 let index = TerminalIndex::from(runs.as_slice());
100
101 assert_eq!(index.len(), 3); assert!(index.runs().iter().all(|run| run.state().is_terminal()));
103 }
104
105 #[test]
106 fn terminal_index_breakdown() {
107 let now = 1000;
108 let task_id = TaskId::new();
109
110 let runs = vec![
111 build_run(task_id, RunState::Completed, 900, now, 0, None),
112 build_run(task_id, RunState::Completed, 950, now, 0, None),
113 build_run(task_id, RunState::Failed, 800, now, 0, None),
114 ];
115
116 let index = TerminalIndex::from(runs.as_slice());
117
118 assert_eq!(index.completed().len(), 2);
119 assert_eq!(index.failed().len(), 1);
120 assert_eq!(index.canceled().len(), 0);
121 }
122
123 #[test]
124 fn terminal_index_is_empty() {
125 let index = TerminalIndex::new();
126 assert!(index.is_empty());
127 assert_eq!(index.len(), 0);
128
129 let now = 1000;
130 let task_id = TaskId::new();
131 let run = build_run(task_id, RunState::Completed, 900, now, 0, None);
132 let index = TerminalIndex::from(std::slice::from_ref(&run));
133
134 assert!(!index.is_empty());
135 assert_eq!(index.len(), 1);
136 }
137
138 #[test]
139 fn terminal_index_preserves_order_and_state_purity() {
140 let now = 1000;
141 let task_id = TaskId::new();
142
143 let scheduled = build_run(task_id, RunState::Scheduled, 700, now, 0, None);
144 let completed = build_run(task_id, RunState::Completed, 710, now, 0, None);
145 let running = build_run(
146 task_id,
147 RunState::Running,
148 720,
149 now,
150 0,
151 Some(actionqueue_core::ids::AttemptId::new()),
152 );
153 let failed = build_run(task_id, RunState::Failed, 730, now, 0, None);
154 let canceled = build_run(task_id, RunState::Canceled, 740, now, 0, None);
155
156 let expected_order =
157 vec![completed.id().to_string(), failed.id().to_string(), canceled.id().to_string()];
158 let runs = vec![scheduled, completed, running, failed, canceled];
159 let index = TerminalIndex::from(runs.as_slice());
160
161 let actual_order: Vec<String> =
162 index.runs().iter().map(|run| run.id().to_string()).collect();
163 assert_eq!(actual_order, expected_order);
164 assert!(index.runs().iter().all(|run| run.state().is_terminal()));
165 }
166}