Skip to main content

qtcloud_devops_cli/
doctor.rs

1/// doctor 命令:检查系统依赖的外部命令状态。
2use std::io::Write;
3use std::path::Path;
4use std::process::Command;
5
6/// 向 stdout 输出系统诊断信息。
7pub fn status(repo_path: &Path) {
8    let mut stdout = std::io::stdout();
9    let _ = status_to(&mut stdout, repo_path);
10}
11
12/// 将系统诊断信息写入指定的 writer。
13pub fn status_to(writer: &mut impl Write, repo_path: &Path) -> std::io::Result<()> {
14    writeln!(writer, "系统诊断")?;
15    writeln!(writer, "{}", "-".repeat(50))?;
16
17    // 检测项目用到的语言
18    let c = crate::contract::load(repo_path);
19    let mut used_langs: Vec<String> = Vec::new();
20    if c.scopes.is_empty() {
21        used_langs = crate::contract::detect_all_languages(repo_path);
22    } else {
23        for s in &c.scopes {
24            let scope_dir = repo_path.join(&s.dir);
25            let mut langs = crate::contract::detect_all_languages(&scope_dir);
26            used_langs.append(&mut langs);
27        }
28    }
29    used_langs.sort();
30    used_langs.dedup();
31
32    // 始终显示的工具
33    writeln!(
34        writer,
35        "  {:<12} {}",
36        "git",
37        check_command("git", &["--version"])
38    )?;
39    write_gh_status(writer)?;
40
41    // 按语言显示工具链
42    for lang in &["rust", "python", "go", "dart", "typescript"] {
43        if !used_langs.iter().any(|l| l == lang) {
44            continue;
45        }
46        match *lang {
47            "rust" => {
48                writeln!(
49                    writer,
50                    "  {:<12} {}",
51                    "cargo",
52                    check_command("cargo", &["--version"])
53                )?;
54                writeln!(
55                    writer,
56                    "  {:<12} {}",
57                    "rustc",
58                    check_command("rustc", &["--version"])
59                )?;
60            }
61            "python" => {
62                writeln!(
63                    writer,
64                    "  {:<12} {}",
65                    "python",
66                    check_command("python", &["--version"])
67                )?;
68                for sub in &["uv", "pytest", "coverage"] {
69                    let v = check_command(sub, &["--version"]);
70                    writeln!(writer, "    {:<10} {}", sub, v)?;
71                }
72            }
73            "go" => {
74                writeln!(
75                    writer,
76                    "  {:<12} {}",
77                    "go",
78                    check_command("go", &["version"])
79                )?;
80            }
81            "dart" => {
82                writeln!(
83                    writer,
84                    "  {:<12} {}",
85                    "flutter",
86                    check_command("flutter", &["--version"])
87                )?;
88                let v = check_command("dart", &["--version"]);
89                writeln!(writer, "    {:<10} {}", "dart", v)?;
90            }
91            "typescript" => {
92                writeln!(
93                    writer,
94                    "  {:<12} {}",
95                    "node",
96                    check_command("node", &["--version"])
97                )?;
98                for sub in &["npm", "npx"] {
99                    let v = check_command(sub, &["--version"]);
100                    writeln!(writer, "    {:<10} {}", sub, v)?;
101                }
102            }
103            _ => {}
104        }
105    }
106
107    Ok(())
108}
109
110fn write_gh_status(writer: &mut impl Write) -> std::io::Result<()> {
111    let ver = check_command("gh", &["--version"]);
112    writeln!(writer, "  {:<12} {}", "gh", ver)?;
113    match Command::new("gh").args(["auth", "status"]).output() {
114        Ok(out) if out.status.success() => {
115            let msg = String::from_utf8_lossy(&out.stdout).trim().to_string();
116            let auth_line = msg.lines().nth(1).map(|l| l.trim()).unwrap_or("");
117            writeln!(writer, "                  ✅ {}", auth_line)?;
118        }
119        Ok(out) => {
120            let msg = String::from_utf8_lossy(&out.stderr).trim().to_string();
121            writeln!(
122                writer,
123                "                  ❌ {}",
124                msg.lines().next().unwrap_or("")
125            )?;
126        }
127        Err(_) => writeln!(writer, "                  ❌ 未登录")?,
128    }
129    Ok(())
130}
131
132fn check_command(cmd: &str, args: &[&str]) -> String {
133    match Command::new(cmd).args(args).output() {
134        Ok(out) if out.status.success() => {
135            let ver = String::from_utf8_lossy(&out.stdout)
136                .lines()
137                .next()
138                .unwrap_or("")
139                .trim()
140                .to_string();
141            format!("✅ {}", ver)
142        }
143        Ok(out) => {
144            let msg = String::from_utf8_lossy(&out.stderr).trim().to_string();
145            format!("❌ {}", msg)
146        }
147        Err(e) => match e.kind() {
148            std::io::ErrorKind::NotFound => format!("❌ 未安装"),
149            _ => format!("❌ {}", e),
150        },
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_check_git_exists() {
160        let result = check_command("git", &["--version"]);
161        assert!(result.starts_with("✅"), "git 应存在: {}", result);
162    }
163
164    #[test]
165    fn test_check_nonexistent() {
166        let result = check_command("nonexistent_cmd_xyz", &["--version"]);
167        assert!(
168            result.contains("未安装"),
169            "不存在的命令应报未安装: {}",
170            result
171        );
172    }
173
174    #[test]
175    fn test_status_to_python() {
176        let d = tempfile::tempdir().unwrap();
177        // 创建 pyproject.toml 模拟 Python 项目
178        std::fs::write(
179            d.path().join("pyproject.toml"),
180            "[project]\nname = \"test\"\n",
181        )
182        .unwrap();
183        let mut buf = Vec::new();
184        status_to(&mut buf, d.path()).unwrap();
185        let output = String::from_utf8(buf).expect("非 UTF-8 输出");
186        assert!(output.contains("git"), "应包含 git");
187        assert!(output.contains("python"), "Python 项目应显示 python");
188    }
189
190    #[test]
191    fn test_status_to_go() {
192        let d = tempfile::tempdir().unwrap();
193        std::fs::write(d.path().join("go.mod"), "module test\n").unwrap();
194        let mut buf = Vec::new();
195        status_to(&mut buf, d.path()).unwrap();
196        let output = String::from_utf8(buf).expect("非 UTF-8 输出");
197        assert!(output.contains("go"), "Go 项目应显示 go 工具链");
198    }
199
200    #[test]
201    fn test_status_to_typescript() {
202        let d = tempfile::tempdir().unwrap();
203        std::fs::write(d.path().join("package.json"), "{\"name\":\"test\"}\n").unwrap();
204        let mut buf = Vec::new();
205        status_to(&mut buf, d.path()).unwrap();
206        let output = String::from_utf8(buf).expect("非 UTF-8 输出");
207        assert!(output.contains("node"), "TS 项目应显示 node 工具链");
208    }
209
210    #[test]
211    fn test_status_to_dart() {
212        let d = tempfile::tempdir().unwrap();
213        std::fs::write(d.path().join("pubspec.yaml"), "name: test\n").unwrap();
214        let mut buf = Vec::new();
215        status_to(&mut buf, d.path()).unwrap();
216        let output = String::from_utf8(buf).expect("非 UTF-8 输出");
217        assert!(output.contains("flutter"), "Dart 项目应显示 flutter 工具链");
218    }
219
220    #[test]
221    fn test_status_to_no_lang() {
222        let d = tempfile::tempdir().unwrap();
223        // 空目录,无项目文件
224        let mut buf = Vec::new();
225        status_to(&mut buf, d.path()).unwrap();
226        let output = String::from_utf8(buf).expect("非 UTF-8 输出");
227        assert!(output.contains("git"), "应始终显示 git");
228        assert!(output.contains("gh"), "应始终显示 gh");
229        // 不应包含 cargo/python/go 等语言特定工具
230        // 空目录无法识别语言,只显示 git/gh
231    }
232
233    #[test]
234    fn test_status_to_output() {
235        let d = tempfile::tempdir().unwrap();
236        // 创建 Cargo.toml 让语言检测到 rust
237        std::fs::write(d.path().join("Cargo.toml"), "[package]\n").unwrap();
238        let mut buf = Vec::new();
239        status_to(&mut buf, d.path()).unwrap();
240        let output = String::from_utf8(buf).expect("非 UTF-8 输出");
241
242        // 检查始终存在的结构元素
243        assert!(output.contains("系统诊断"), "应包含标题");
244        assert!(output.contains(&"-".repeat(50)), "应包含分隔线");
245        assert!(output.contains("git"), "应包含 git");
246        assert!(output.contains("gh"), "应包含 gh");
247        // Rust 工具链(根据 Cargo.toml 检测)
248        assert!(output.contains("cargo"), "应包含 cargo");
249        assert!(output.contains("rustc"), "应包含 rustc");
250    }
251}