purger_core/
cleaner.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use std::process::Command;
5use std::time::Instant;
6use tracing::{debug, error, info};
7
8use crate::CleanResult;
9use crate::project::RustProject;
10
11/// 清理策略
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
13pub enum CleanStrategy {
14    /// 使用cargo clean命令
15    #[default]
16    CargoClean,
17    /// 直接删除target目录
18    DirectDelete,
19}
20
21/// 清理进度信息
22#[derive(Debug, Clone)]
23pub struct CleanProgress {
24    pub project_name: String,
25    pub current_file: Option<String>,
26    pub files_processed: usize,
27    pub total_files: Option<usize>,
28    pub phase: CleanPhase,
29}
30
31/// 清理阶段
32#[derive(Debug, Clone, PartialEq)]
33pub enum CleanPhase {
34    Starting,
35    Analyzing,
36    Cleaning,
37    Finalizing,
38    Complete,
39}
40
41/// 清理器配置
42#[derive(Debug, Clone)]
43pub struct CleanConfig {
44    pub strategy: CleanStrategy,
45    pub dry_run: bool,
46    pub parallel: bool,
47    pub timeout_seconds: u64,
48
49    // 可执行文件保留选项
50    /// 是否保留可执行文件
51    pub keep_executable: bool,
52    /// 可执行文件备份目录(如果为None,则在项目目录下创建executables文件夹)
53    pub executable_backup_dir: Option<PathBuf>,
54}
55
56impl Default for CleanConfig {
57    fn default() -> Self {
58        Self {
59            strategy: CleanStrategy::CargoClean,
60            dry_run: false,
61            parallel: true,
62            timeout_seconds: 30,
63
64            // 可执行文件保留选项默认值
65            keep_executable: false,
66            executable_backup_dir: None,
67        }
68    }
69}
70
71/// 项目清理器
72pub struct ProjectCleaner {
73    config: CleanConfig,
74}
75
76impl ProjectCleaner {
77    /// 创建新的清理器
78    pub fn new(config: CleanConfig) -> Self {
79        Self { config }
80    }
81
82    /// 清理单个项目
83    pub fn clean_project(&self, project: &RustProject) -> Result<u64> {
84        self.clean_project_with_progress(project, |_| {})
85    }
86
87    /// 清理单个项目(带进度回调)
88    pub fn clean_project_with_progress<F>(
89        &self,
90        project: &RustProject,
91        progress_callback: F,
92    ) -> Result<u64>
93    where
94        F: Fn(CleanProgress),
95    {
96        if self.config.dry_run {
97            info!(
98                "DRY RUN: 将清理项目 {} ({})",
99                project.name,
100                project.formatted_size()
101            );
102            return Ok(project.target_size);
103        }
104
105        if !project.has_target {
106            debug!("项目 {} 没有target目录,跳过", project.name);
107            return Ok(0);
108        }
109
110        let size_before = project.target_size;
111        info!(
112            "开始清理项目: {} ({})",
113            project.name,
114            project.formatted_size()
115        );
116
117        // 发送开始进度
118        progress_callback(CleanProgress {
119            project_name: project.name.clone(),
120            current_file: None,
121            files_processed: 0,
122            total_files: None,
123            phase: CleanPhase::Starting,
124        });
125
126        let result = match self.config.strategy {
127            CleanStrategy::CargoClean => {
128                self.clean_with_cargo_progress(project, &progress_callback)
129            }
130            CleanStrategy::DirectDelete => {
131                self.clean_with_delete_progress(project, &progress_callback)
132            }
133        };
134
135        match result {
136            Ok(_) => {
137                // 发送完成进度
138                progress_callback(CleanProgress {
139                    project_name: project.name.clone(),
140                    current_file: None,
141                    files_processed: 0,
142                    total_files: Some(0),
143                    phase: CleanPhase::Complete,
144                });
145                info!("成功清理项目: {}", project.name);
146                Ok(size_before)
147            }
148            Err(e) => {
149                error!("清理项目失败 {}: {}", project.name, e);
150                Err(e)
151            }
152        }
153    }
154
155    /// 批量清理项目
156    pub fn clean_projects(&self, projects: &[RustProject]) -> CleanResult {
157        let start_time = Instant::now();
158        let mut result = CleanResult::new();
159
160        info!("开始清理 {} 个项目", projects.len());
161
162        if self.config.parallel {
163            self.clean_projects_parallel(projects, &mut result);
164        } else {
165            self.clean_projects_sequential(projects, &mut result);
166        }
167
168        result.duration_ms = start_time.elapsed().as_millis() as u64;
169
170        info!(
171            "清理完成: 成功 {} 个,失败 {} 个,释放空间 {},耗时 {}ms",
172            result.cleaned_projects,
173            result.failed_projects.len(),
174            result.format_size(),
175            result.duration_ms
176        );
177
178        result
179    }
180
181    /// 串行清理项目
182    fn clean_projects_sequential(&self, projects: &[RustProject], result: &mut CleanResult) {
183        for project in projects {
184            match self.clean_project(project) {
185                Ok(size_freed) => result.add_success(size_freed),
186                Err(_) => result.add_failure(project.path.to_string_lossy().to_string()),
187            }
188        }
189    }
190
191    /// 并行清理项目(注意:这里简化实现,实际可能需要更复杂的并行控制)
192    fn clean_projects_parallel(&self, projects: &[RustProject], result: &mut CleanResult) {
193        // 由于需要修改result,这里暂时使用串行实现
194        // 在实际应用中,可以使用Arc<Mutex<CleanResult>>或其他并发原语
195        self.clean_projects_sequential(projects, result);
196    }
197
198    /// 使用cargo clean清理
199    #[allow(dead_code)]
200    fn clean_with_cargo(&self, project: &RustProject) -> Result<()> {
201        self.clean_with_cargo_progress(project, &|_| {})
202    }
203
204    /// 使用cargo clean清理(带进度回调)
205    fn clean_with_cargo_progress<F>(
206        &self,
207        project: &RustProject,
208        progress_callback: &F,
209    ) -> Result<()>
210    where
211        F: Fn(CleanProgress),
212    {
213        debug!("使用cargo clean清理项目: {}", project.name);
214
215        // 首先运行 dry-run 来获取文件列表
216        progress_callback(CleanProgress {
217            project_name: project.name.clone(),
218            current_file: None,
219            files_processed: 0,
220            total_files: None,
221            phase: CleanPhase::Analyzing,
222        });
223
224        let file_list = self.get_cargo_clean_file_list(project)?;
225        let total_files = file_list.len();
226
227        progress_callback(CleanProgress {
228            project_name: project.name.clone(),
229            current_file: None,
230            files_processed: 0,
231            total_files: Some(total_files),
232            phase: CleanPhase::Cleaning,
233        });
234
235        // 执行实际的清理
236        let mut cmd = Command::new("cargo");
237        cmd.arg("clean").current_dir(&project.path);
238
239        // 模拟进度更新(因为cargo clean本身不提供实时进度)
240        let handle = std::thread::spawn(move || cmd.output());
241
242        // 在清理过程中模拟进度更新
243        let mut processed = 0;
244        while !handle.is_finished() {
245            if processed < total_files {
246                processed = (processed + total_files / 10).min(total_files);
247                progress_callback(CleanProgress {
248                    project_name: project.name.clone(),
249                    current_file: file_list.get(processed.saturating_sub(1)).cloned(),
250                    files_processed: processed,
251                    total_files: Some(total_files),
252                    phase: CleanPhase::Cleaning,
253                });
254            }
255            std::thread::sleep(std::time::Duration::from_millis(100));
256        }
257
258        let output = handle
259            .join()
260            .map_err(|_| anyhow::anyhow!("清理线程异常"))?
261            .context("执行cargo clean失败")?;
262
263        if !output.status.success() {
264            let stderr = String::from_utf8_lossy(&output.stderr);
265            anyhow::bail!("cargo clean失败: {}", stderr);
266        }
267
268        // 最终进度更新
269        progress_callback(CleanProgress {
270            project_name: project.name.clone(),
271            current_file: None,
272            files_processed: total_files,
273            total_files: Some(total_files),
274            phase: CleanPhase::Finalizing,
275        });
276
277        Ok(())
278    }
279
280    /// 获取cargo clean将要删除的文件列表
281    fn get_cargo_clean_file_list(&self, project: &RustProject) -> Result<Vec<String>> {
282        let mut cmd = Command::new("cargo");
283        cmd.arg("clean").arg("--dry-run").current_dir(&project.path);
284
285        let output = cmd.output().context("执行cargo clean --dry-run失败")?;
286
287        if !output.status.success() {
288            let stderr = String::from_utf8_lossy(&output.stderr);
289            anyhow::bail!("cargo clean --dry-run失败: {}", stderr);
290        }
291
292        let stdout = String::from_utf8_lossy(&output.stdout);
293        let files: Vec<String> = stdout
294            .lines()
295            .filter(|line| !line.trim().is_empty() && !line.contains("Summary"))
296            .map(|line| {
297                // 提取文件名部分
298                if let Some(file_name) = std::path::Path::new(line.trim()).file_name() {
299                    file_name.to_string_lossy().to_string()
300                } else {
301                    line.trim().to_string()
302                }
303            })
304            .collect();
305
306        Ok(files)
307    }
308
309    /// 直接删除target目录
310    #[allow(dead_code)]
311    fn clean_with_delete(&self, project: &RustProject) -> Result<()> {
312        self.clean_with_delete_progress(project, &|_| {})
313    }
314
315    /// 直接删除target目录(带进度回调)
316    fn clean_with_delete_progress<F>(
317        &self,
318        project: &RustProject,
319        progress_callback: &F,
320    ) -> Result<()>
321    where
322        F: Fn(CleanProgress),
323    {
324        debug!("直接删除target目录: {}", project.name);
325
326        let target_path = project.target_path();
327        if !target_path.exists() {
328            return Ok(());
329        }
330
331        progress_callback(CleanProgress {
332            project_name: project.name.clone(),
333            current_file: None,
334            files_processed: 0,
335            total_files: None,
336            phase: CleanPhase::Analyzing,
337        });
338
339        // 如果需要保留可执行文件,先备份
340        if self.config.keep_executable {
341            self.backup_executables(project, progress_callback)?;
342        }
343
344        // 计算文件数量(用于进度显示)
345        let file_count = self.count_files_in_dir(&target_path)?;
346
347        progress_callback(CleanProgress {
348            project_name: project.name.clone(),
349            current_file: Some("target".to_string()),
350            files_processed: 0,
351            total_files: Some(file_count),
352            phase: CleanPhase::Cleaning,
353        });
354
355        // 执行删除
356        std::fs::remove_dir_all(&target_path).context("删除target目录失败")?;
357
358        progress_callback(CleanProgress {
359            project_name: project.name.clone(),
360            current_file: None,
361            files_processed: file_count,
362            total_files: Some(file_count),
363            phase: CleanPhase::Finalizing,
364        });
365
366        Ok(())
367    }
368
369    /// 备份可执行文件
370    fn backup_executables<F>(&self, project: &RustProject, progress_callback: &F) -> Result<()>
371    where
372        F: Fn(CleanProgress),
373    {
374        let target_path = project.target_path();
375        let executables = self.find_executables(&target_path)?;
376
377        if executables.is_empty() {
378            debug!("项目 {} 没有找到可执行文件", project.name);
379            return Ok(());
380        }
381
382        info!(
383            "项目 {} 找到 {} 个可执行文件,开始备份",
384            project.name,
385            executables.len()
386        );
387
388        // 确定备份目录
389        let backup_dir = self.get_backup_directory(project)?;
390        std::fs::create_dir_all(&backup_dir).context("创建备份目录失败")?;
391
392        // 备份每个可执行文件
393        for (i, exe_path) in executables.iter().enumerate() {
394            let file_name = exe_path
395                .file_name()
396                .ok_or_else(|| anyhow::anyhow!("无效的可执行文件路径"))?;
397            let backup_path = backup_dir.join(file_name);
398
399            progress_callback(CleanProgress {
400                project_name: project.name.clone(),
401                current_file: Some(format!("备份 {}", file_name.to_string_lossy())),
402                files_processed: i,
403                total_files: Some(executables.len()),
404                phase: CleanPhase::Cleaning,
405            });
406
407            std::fs::copy(exe_path, &backup_path)
408                .with_context(|| format!("备份可执行文件失败: {exe_path:?} -> {backup_path:?}"))?;
409
410            debug!("备份可执行文件: {:?} -> {:?}", exe_path, backup_path);
411        }
412
413        info!(
414            "成功备份 {} 个可执行文件到 {:?}",
415            executables.len(),
416            backup_dir
417        );
418        Ok(())
419    }
420
421    /// 查找target目录中的可执行文件
422    fn find_executables(&self, target_path: &std::path::Path) -> Result<Vec<PathBuf>> {
423        let mut executables = Vec::new();
424
425        // 检查常见的可执行文件目录
426        let exe_dirs = [target_path.join("debug"), target_path.join("release")];
427
428        for exe_dir in &exe_dirs {
429            if exe_dir.exists() {
430                self.scan_directory_for_executables(exe_dir, &mut executables)?;
431            }
432        }
433
434        // 也检查其他可能的目录(如交叉编译目标)
435        if let Ok(entries) = std::fs::read_dir(target_path) {
436            for entry in entries.flatten() {
437                let path = entry.path();
438                if path.is_dir()
439                    && !path
440                        .file_name()
441                        .unwrap_or_default()
442                        .to_string_lossy()
443                        .starts_with('.')
444                {
445                    // 检查是否是目标架构目录
446                    if let Ok(sub_entries) = std::fs::read_dir(&path) {
447                        for sub_entry in sub_entries.flatten() {
448                            let sub_path = sub_entry.path();
449                            if sub_path.is_dir()
450                                && (sub_path.file_name().unwrap_or_default() == "debug"
451                                    || sub_path.file_name().unwrap_or_default() == "release")
452                            {
453                                self.scan_directory_for_executables(&sub_path, &mut executables)?;
454                            }
455                        }
456                    }
457                }
458            }
459        }
460
461        Ok(executables)
462    }
463
464    /// 扫描目录查找可执行文件
465    fn scan_directory_for_executables(
466        &self,
467        dir: &std::path::Path,
468        executables: &mut Vec<PathBuf>,
469    ) -> Result<()> {
470        if let Ok(entries) = std::fs::read_dir(dir) {
471            for entry in entries.flatten() {
472                let path = entry.path();
473                if path.is_file() && self.is_executable(&path) {
474                    executables.push(path);
475                }
476            }
477        }
478        Ok(())
479    }
480
481    /// 判断文件是否为可执行文件
482    fn is_executable(&self, path: &std::path::Path) -> bool {
483        // 在Windows上检查.exe扩展名
484        #[cfg(target_os = "windows")]
485        {
486            path.extension().is_some_and(|ext| ext == "exe")
487        }
488
489        // 在Unix系统上检查可执行权限
490        #[cfg(not(target_os = "windows"))]
491        {
492            use std::os::unix::fs::PermissionsExt;
493            if let Ok(metadata) = std::fs::metadata(path) {
494                let permissions = metadata.permissions();
495                permissions.mode() & 0o111 != 0
496            } else {
497                false
498            }
499        }
500    }
501
502    /// 获取备份目录
503    fn get_backup_directory(&self, project: &RustProject) -> Result<PathBuf> {
504        if let Some(ref backup_dir) = self.config.executable_backup_dir {
505            // 使用指定的备份目录
506            Ok(backup_dir.join(&project.name))
507        } else {
508            // 在项目目录下创建executables文件夹
509            Ok(project.path.join("executables"))
510        }
511    }
512
513    /// 计算目录中的文件数量
514    fn count_files_in_dir(&self, dir: &std::path::Path) -> Result<usize> {
515        use walkdir::WalkDir;
516
517        let count = WalkDir::new(dir)
518            .into_iter()
519            .filter_map(|entry| entry.ok())
520            .filter(|entry| entry.file_type().is_file())
521            .count();
522
523        Ok(count)
524    }
525
526    /// 预览清理操作(dry run)
527    pub fn preview_clean(&self, projects: &[RustProject]) -> CleanResult {
528        let mut config = self.config.clone();
529        config.dry_run = true;
530
531        let cleaner = ProjectCleaner::new(config);
532        cleaner.clean_projects(projects)
533    }
534
535    /// 检查cargo命令是否可用
536    pub fn check_cargo_available() -> bool {
537        Command::new("cargo")
538            .arg("--version")
539            .output()
540            .map(|output| output.status.success())
541            .unwrap_or(false)
542    }
543}
544
545impl Default for ProjectCleaner {
546    fn default() -> Self {
547        Self::new(CleanConfig::default())
548    }
549}
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554    use std::fs;
555    use std::path::Path;
556    use tempfile::TempDir;
557
558    fn create_test_project_with_target(dir: &Path, name: &str) -> Result<RustProject> {
559        let project_dir = dir.join(name);
560        fs::create_dir_all(&project_dir)?;
561
562        let cargo_toml = format!(
563            r#"
564[package]
565name = "{name}"
566version = "0.1.0"
567edition = "2021"
568"#
569        );
570
571        fs::write(project_dir.join("Cargo.toml"), cargo_toml)?;
572
573        let target_dir = project_dir.join("target");
574        fs::create_dir_all(&target_dir)?;
575        fs::write(
576            target_dir.join("test.txt"),
577            "test content for size calculation",
578        )?;
579
580        RustProject::from_path(&project_dir)
581    }
582
583    #[test]
584    fn test_cleaner_dry_run() -> Result<()> {
585        let temp_dir = TempDir::new()?;
586        let project = create_test_project_with_target(temp_dir.path(), "test_project")?;
587
588        let config = CleanConfig {
589            dry_run: true,
590            ..Default::default()
591        };
592
593        let cleaner = ProjectCleaner::new(config);
594        let size_freed = cleaner.clean_project(&project)?;
595
596        // 在dry run模式下,应该返回原始大小
597        assert_eq!(size_freed, project.target_size);
598
599        // target目录应该仍然存在
600        assert!(project.target_path().exists());
601
602        Ok(())
603    }
604
605    #[test]
606    fn test_cleaner_direct_delete() -> Result<()> {
607        let temp_dir = TempDir::new()?;
608        let project = create_test_project_with_target(temp_dir.path(), "test_project")?;
609
610        let config = CleanConfig {
611            strategy: CleanStrategy::DirectDelete,
612            ..Default::default()
613        };
614
615        let cleaner = ProjectCleaner::new(config);
616        let size_freed = cleaner.clean_project(&project)?;
617
618        // 应该释放了一些空间
619        assert!(size_freed > 0);
620
621        // target目录应该被删除
622        assert!(!project.target_path().exists());
623
624        Ok(())
625    }
626
627    #[test]
628    fn test_check_cargo_available() {
629        // 这个测试可能在某些环境中失败,如果cargo不可用
630        // 在实际项目中,可能需要mock这个功能
631        let available = ProjectCleaner::check_cargo_available();
632        println!("Cargo available: {available}");
633    }
634
635    #[test]
636    fn test_clean_projects_batch() -> Result<()> {
637        let temp_dir = TempDir::new()?;
638        let projects = vec![
639            create_test_project_with_target(temp_dir.path(), "project1")?,
640            create_test_project_with_target(temp_dir.path(), "project2")?,
641        ];
642
643        let config = CleanConfig {
644            strategy: CleanStrategy::DirectDelete,
645            dry_run: false,
646            ..Default::default()
647        };
648
649        let cleaner = ProjectCleaner::new(config);
650        let result = cleaner.clean_projects(&projects);
651
652        assert_eq!(result.cleaned_projects, 2);
653        assert!(result.total_size_freed > 0);
654        assert!(result.failed_projects.is_empty());
655
656        Ok(())
657    }
658
659    #[test]
660    fn test_clean_config_default() {
661        let config = CleanConfig::default();
662        assert_eq!(config.strategy, CleanStrategy::CargoClean);
663        assert!(!config.dry_run);
664        assert!(config.parallel);
665        assert_eq!(config.timeout_seconds, 30);
666        assert!(!config.keep_executable);
667        assert!(config.executable_backup_dir.is_none());
668    }
669
670    #[test]
671    fn test_clean_progress_phases() {
672        let progress = CleanProgress {
673            project_name: "test".to_string(),
674            current_file: Some("test.txt".to_string()),
675            files_processed: 5,
676            total_files: Some(10),
677            phase: CleanPhase::Cleaning,
678        };
679
680        assert_eq!(progress.project_name, "test");
681        assert_eq!(progress.current_file, Some("test.txt".to_string()));
682        assert_eq!(progress.files_processed, 5);
683        assert_eq!(progress.total_files, Some(10));
684        assert_eq!(progress.phase, CleanPhase::Cleaning);
685    }
686
687    #[test]
688    fn test_clean_strategy_default() {
689        let strategy = CleanStrategy::default();
690        assert_eq!(strategy, CleanStrategy::CargoClean);
691    }
692
693    #[test]
694    fn test_clean_with_progress_callback() -> Result<()> {
695        let temp_dir = TempDir::new()?;
696        let project = create_test_project_with_target(temp_dir.path(), "test_project")?;
697
698        let config = CleanConfig {
699            strategy: CleanStrategy::DirectDelete,
700            dry_run: true, // 使用dry run避免实际删除
701            ..Default::default()
702        };
703
704        let cleaner = ProjectCleaner::new(config);
705
706        // 简单测试进度回调不会导致panic
707        let size_freed = cleaner.clean_project_with_progress(&project, |_progress| {
708            // 进度回调被调用,但我们不在这里做任何可变操作
709        })?;
710
711        assert!(size_freed > 0);
712
713        Ok(())
714    }
715
716    #[test]
717    fn test_clean_result_operations() {
718        let mut result = CleanResult::new();
719
720        // 测试初始状态
721        assert_eq!(result.cleaned_projects, 0);
722        assert_eq!(result.total_size_freed, 0);
723        assert!(result.failed_projects.is_empty());
724
725        // 测试添加成功
726        result.add_success(1024);
727        assert_eq!(result.cleaned_projects, 1);
728        assert_eq!(result.total_size_freed, 1024);
729
730        // 测试添加失败
731        result.add_failure("failed_project".to_string());
732        assert_eq!(result.failed_projects.len(), 1);
733        assert_eq!(result.failed_projects[0], "failed_project");
734
735        // 测试格式化大小
736        let formatted = result.format_size();
737        assert_eq!(formatted, "1.00 KB");
738    }
739
740    #[test]
741    fn test_clean_nonexistent_project() -> Result<()> {
742        let temp_dir = TempDir::new()?;
743        let fake_project = RustProject {
744            path: temp_dir.path().join("nonexistent"),
745            name: "nonexistent".to_string(),
746            target_size: 1000,
747            last_modified: std::time::SystemTime::now(),
748            is_workspace: false,
749            has_target: true,
750        };
751
752        let cleaner = ProjectCleaner::default();
753        let result = cleaner.clean_project(&fake_project);
754
755        // 清理不存在的项目应该返回0或错误
756        if let Ok(size) = result {
757            assert_eq!(size, 0);
758        }
759        // 错误也是可接受的
760
761        Ok(())
762    }
763
764    #[test]
765    fn test_clean_readonly_target() -> Result<()> {
766        let temp_dir = TempDir::new()?;
767        let project = create_test_project_with_target(temp_dir.path(), "readonly_project")?;
768
769        // 尝试将target目录设为只读(在某些系统上可能不起作用)
770        let target_path = project.path.join("target");
771        if target_path.exists() {
772            // 在Windows上设置只读属性
773            #[cfg(windows)]
774            {
775                let mut perms = std::fs::metadata(&target_path)?.permissions();
776                perms.set_readonly(true);
777                let _ = std::fs::set_permissions(&target_path, perms);
778            }
779
780            // 在Unix系统上设置只读权限
781            #[cfg(unix)]
782            {
783                use std::os::unix::fs::PermissionsExt;
784                let _ =
785                    std::fs::set_permissions(&target_path, std::fs::Permissions::from_mode(0o444));
786            }
787        }
788
789        let cleaner = ProjectCleaner::default();
790        let result = cleaner.clean_project(&project);
791
792        // 清理只读目录可能失败,这是预期的
793        // 如果成功了也没关系,失败是预期的
794        let _ = result;
795
796        Ok(())
797    }
798
799    #[test]
800    fn test_clean_with_timeout() -> Result<()> {
801        let temp_dir = TempDir::new()?;
802        let project = create_test_project_with_target(temp_dir.path(), "timeout_project")?;
803
804        // 使用非常短的超时时间
805        let config = CleanConfig {
806            strategy: CleanStrategy::CargoClean,
807            timeout_seconds: 1, // 1秒超时
808            ..Default::default()
809        };
810
811        let cleaner = ProjectCleaner::new(config);
812        let result = cleaner.clean_project(&project);
813
814        // 可能会因为超时而失败,也可能成功(如果操作很快)
815        // 成功也是可能的,超时失败是预期的
816        let _ = result;
817
818        Ok(())
819    }
820
821    #[test]
822    fn test_clean_projects_with_mixed_results() -> Result<()> {
823        let temp_dir = TempDir::new()?;
824
825        // 创建一个正常项目
826        let good_project = create_test_project_with_target(temp_dir.path(), "good_project")?;
827
828        // 创建一个不存在target目录的项目
829        let bad_project_path = temp_dir.path().join("bad_project");
830        std::fs::create_dir_all(&bad_project_path)?;
831        std::fs::write(
832            bad_project_path.join("Cargo.toml"),
833            r#"
834[package]
835name = "bad_project"
836version = "0.1.0"
837edition = "2021"
838"#,
839        )?;
840
841        let bad_project = RustProject {
842            path: bad_project_path,
843            name: "bad_project".to_string(),
844            target_size: 0, // 没有target目录
845            last_modified: std::time::SystemTime::now(),
846            is_workspace: false,
847            has_target: false, // 关键:没有target目录
848        };
849
850        let projects = vec![good_project, bad_project];
851        let cleaner = ProjectCleaner::default();
852        let result = cleaner.clean_projects(&projects);
853
854        // 验证清理结果
855        // 可能有一些项目清理成功,一些失败,这是正常的混合结果
856        assert!(result.cleaned_projects + result.failed_projects.len() == 2);
857
858        // 验证至少处理了所有项目
859        println!(
860            "清理结果: 成功 {}, 失败 {}",
861            result.cleaned_projects,
862            result.failed_projects.len()
863        );
864
865        Ok(())
866    }
867}