solti_model/resource/
run.rs1use std::time::SystemTime;
6
7use serde::{Deserialize, Serialize};
8
9use crate::TaskPhase;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct TaskRun {
37 pub attempt: u32,
39 pub phase: TaskPhase,
41 #[serde(with = "super::metadata::time_serde")]
43 pub started_at: SystemTime,
44 #[serde(
46 skip_serializing_if = "Option::is_none",
47 with = "option_time_serde",
48 default
49 )]
50 pub finished_at: Option<SystemTime>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub error: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub exit_code: Option<i32>,
57}
58
59impl TaskRun {
60 pub fn starting(attempt: u32) -> Self {
62 Self {
63 attempt,
64 phase: TaskPhase::Running,
65 started_at: SystemTime::now(),
66 finished_at: None,
67 error: None,
68 exit_code: None,
69 }
70 }
71
72 pub fn finish(&mut self, phase: TaskPhase, error: Option<String>, exit_code: Option<i32>) {
74 self.finished_at = Some(SystemTime::now());
75 self.phase = phase;
76 self.error = error;
77 self.exit_code = exit_code;
78 }
79
80 pub fn is_active(&self) -> bool {
82 self.finished_at.is_none()
83 }
84}
85
86mod option_time_serde {
87 use serde::{Deserialize, Deserializer, Serialize, Serializer};
88 use std::time::{SystemTime, UNIX_EPOCH};
89
90 pub fn serialize<S>(time: &Option<SystemTime>, serializer: S) -> Result<S::Ok, S::Error>
91 where
92 S: Serializer,
93 {
94 match time {
95 Some(t) => {
96 let since_epoch = t
97 .duration_since(UNIX_EPOCH)
98 .map_err(serde::ser::Error::custom)?;
99 let ms = since_epoch.as_secs() * 1_000 + u64::from(since_epoch.subsec_millis());
100 ms.serialize(serializer)
101 }
102 None => serializer.serialize_none(),
103 }
104 }
105
106 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<SystemTime>, D::Error>
107 where
108 D: Deserializer<'de>,
109 {
110 let opt: Option<u64> = Option::deserialize(deserializer)?;
111 Ok(opt.map(|ms| UNIX_EPOCH + std::time::Duration::from_millis(ms)))
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn starting_creates_running_run() {
121 let run = TaskRun::starting(1);
122 assert_eq!(run.attempt, 1);
123 assert_eq!(run.phase, TaskPhase::Running);
124 assert!(run.is_active());
125 assert!(run.finished_at.is_none());
126 assert!(run.error.is_none());
127 assert!(run.exit_code.is_none());
128 }
129
130 #[test]
131 fn finish_closes_run() {
132 let mut run = TaskRun::starting(2);
133 run.finish(TaskPhase::Failed, Some("boom".into()), Some(1));
134
135 assert!(!run.is_active());
136 assert!(run.finished_at.is_some());
137 assert_eq!(run.phase, TaskPhase::Failed);
138 assert_eq!(run.error.as_deref(), Some("boom"));
139 assert_eq!(run.exit_code, Some(1));
140 }
141
142 #[test]
143 fn finish_succeeded_no_error() {
144 let mut run = TaskRun::starting(1);
145 run.finish(TaskPhase::Succeeded, None, None);
146
147 assert!(!run.is_active());
148 assert_eq!(run.phase, TaskPhase::Succeeded);
149 assert!(run.error.is_none());
150 assert!(run.exit_code.is_none());
151 }
152
153 #[test]
154 fn serde_roundtrip_active() {
155 let run = TaskRun::starting(3);
156 let json = serde_json::to_string(&run).unwrap();
157 let back: TaskRun = serde_json::from_str(&json).unwrap();
158
159 assert_eq!(back.attempt, 3);
160 assert_eq!(back.phase, TaskPhase::Running);
161 assert!(back.finished_at.is_none());
162 }
163
164 #[test]
165 fn serde_roundtrip_finished() {
166 let mut run = TaskRun::starting(1);
167 run.finish(TaskPhase::Timeout, Some("timeout".into()), None);
168
169 let json = serde_json::to_string(&run).unwrap();
170 let back: TaskRun = serde_json::from_str(&json).unwrap();
171
172 assert_eq!(back.phase, TaskPhase::Timeout);
173 assert!(back.finished_at.is_some());
174 assert_eq!(back.error.as_deref(), Some("timeout"));
175 }
176
177 #[test]
178 fn serde_skips_none_fields() {
179 let run = TaskRun::starting(1);
180 let json = serde_json::to_string(&run).unwrap();
181 assert!(!json.contains("finishedAt"));
182 assert!(!json.contains("error"));
183 assert!(!json.contains("exitCode"));
184 }
185}