Skip to main content

solti_api/grpc/convert/
run.rs

1//! # Run Conversion
2//!
3//! Converts domain run history into protobuf response values.
4//! Embedded run history has no public wire representation.
5
6use solti_model::TaskRun;
7
8use super::time::system_time_to_ms;
9use crate::error::ApiError;
10use crate::proto_api;
11use crate::visibility::run_is_visible;
12
13impl TryFrom<TaskRun> for proto_api::TaskRunInfo {
14    type Error = ApiError;
15
16    fn try_from(run: TaskRun) -> Result<Self, Self::Error> {
17        if !run_is_visible(&run) {
18            return Err(ApiError::Internal(
19                "handler returned an Embedded task run with no wire representation".into(),
20            ));
21        }
22
23        let (workload, generation, attempt, phase, started_at, finished_at, error, exit_code) =
24            run.into_parts();
25        Ok(proto_api::TaskRunInfo {
26            workload_api_version: workload.api_version().to_owned(),
27            workload_kind: workload.kind().to_owned(),
28            generation,
29            phase: proto_api::TaskPhase::try_from(phase)? as i32,
30            finished_at: finished_at.map(system_time_to_ms).transpose()?,
31            started_at: system_time_to_ms(started_at)?,
32            exit_code,
33            attempt,
34            error,
35        })
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42    use solti_model::{TaskPhase, WORKLOAD_API_VERSION, WorkloadTypeMeta};
43    use std::time::{Duration, UNIX_EPOCH};
44
45    #[test]
46    fn run_converts_all_fields() {
47        let started = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
48        let finished = UNIX_EPOCH + Duration::from_millis(1_700_000_001_500);
49
50        let workload = WorkloadTypeMeta::new("example.io/v1", "DatabaseBackup").unwrap();
51        let run = TaskRun::from_parts(
52            workload,
53            3,
54            2,
55            TaskPhase::Failed,
56            started,
57            Some(finished),
58            Some("boom".into()),
59            Some(137),
60        )
61        .unwrap();
62
63        let proto = proto_api::TaskRunInfo::try_from(run).unwrap();
64
65        assert_eq!(proto.workload_api_version, "example.io/v1");
66        assert_eq!(proto.workload_kind, "DatabaseBackup");
67        assert_eq!(proto.attempt, 2);
68        assert_eq!(proto.generation, 3);
69        assert_eq!(proto.phase, proto_api::TaskPhase::Failed as i32);
70        assert_eq!(proto.started_at, 1_700_000_000_000);
71        assert_eq!(proto.finished_at, Some(1_700_000_001_500));
72        assert_eq!(proto.error.as_deref(), Some("boom"));
73        assert_eq!(proto.exit_code, Some(137));
74    }
75
76    #[test]
77    fn run_active_has_no_finished_timestamp() {
78        let workload = WorkloadTypeMeta::new(WORKLOAD_API_VERSION, "Subprocess").unwrap();
79        let run = TaskRun::starting(1, 1, workload).unwrap();
80        let proto = proto_api::TaskRunInfo::try_from(run).unwrap();
81        assert_eq!(proto.finished_at, None);
82        assert_eq!(proto.exit_code, None);
83    }
84
85    #[test]
86    fn embedded_run_is_not_exposed() {
87        let workload = WorkloadTypeMeta::new(WORKLOAD_API_VERSION, "Embedded").unwrap();
88        let run = TaskRun::starting(1, 1, workload).unwrap();
89
90        let error = proto_api::TaskRunInfo::try_from(run).unwrap_err();
91        assert!(matches!(error, ApiError::Internal(message) if message.contains("Embedded")));
92    }
93}