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 keybind: crate::model::KeybindConfig::default(),
110 abbr: abbrs,
111 }
112 }
113
114 fn abbr_when(key: &str, exp: &str, cmds: Vec<&str>) -> Abbr {
115 Abbr {
116 key: key.into(),
117 expand: exp.into(),
118 when_command_exists: Some(cmds.into_iter().map(String::from).collect()),
119 }
120 }
121
122 #[test]
123 fn all_healthy() {
124 let dir = tempfile::tempdir().unwrap();
125 let path = dir.path().join("config.toml");
126 let mut f = std::fs::File::create(&path).unwrap();
127 writeln!(f, "version = 1").unwrap();
128
129 let cfg = test_config(vec![abbr_when("ls", "lsd", vec!["lsd"])]);
130 let result = diagnose(&path, Some(&cfg), |_| true);
131
132 assert!(result.is_healthy());
133 assert_eq!(result.checks[0].status, CheckStatus::Ok); assert_eq!(result.checks[1].status, CheckStatus::Ok); assert_eq!(result.checks[2].status, CheckStatus::Ok); }
137
138 #[test]
139 fn config_file_missing() {
140 let path = std::path::PathBuf::from("/nonexistent/config.toml");
141 let result = diagnose(&path, None, |_| true);
142
143 assert!(!result.is_healthy());
144 assert_eq!(result.checks[0].status, CheckStatus::Error);
145 assert_eq!(result.checks[1].status, CheckStatus::Error);
146 }
147
148 #[test]
149 fn command_not_found_is_warn() {
150 let dir = tempfile::tempdir().unwrap();
151 let path = dir.path().join("config.toml");
152 std::fs::write(&path, "version = 1").unwrap();
153
154 let cfg = test_config(vec![abbr_when("ls", "lsd", vec!["lsd"])]);
155 let result = diagnose(&path, Some(&cfg), |_| false);
156
157 assert!(result.is_healthy());
159 assert_eq!(result.checks[2].status, CheckStatus::Warn);
160 assert!(result.checks[2].detail.contains("not found"));
161 }
162
163 #[test]
164 fn diag_result_is_healthy_with_error() {
165 let result = DiagResult {
166 checks: vec![Check {
167 name: "test".into(),
168 status: CheckStatus::Error,
169 detail: "bad".into(),
170 }],
171 };
172 assert!(!result.is_healthy());
173 }
174}