Skip to main content

release_kit/commands/
doctor.rs

1//! `rk doctor`: is this host ready?
2//!
3//! Runs the whole probe catalog from `crate::probes` and reports by
4//! class. The catalog is shared: a mutating command guards the subset it
5//! depends on with the same probes, so what the doctor says and what a
6//! command refuses on cannot drift apart.
7
8use serde::Serialize;
9
10use crate::cli::doctor::DoctorArgs;
11use crate::error::RkError;
12use crate::output::Output;
13use crate::probes::{self, ProbeClass, ProbeResult, ProbeStatus};
14
15/// The machine form of the doctor report.
16#[derive(Debug, Serialize)]
17struct Report {
18    /// The shape version of this document.
19    schema: &'static str,
20    /// Every probe's answer, in catalog order.
21    probes: Vec<ProbeResult>,
22    /// What plausibly follows.
23    next: Vec<String>,
24}
25
26/// Run the catalog and report it.
27///
28/// # Errors
29///
30/// Returns [`RkError::Other`] only when the report cannot serialize; probe
31/// failures are results, not errors, and the exit code stays 0.
32pub fn run(args: &DoctorArgs) -> Result<(), RkError> {
33    let out = Output::new(args.json);
34    let probes = probes::run_all();
35    let next = vec!["rk usage lists every verb and flag in one call".to_owned()];
36
37    for class in [ProbeClass::Hard, ProbeClass::Soft] {
38        out.result_line(match class {
39            ProbeClass::Hard => "hard",
40            ProbeClass::Soft => "soft",
41        });
42        for probe in probes.iter().filter(|probe| probe.class == class) {
43            let status = match probe.status {
44                ProbeStatus::Ok => "ok    ",
45                ProbeStatus::Failed => "failed",
46            };
47            out.result_line(format!("  {status}  {}: {}", probe.id, probe.message));
48            if let Some(remediation) = &probe.remediation {
49                out.result_line(format!("          next: {remediation}"));
50            }
51        }
52    }
53    out.next(&next);
54
55    out.emit(&Report {
56        schema: "rk.doctor/1",
57        probes,
58        next,
59    })
60}
61
62#[cfg(test)]
63mod tests {
64    #![allow(clippy::expect_used)]
65
66    use crate::probes::{ProbeClass, ProbeResult, ProbeStatus};
67
68    /// The complete `rk.doctor/1` shape, held by snapshot.
69    #[test]
70    fn the_doctor_report_schema_snapshot_holds() {
71        let report = super::Report {
72            schema: "rk.doctor/1",
73            probes: vec![ProbeResult {
74                id: "sh",
75                class: ProbeClass::Hard,
76                status: ProbeStatus::Ok,
77                message: "sh runs".into(),
78                remediation: None,
79            }],
80            next: vec!["rk usage lists every verb and flag in one call".into()],
81        };
82        assert_eq!(
83            serde_json::to_string(&report).expect("a report serializes"),
84            r#"{"schema":"rk.doctor/1","probes":[{"id":"sh","class":"hard","status":"ok","message":"sh runs"}],"next":["rk usage lists every verb and flag in one call"]}"#
85        );
86    }
87
88    /// The `rk.doctor/1` probe shape, held by snapshot.
89    #[test]
90    fn the_probe_schema_snapshot_holds() {
91        let ok = ProbeResult {
92            id: "sh",
93            class: ProbeClass::Hard,
94            status: ProbeStatus::Ok,
95            message: "sh runs".into(),
96            remediation: None,
97        };
98        assert_eq!(
99            serde_json::to_string(&ok).expect("a probe serializes"),
100            r#"{"id":"sh","class":"hard","status":"ok","message":"sh runs"}"#
101        );
102        let failed = ProbeResult {
103            id: "gh-auth",
104            class: ProbeClass::Soft,
105            status: ProbeStatus::Failed,
106            message: "gh is not authenticated".into(),
107            remediation: Some("run gh auth login".into()),
108        };
109        assert_eq!(
110            serde_json::to_string(&failed).expect("a probe serializes"),
111            r#"{"id":"gh-auth","class":"soft","status":"failed","message":"gh is not authenticated","remediation":"run gh auth login"}"#
112        );
113    }
114}