1use std::path::Path;
2
3use crate::model::Config;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum CheckStatus {
7 Ok,
8 Warn,
9 Error,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Check {
14 pub name: String,
15 pub status: CheckStatus,
16 pub detail: String,
17}
18
19#[derive(Debug, Clone)]
20pub struct DiagResult {
21 pub checks: Vec<Check>,
22}
23
24impl DiagResult {
25 pub fn is_healthy(&self) -> bool {
26 self.checks
27 .iter()
28 .all(|c| c.status == CheckStatus::Ok || c.status == CheckStatus::Warn)
29 }
30}
31
32pub fn diagnose<F>(config_path: &Path, config: Option<&Config>, command_exists: F) -> DiagResult
37where
38 F: Fn(&str) -> bool,
39{
40 let mut checks = Vec::new();
41
42 let config_exists = config_path.exists();
44 checks.push(Check {
45 name: "config_file".into(),
46 status: if config_exists {
47 CheckStatus::Ok
48 } else {
49 CheckStatus::Error
50 },
51 detail: if config_exists {
52 format!("found: {}", config_path.display())
53 } else {
54 format!("not found: {}", config_path.display())
55 },
56 });
57
58 checks.push(Check {
60 name: "config_parse".into(),
61 status: if config.is_some() {
62 CheckStatus::Ok
63 } else {
64 CheckStatus::Error
65 },
66 detail: if config.is_some() {
67 "config loaded successfully".into()
68 } else {
69 "failed to load config".into()
70 },
71 });
72
73 if let Some(cfg) = config {
75 for abbr in &cfg.abbr {
76 if let Some(cmds) = &abbr.when_command_exists {
77 for cmd in cmds {
78 let exists = command_exists(cmd);
79 checks.push(Check {
80 name: format!("command:{cmd}"),
81 status: if exists {
82 CheckStatus::Ok
83 } else {
84 CheckStatus::Warn
85 },
86 detail: if exists {
87 format!("'{cmd}' found (required by '{}')", abbr.key)
88 } else {
89 format!("'{cmd}' not found (required by '{}')", abbr.key)
90 },
91 });
92 }
93 }
94 }
95 }
96
97 DiagResult { checks }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::model::{Abbr, Config};
104 use std::io::Write;
105
106 fn test_config(abbrs: Vec<Abbr>) -> Config {
107 Config {
108 version: 1,
109 abbr: abbrs,
110 }
111 }
112
113 fn abbr_when(key: &str, exp: &str, cmds: Vec<&str>) -> Abbr {
114 Abbr {
115 key: key.into(),
116 expand: exp.into(),
117 when_command_exists: Some(cmds.into_iter().map(String::from).collect()),
118 }
119 }
120
121 #[test]
122 fn all_healthy() {
123 let dir = tempfile::tempdir().unwrap();
124 let path = dir.path().join("config.toml");
125 let mut f = std::fs::File::create(&path).unwrap();
126 writeln!(f, "version = 1").unwrap();
127
128 let cfg = test_config(vec![abbr_when("ls", "lsd", vec!["lsd"])]);
129 let result = diagnose(&path, Some(&cfg), |_| true);
130
131 assert!(result.is_healthy());
132 assert_eq!(result.checks[0].status, CheckStatus::Ok); assert_eq!(result.checks[1].status, CheckStatus::Ok); assert_eq!(result.checks[2].status, CheckStatus::Ok); }
136
137 #[test]
138 fn config_file_missing() {
139 let path = std::path::PathBuf::from("/nonexistent/config.toml");
140 let result = diagnose(&path, None, |_| true);
141
142 assert!(!result.is_healthy());
143 assert_eq!(result.checks[0].status, CheckStatus::Error);
144 assert_eq!(result.checks[1].status, CheckStatus::Error);
145 }
146
147 #[test]
148 fn command_not_found_is_warn() {
149 let dir = tempfile::tempdir().unwrap();
150 let path = dir.path().join("config.toml");
151 std::fs::write(&path, "version = 1").unwrap();
152
153 let cfg = test_config(vec![abbr_when("ls", "lsd", vec!["lsd"])]);
154 let result = diagnose(&path, Some(&cfg), |_| false);
155
156 assert!(result.is_healthy());
158 assert_eq!(result.checks[2].status, CheckStatus::Warn);
159 assert!(result.checks[2].detail.contains("not found"));
160 }
161
162 #[test]
163 fn diag_result_is_healthy_with_error() {
164 let result = DiagResult {
165 checks: vec![Check {
166 name: "test".into(),
167 status: CheckStatus::Error,
168 detail: "bad".into(),
169 }],
170 };
171 assert!(!result.is_healthy());
172 }
173}