use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::error::CliError;
#[derive(Debug, Clone)]
pub struct CargoCheckResult {
pub success: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
pub struct CargoChecker;
const CHECK_TIMEOUT: Duration = Duration::from_secs(30);
impl CargoChecker {
pub async fn check(plugin_root: &Path) -> Result<CargoCheckResult, CliError> {
let output = tokio::time::timeout(
CHECK_TIMEOUT,
tokio::process::Command::new("cargo")
.arg("check")
.current_dir(plugin_root)
.output(),
)
.await
.map_err(|_| CliError::Generic("cargo check timeout".to_string()))?
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
CliError::Generic("cargo not found".to_string())
} else {
CliError::Io(e)
}
})?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let mut errors = Vec::new();
let mut warnings = Vec::new();
for line in stderr.lines() {
if line.starts_with("error") || line.contains("error[") {
errors.push(line.to_string());
} else if line.starts_with("warning") {
warnings.push(line.to_string());
}
}
for line in stdout.lines() {
if line.starts_with("error") || line.contains("error[") {
errors.push(line.to_string());
} else if line.starts_with("warning") {
warnings.push(line.to_string());
}
}
let success = output.status.success() && errors.is_empty();
Ok(CargoCheckResult {
success,
errors,
warnings,
})
}
pub async fn rollback(files: &[PathBuf]) -> Vec<(PathBuf, std::io::Error)> {
let mut failures = Vec::new();
for file in files {
if let Err(e) = tokio::fs::remove_file(file).await {
eprintln!("Warning: failed to remove {}: {e}", file.display());
failures.push((file.clone(), e));
}
}
failures
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_check_nonexistent_dir() {
let result = CargoChecker::check(Path::new("/nonexistent/path/12345")).await;
assert!(result.is_err());
}
#[tokio::test]
#[ignore = "需要完整 workspace + sz-orm 路径依赖,CI 中由 Check job 覆盖"]
async fn test_check_current_workspace() {
let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_path_buf();
let result = CargoChecker::check(&workspace_root).await;
assert!(
result.is_ok(),
"cargo check should succeed: {:?}",
result.err()
);
let check_result = result.unwrap();
assert!(check_result.success, "workspace should compile");
}
#[tokio::test]
async fn test_rollback_empty_list() {
let failures = CargoChecker::rollback(&[]).await;
assert!(failures.is_empty());
}
#[tokio::test]
async fn test_rollback_nonexistent_file() {
let temp = tempfile::tempdir().expect("tempdir failed");
let nonexistent = temp.path().join("nonexistent.rs");
let failures = CargoChecker::rollback(&[nonexistent]).await;
assert_eq!(failures.len(), 1);
}
#[tokio::test]
async fn test_rollback_existing_file() {
let temp = tempfile::tempdir().expect("tempdir failed");
let file = temp.path().join("test.txt");
tokio::fs::write(&file, "test content")
.await
.expect("write failed");
assert!(file.exists());
let failures = CargoChecker::rollback(std::slice::from_ref(&file)).await;
assert!(failures.is_empty());
assert!(!file.exists());
}
#[tokio::test]
async fn test_check_simple_project_success() {
let temp = tempfile::tempdir().expect("tempdir failed");
tokio::fs::write(
temp.path().join("Cargo.toml"),
"[package]\nname = \"test_proj\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n",
)
.await
.unwrap();
tokio::fs::create_dir_all(temp.path().join("src"))
.await
.unwrap();
tokio::fs::write(
temp.path().join("src/main.rs"),
"fn main() { println!(\"hello\"); }\n",
)
.await
.unwrap();
let result = CargoChecker::check(temp.path()).await;
assert!(result.is_ok(), "cargo check 应成功: {:?}", result.err());
let check_result = result.unwrap();
assert!(check_result.success, "简单项目应编译成功");
}
#[tokio::test]
async fn test_check_project_with_compile_error() {
let temp = tempfile::tempdir().expect("tempdir failed");
tokio::fs::write(
temp.path().join("Cargo.toml"),
"[package]\nname = \"test_err\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n",
)
.await
.unwrap();
tokio::fs::create_dir_all(temp.path().join("src"))
.await
.unwrap();
tokio::fs::write(
temp.path().join("src/main.rs"),
"fn main() { let x: i32 = \"not a number\"; }\n",
)
.await
.unwrap();
let result = CargoChecker::check(temp.path()).await;
assert!(result.is_ok(), "cargo check 应返回结果: {:?}", result.err());
let check_result = result.unwrap();
assert!(!check_result.success, "有编译错误时 success 应为 false");
assert!(!check_result.errors.is_empty(), "应捕获编译错误");
}
}