use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use crate::TaskPhase;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskRun {
pub attempt: u32,
pub phase: TaskPhase,
#[serde(with = "super::metadata::time_serde")]
pub started_at: SystemTime,
#[serde(
skip_serializing_if = "Option::is_none",
with = "option_time_serde",
default
)]
pub finished_at: Option<SystemTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
}
impl TaskRun {
pub fn starting(attempt: u32) -> Self {
Self {
attempt,
phase: TaskPhase::Running,
started_at: SystemTime::now(),
finished_at: None,
error: None,
exit_code: None,
}
}
pub fn finish(&mut self, phase: TaskPhase, error: Option<String>, exit_code: Option<i32>) {
self.finished_at = Some(SystemTime::now());
self.phase = phase;
self.error = error;
self.exit_code = exit_code;
}
pub fn is_active(&self) -> bool {
self.finished_at.is_none()
}
}
mod option_time_serde {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::{SystemTime, UNIX_EPOCH};
pub fn serialize<S>(time: &Option<SystemTime>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match time {
Some(t) => {
let since_epoch = t
.duration_since(UNIX_EPOCH)
.map_err(serde::ser::Error::custom)?;
let ms = since_epoch.as_secs() * 1_000 + u64::from(since_epoch.subsec_millis());
ms.serialize(serializer)
}
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<SystemTime>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<u64> = Option::deserialize(deserializer)?;
Ok(opt.map(|ms| UNIX_EPOCH + std::time::Duration::from_millis(ms)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starting_creates_running_run() {
let run = TaskRun::starting(1);
assert_eq!(run.attempt, 1);
assert_eq!(run.phase, TaskPhase::Running);
assert!(run.is_active());
assert!(run.finished_at.is_none());
assert!(run.error.is_none());
assert!(run.exit_code.is_none());
}
#[test]
fn finish_closes_run() {
let mut run = TaskRun::starting(2);
run.finish(TaskPhase::Failed, Some("boom".into()), Some(1));
assert!(!run.is_active());
assert!(run.finished_at.is_some());
assert_eq!(run.phase, TaskPhase::Failed);
assert_eq!(run.error.as_deref(), Some("boom"));
assert_eq!(run.exit_code, Some(1));
}
#[test]
fn finish_succeeded_no_error() {
let mut run = TaskRun::starting(1);
run.finish(TaskPhase::Succeeded, None, None);
assert!(!run.is_active());
assert_eq!(run.phase, TaskPhase::Succeeded);
assert!(run.error.is_none());
assert!(run.exit_code.is_none());
}
#[test]
fn serde_roundtrip_active() {
let run = TaskRun::starting(3);
let json = serde_json::to_string(&run).unwrap();
let back: TaskRun = serde_json::from_str(&json).unwrap();
assert_eq!(back.attempt, 3);
assert_eq!(back.phase, TaskPhase::Running);
assert!(back.finished_at.is_none());
}
#[test]
fn serde_roundtrip_finished() {
let mut run = TaskRun::starting(1);
run.finish(TaskPhase::Timeout, Some("timeout".into()), None);
let json = serde_json::to_string(&run).unwrap();
let back: TaskRun = serde_json::from_str(&json).unwrap();
assert_eq!(back.phase, TaskPhase::Timeout);
assert!(back.finished_at.is_some());
assert_eq!(back.error.as_deref(), Some("timeout"));
}
#[test]
fn serde_skips_none_fields() {
let run = TaskRun::starting(1);
let json = serde_json::to_string(&run).unwrap();
assert!(!json.contains("finishedAt"));
assert!(!json.contains("error"));
assert!(!json.contains("exitCode"));
}
}