1use crate::config::EXIT_CODE_INTERRUPTED;
2
3#[allow(dead_code)]
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5#[repr(i32)]
6pub enum ExitCode {
7 Success = 0,
8 Error = 1,
9 Warning = 2,
10 Retryable = 3,
11 Authorization = 4,
12 Unsupported = 5,
13 Interrupted = EXIT_CODE_INTERRUPTED,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum DistributedReadOutcome {
21 Success,
22 Unsupported,
23 AuthorizationFailure,
24 RetryableFailure,
25 TerminalFailure,
26 Cancelled,
27}
28
29impl DistributedReadOutcome {
30 pub fn exit_code(self) -> ExitCode {
31 match self {
32 Self::Success => ExitCode::Success,
33 Self::Unsupported => ExitCode::Unsupported,
34 Self::AuthorizationFailure => ExitCode::Authorization,
35 Self::RetryableFailure => ExitCode::Retryable,
36 Self::TerminalFailure => ExitCode::Error,
37 Self::Cancelled => ExitCode::Interrupted,
38 }
39 }
40
41 pub fn as_str(self) -> &'static str {
42 match self {
43 Self::Success => "success",
44 Self::Unsupported => "unsupported",
45 Self::AuthorizationFailure => "authorization_failure",
46 Self::RetryableFailure => "retryable_failure",
47 Self::TerminalFailure => "terminal_failure",
48 Self::Cancelled => "cancelled",
49 }
50 }
51}
52
53#[allow(dead_code)]
54impl ExitCode {
55 pub fn as_i32(self) -> i32 {
56 self as i32
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum ClusterManagementOutcome {
65 Succeeded,
66 Pending,
67 RetryableFailure,
68 TerminalFailure,
69 AuthorizationFailure,
70}
71
72impl ClusterManagementOutcome {
73 pub fn from_wire(outcome_class: &str, reason: &str) -> Self {
74 let outcome_class = outcome_class.to_ascii_lowercase();
75 let reason = reason.to_ascii_lowercase();
76 if outcome_class == "authorization_failure"
77 || reason.contains("authorization")
78 || reason.contains("permission")
79 || reason.contains("forbidden")
80 {
81 return Self::AuthorizationFailure;
82 }
83 match outcome_class.as_str() {
84 "succeeded" | "success" => Self::Succeeded,
85 "pending" => Self::Pending,
86 "retryable_failure" => Self::RetryableFailure,
87 "terminal_failure" => Self::TerminalFailure,
88 _ => Self::TerminalFailure,
89 }
90 }
91
92 pub fn exit_code(self) -> ExitCode {
93 match self {
94 Self::Succeeded => ExitCode::Success,
95 Self::Pending => ExitCode::Warning,
96 Self::RetryableFailure => ExitCode::Retryable,
97 Self::TerminalFailure => ExitCode::Error,
98 Self::AuthorizationFailure => ExitCode::Authorization,
99 }
100 }
101
102 pub fn is_success(self) -> bool {
103 matches!(self, Self::Succeeded)
104 }
105}
106
107#[allow(dead_code)]
108#[derive(Debug, Default)]
109pub struct ExitCodeCollector {
110 success_count: usize,
111 error_count: usize,
112}
113
114#[allow(dead_code)]
115impl ExitCodeCollector {
116 pub fn new() -> Self {
117 Self::default()
118 }
119
120 pub fn record_success(&mut self) {
121 self.success_count += 1;
122 }
123
124 pub fn record_error(&mut self) {
125 self.error_count += 1;
126 }
127
128 pub fn finalize(&self) -> ExitCode {
129 match (self.success_count > 0, self.error_count > 0) {
130 (false, false) => ExitCode::Success,
131 (true, false) => ExitCode::Success,
132 (false, true) => ExitCode::Error,
133 (true, true) => ExitCode::Warning,
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn exit_code_values() {
144 assert_eq!(ExitCode::Success.as_i32(), 0);
145 assert_eq!(ExitCode::Error.as_i32(), 1);
146 assert_eq!(ExitCode::Warning.as_i32(), 2);
147 assert_eq!(ExitCode::Retryable.as_i32(), 3);
148 assert_eq!(ExitCode::Authorization.as_i32(), 4);
149 assert_eq!(ExitCode::Unsupported.as_i32(), 5);
150 assert_eq!(ExitCode::Interrupted.as_i32(), EXIT_CODE_INTERRUPTED);
151 }
152
153 #[test]
154 fn cluster_management_outcomes_have_stable_exit_classes() {
155 assert_eq!(
156 ClusterManagementOutcome::from_wire("succeeded", "committed").exit_code(),
157 ExitCode::Success
158 );
159 assert_eq!(
160 ClusterManagementOutcome::from_wire("pending", "waiting_for_quorum").exit_code(),
161 ExitCode::Warning
162 );
163 assert_eq!(
164 ClusterManagementOutcome::from_wire("retryable_failure", "not_leader").exit_code(),
165 ExitCode::Retryable
166 );
167 assert_eq!(
168 ClusterManagementOutcome::from_wire("terminal_failure", "stale_version").exit_code(),
169 ExitCode::Error
170 );
171 assert_eq!(
172 ClusterManagementOutcome::from_wire("terminal_failure", "authorization_denied")
173 .exit_code(),
174 ExitCode::Authorization
175 );
176 assert_eq!(
177 ClusterManagementOutcome::from_wire("new_outcome", "unknown").exit_code(),
178 ExitCode::Error
179 );
180 }
181
182 #[test]
183 fn collector_defaults_to_success() {
184 let collector = ExitCodeCollector::new();
185
186 assert_eq!(collector.finalize(), ExitCode::Success);
187 }
188
189 #[test]
190 fn collector_reports_success_only() {
191 let mut collector = ExitCodeCollector::new();
192 collector.record_success();
193
194 assert_eq!(collector.finalize(), ExitCode::Success);
195 }
196
197 #[test]
198 fn collector_reports_error_only() {
199 let mut collector = ExitCodeCollector::new();
200 collector.record_error();
201
202 assert_eq!(collector.finalize(), ExitCode::Error);
203 }
204
205 #[test]
206 fn collector_reports_warning_on_mixed_results() {
207 let mut collector = ExitCodeCollector::new();
208 collector.record_success();
209 collector.record_error();
210
211 assert_eq!(collector.finalize(), ExitCode::Warning);
212 }
213
214 #[test]
215 fn distributed_read_outcomes_have_distinct_exit_classes() {
216 assert_eq!(
217 DistributedReadOutcome::Success.exit_code(),
218 ExitCode::Success
219 );
220 assert_eq!(
221 DistributedReadOutcome::Unsupported.exit_code(),
222 ExitCode::Unsupported
223 );
224 assert_eq!(
225 DistributedReadOutcome::AuthorizationFailure.exit_code(),
226 ExitCode::Authorization
227 );
228 assert_eq!(
229 DistributedReadOutcome::RetryableFailure.exit_code(),
230 ExitCode::Retryable
231 );
232 assert_eq!(
233 DistributedReadOutcome::TerminalFailure.exit_code(),
234 ExitCode::Error
235 );
236 assert_eq!(
237 DistributedReadOutcome::Cancelled.exit_code(),
238 ExitCode::Interrupted
239 );
240 }
241}