Skip to main content

pitchfork_cli/
daemon_status.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumIs)]
4#[strum(serialize_all = "snake_case")]
5#[serde(rename_all = "snake_case")]
6#[derive(Default)]
7pub enum DaemonStatus {
8    Failed(String),
9    Waiting,
10    Running,
11    Stopping,
12    /// Exit code of the process, or -1 if unknown.
13    Errored(i32),
14    /// A `oneshot = true` daemon whose process ran to completion with exit
15    /// code 0.
16    ///
17    /// Distinct from `Stopped` so the CLI, the TUI and the web UI can tell
18    /// "finished its work" from "never ran" or "was interrupted", and so a
19    /// failed run stays distinguishable from a successful one.
20    ///
21    /// It does not mark the task as permanently done: a start re-runs a
22    /// completed oneshot, including when it is reached as a dependency, which
23    /// is why the guide requires the command to be idempotent. What waits on
24    /// a oneshot is the start that is running it, not this status.
25    Completed,
26    #[default]
27    Stopped,
28}
29
30impl DaemonStatus {
31    pub fn style(&self) -> String {
32        let s = self.to_string();
33        match self {
34            DaemonStatus::Failed(_) => console::style(s).red().to_string(),
35            DaemonStatus::Waiting => console::style(s).yellow().to_string(),
36            DaemonStatus::Running => console::style(s).green().to_string(),
37            DaemonStatus::Stopping => console::style(s).yellow().to_string(),
38            DaemonStatus::Stopped => console::style(s).dim().to_string(),
39            DaemonStatus::Completed => console::style(s).green().dim().to_string(),
40            DaemonStatus::Errored(_) => console::style(s).red().to_string(),
41        }
42    }
43
44    pub fn error_message(&self) -> Option<String> {
45        match self {
46            DaemonStatus::Failed(msg) => Some(msg.clone()),
47            DaemonStatus::Errored(code) if *code != -1 => Some(format!("exit code {code}")),
48            DaemonStatus::Errored(_) => Some("unknown exit code".to_string()),
49            _ => None,
50        }
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    fn all_variants() -> Vec<(&'static str, DaemonStatus)> {
59        vec![
60            ("running", DaemonStatus::Running),
61            ("stopped", DaemonStatus::Stopped),
62            ("waiting", DaemonStatus::Waiting),
63            ("stopping", DaemonStatus::Stopping),
64            ("failed", DaemonStatus::Failed("some error".to_string())),
65            ("errored", DaemonStatus::Errored(1)),
66            ("errored_unknown", DaemonStatus::Errored(-1)),
67            ("completed", DaemonStatus::Completed),
68        ]
69    }
70
71    #[test]
72    fn test_completed_serializes_as_completed() {
73        // `mise daemons ls` and other consumers read this string; keep it
74        // stable and distinct from "stopped".
75        assert_eq!(DaemonStatus::Completed.to_string(), "completed");
76        assert_eq!(
77            serde_json::to_string(&DaemonStatus::Completed).unwrap(),
78            "\"completed\""
79        );
80    }
81
82    #[test]
83    fn test_completed_has_no_error_message() {
84        assert!(DaemonStatus::Completed.error_message().is_none());
85    }
86
87    #[test]
88    fn test_daemon_status_json_roundtrip() {
89        for (name, status) in all_variants() {
90            let json_str = serde_json::to_string(&status)
91                .unwrap_or_else(|_| panic!("Failed to serialize {name}"));
92            let result: Result<DaemonStatus, _> = serde_json::from_str(&json_str);
93            assert!(
94                result.is_ok(),
95                "Failed to deserialize {name}: {:?}",
96                result.err()
97            );
98        }
99    }
100
101    #[test]
102    fn test_daemon_status_toml_roundtrip() {
103        #[derive(Serialize, Deserialize, Debug)]
104        struct Wrapper {
105            status: DaemonStatus,
106        }
107
108        for (name, status) in all_variants() {
109            let w = Wrapper { status };
110            let toml_str =
111                toml::to_string(&w).unwrap_or_else(|e| panic!("Failed to serialize {name}: {e}"));
112            let result: Result<Wrapper, _> = toml::from_str(&toml_str);
113            assert!(
114                result.is_ok(),
115                "Failed to deserialize {name}: {:?}\nTOML was: {toml_str:?}",
116                result.err()
117            );
118        }
119    }
120}