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