sz_rust_cli/
cargo_checker.rs1use std::path::{Path, PathBuf};
9use std::time::Duration;
10
11use crate::error::CliError;
12
13#[derive(Debug, Clone)]
15pub struct CargoCheckResult {
16 pub success: bool,
18 pub errors: Vec<String>,
20 pub warnings: Vec<String>,
22}
23
24pub struct CargoChecker;
26
27const CHECK_TIMEOUT: Duration = Duration::from_secs(30);
29
30impl CargoChecker {
31 pub async fn check(plugin_root: &Path) -> Result<CargoCheckResult, CliError> {
40 let output = tokio::time::timeout(
41 CHECK_TIMEOUT,
42 tokio::process::Command::new("cargo")
43 .arg("check")
44 .current_dir(plugin_root)
45 .output(),
46 )
47 .await
48 .map_err(|_| CliError::Generic("cargo check timeout".to_string()))?
49 .map_err(|e| {
50 if e.kind() == std::io::ErrorKind::NotFound {
51 CliError::Generic("cargo not found".to_string())
52 } else {
53 CliError::Io(e)
54 }
55 })?;
56
57 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
58 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
59
60 let mut errors = Vec::new();
61 let mut warnings = Vec::new();
62
63 for line in stderr.lines() {
64 if line.starts_with("error") || line.contains("error[") {
65 errors.push(line.to_string());
66 } else if line.starts_with("warning") {
67 warnings.push(line.to_string());
68 }
69 }
70
71 for line in stdout.lines() {
72 if line.starts_with("error") || line.contains("error[") {
73 errors.push(line.to_string());
74 } else if line.starts_with("warning") {
75 warnings.push(line.to_string());
76 }
77 }
78
79 let success = output.status.success() && errors.is_empty();
80
81 Ok(CargoCheckResult {
82 success,
83 errors,
84 warnings,
85 })
86 }
87
88 pub async fn rollback(files: &[PathBuf]) -> Vec<(PathBuf, std::io::Error)> {
92 let mut failures = Vec::new();
93 for file in files {
94 if let Err(e) = tokio::fs::remove_file(file).await {
95 eprintln!("Warning: failed to remove {}: {e}", file.display());
96 failures.push((file.clone(), e));
97 }
98 }
99 failures
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[tokio::test]
108 async fn test_check_nonexistent_dir() {
109 let result = CargoChecker::check(Path::new("/nonexistent/path/12345")).await;
110 assert!(result.is_err());
111 }
112
113 #[tokio::test]
114 #[ignore = "需要完整 workspace + sz-orm 路径依赖,CI 中由 Check job 覆盖"]
115 async fn test_check_current_workspace() {
116 let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
117 .parent()
118 .unwrap()
119 .to_path_buf();
120 let result = CargoChecker::check(&workspace_root).await;
121 assert!(
122 result.is_ok(),
123 "cargo check should succeed: {:?}",
124 result.err()
125 );
126 let check_result = result.unwrap();
127 assert!(check_result.success, "workspace should compile");
128 }
129
130 #[tokio::test]
131 async fn test_rollback_empty_list() {
132 let failures = CargoChecker::rollback(&[]).await;
133 assert!(failures.is_empty());
134 }
135
136 #[tokio::test]
137 async fn test_rollback_nonexistent_file() {
138 let temp = tempfile::tempdir().expect("tempdir failed");
139 let nonexistent = temp.path().join("nonexistent.rs");
140 let failures = CargoChecker::rollback(&[nonexistent]).await;
141 assert_eq!(failures.len(), 1);
142 }
143
144 #[tokio::test]
145 async fn test_rollback_existing_file() {
146 let temp = tempfile::tempdir().expect("tempdir failed");
147 let file = temp.path().join("test.txt");
148 tokio::fs::write(&file, "test content")
149 .await
150 .expect("write failed");
151 assert!(file.exists());
152
153 let failures = CargoChecker::rollback(std::slice::from_ref(&file)).await;
154 assert!(failures.is_empty());
155 assert!(!file.exists());
156 }
157
158 #[tokio::test]
159 async fn test_check_simple_project_success() {
160 let temp = tempfile::tempdir().expect("tempdir failed");
161 tokio::fs::write(
162 temp.path().join("Cargo.toml"),
163 "[package]\nname = \"test_proj\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n",
164 )
165 .await
166 .unwrap();
167 tokio::fs::create_dir_all(temp.path().join("src"))
168 .await
169 .unwrap();
170 tokio::fs::write(
171 temp.path().join("src/main.rs"),
172 "fn main() { println!(\"hello\"); }\n",
173 )
174 .await
175 .unwrap();
176
177 let result = CargoChecker::check(temp.path()).await;
178 assert!(result.is_ok(), "cargo check 应成功: {:?}", result.err());
179 let check_result = result.unwrap();
180 assert!(check_result.success, "简单项目应编译成功");
181 }
182
183 #[tokio::test]
184 async fn test_check_project_with_compile_error() {
185 let temp = tempfile::tempdir().expect("tempdir failed");
186 tokio::fs::write(
187 temp.path().join("Cargo.toml"),
188 "[package]\nname = \"test_err\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n",
189 )
190 .await
191 .unwrap();
192 tokio::fs::create_dir_all(temp.path().join("src"))
193 .await
194 .unwrap();
195 tokio::fs::write(
196 temp.path().join("src/main.rs"),
197 "fn main() { let x: i32 = \"not a number\"; }\n",
198 )
199 .await
200 .unwrap();
201
202 let result = CargoChecker::check(temp.path()).await;
203 assert!(result.is_ok(), "cargo check 应返回结果: {:?}", result.err());
204 let check_result = result.unwrap();
205 assert!(!check_result.success, "有编译错误时 success 应为 false");
206 assert!(!check_result.errors.is_empty(), "应捕获编译错误");
207 }
208}