1use std::collections::HashSet;
38use std::path::{Path, PathBuf};
39
40use anyhow::Result;
41
42use crate::adapters;
43use crate::constants;
44use crate::json;
45use crate::output;
46
47pub struct CacheReport {
49 pub manager: &'static str,
51 pub kind: &'static str,
53 pub path: PathBuf,
55 pub bytes: u64,
57 pub clear_command: &'static str,
59 pub clear: Clear,
61 pub note: Option<&'static str>,
63}
64
65#[derive(Clone, Copy)]
67pub enum Clear {
68 Command(&'static str, &'static [&'static str]),
72 Directory,
75}
76
77struct Probe {
79 manager: &'static str,
80 kind: &'static str,
81 query: Option<(&'static str, &'static [&'static str])>,
87 clear_command: &'static str,
88 clear: Clear,
89 note: Option<&'static str>,
90}
91
92#[cfg(windows)]
95const CARGO_CACHE_CLEAR: &str =
96 r"Remove-Item -Recurse -Force $env:USERPROFILE\.cargo\registry\cache";
97#[cfg(not(windows))]
98const CARGO_CACHE_CLEAR: &str = "rm -rf ~/.cargo/registry/cache";
99
100#[cfg(windows)]
101const CARGO_SRC_CLEAR: &str = r"Remove-Item -Recurse -Force $env:USERPROFILE\.cargo\registry\src";
102#[cfg(not(windows))]
103const CARGO_SRC_CLEAR: &str = "rm -rf ~/.cargo/registry/src";
104
105#[cfg(windows)]
109const MAVEN_REPO_CLEAR: &str = r"Remove-Item -Recurse -Force $env:USERPROFILE\.m2\repository";
110#[cfg(not(windows))]
111const MAVEN_REPO_CLEAR: &str = "rm -rf ~/.m2/repository";
112
113#[cfg(windows)]
114const GRADLE_CACHE_CLEAR: &str = r"Remove-Item -Recurse -Force $env:USERPROFILE\.gradle\caches";
115#[cfg(not(windows))]
116const GRADLE_CACHE_CLEAR: &str = "rm -rf ~/.gradle/caches";
117
118#[cfg(windows)]
119const GRADLE_DISTS_CLEAR: &str =
120 r"Remove-Item -Recurse -Force $env:USERPROFILE\.gradle\wrapper\dists";
121#[cfg(not(windows))]
122const GRADLE_DISTS_CLEAR: &str = "rm -rf ~/.gradle/wrapper/dists";
123
124#[cfg(windows)]
125const VCPKG_ARCHIVES_CLEAR: &str = r"Remove-Item -Recurse -Force $env:LOCALAPPDATA\vcpkg\archives";
126#[cfg(not(windows))]
127const VCPKG_ARCHIVES_CLEAR: &str = "rm -rf ~/.cache/vcpkg/archives";
128
129#[cfg(windows)]
132const HEX_CACHE_CLEAR: &str = r"Remove-Item -Recurse -Force $env:USERPROFILE\.hex\packages";
133#[cfg(not(windows))]
134const HEX_CACHE_CLEAR: &str = "rm -rf ~/.hex/packages";
135
136const PROBES: &[Probe] = &[
137 Probe {
138 manager: "npm",
139 kind: "cache",
140 query: Some(("npm", &["config", "get", "cache"])),
141 clear_command: "npm cache clean --force",
142 clear: Clear::Command("npm", &["cache", "clean", "--force"]),
143 note: None,
144 },
145 Probe {
146 manager: "pnpm",
147 kind: "store",
148 query: Some(("pnpm", &["store", "path"])),
149 clear_command: "pnpm store prune",
150 clear: Clear::Command("pnpm", &["store", "prune"]),
151 note: Some(
152 "hardlinked into every node_modules on the machine; emptying it is what makes \
153 the next pnpm install a download",
154 ),
155 },
156 Probe {
157 manager: "yarn",
158 kind: "cache",
159 query: Some(("yarn", &["cache", "dir"])),
160 clear_command: "yarn cache clean",
161 clear: Clear::Command("yarn", &["cache", "clean"]),
162 note: None,
163 },
164 Probe {
165 manager: "bun",
166 kind: "cache",
167 query: Some(("bun", &["pm", "cache"])),
168 clear_command: "bun pm cache rm",
169 clear: Clear::Command("bun", &["pm", "cache", "rm"]),
170 note: None,
171 },
172 Probe {
173 manager: "uv",
174 kind: "cache",
175 query: Some(("uv", &["cache", "dir"])),
176 clear_command: "uv cache prune",
179 clear: Clear::Command("uv", &["cache", "prune"]),
180 note: None,
181 },
182 Probe {
183 manager: "pip",
184 kind: "cache",
185 query: Some(("pip", &["cache", "dir"])),
186 clear_command: "pip cache purge",
187 clear: Clear::Command("pip", &["cache", "purge"]),
188 note: None,
189 },
190 Probe {
191 manager: "cargo",
192 kind: "registry cache",
193 query: None,
194 clear_command: CARGO_CACHE_CLEAR,
195 clear: Clear::Directory,
196 note: Some("the downloaded .crate archives; clearing them means downloading again"),
197 },
198 Probe {
199 manager: "cargo",
200 kind: "registry sources",
201 query: None,
202 clear_command: CARGO_SRC_CLEAR,
203 clear: Clear::Directory,
204 note: Some("unpacked copies of the archives above; cargo re-extracts these offline"),
205 },
206 Probe {
207 manager: "go",
208 kind: "module cache",
209 query: Some(("go", &["env", "GOMODCACHE"])),
210 clear_command: "go clean -modcache",
211 clear: Clear::Command("go", &["clean", "-modcache"]),
212 note: None,
213 },
214 Probe {
215 manager: "go",
216 kind: "build cache",
217 query: Some(("go", &["env", "GOCACHE"])),
218 clear_command: "go clean -cache",
219 clear: Clear::Command("go", &["clean", "-cache"]),
220 note: Some("compiled build artifacts; clearing them means the next build is a cold one"),
221 },
222 Probe {
227 manager: "maven",
228 kind: "local repository",
229 query: None,
230 clear_command: MAVEN_REPO_CLEAR,
231 clear: Clear::Directory,
232 note: Some(
233 "every Maven build on the machine resolves from here; the next build re-downloads what it needs",
234 ),
235 },
236 Probe {
237 manager: "gradle",
238 kind: "caches",
239 query: None,
240 clear_command: GRADLE_CACHE_CLEAR,
241 clear: Clear::Directory,
242 note: Some(
243 "downloaded dependencies and build caches shared by every Gradle project; rebuilt on demand",
244 ),
245 },
246 Probe {
247 manager: "gradle",
248 kind: "wrapper distributions",
249 query: None,
250 clear_command: GRADLE_DISTS_CLEAR,
251 clear: Clear::Directory,
252 note: Some(
253 "one full Gradle per version any wrapper ever asked for; re-downloaded on demand",
254 ),
255 },
256 Probe {
260 manager: "nuget",
261 kind: "global packages",
262 query: None,
263 clear_command: "dotnet nuget locals global-packages --clear",
264 clear: Clear::Command("dotnet", &["nuget", "locals", "global-packages", "--clear"]),
265 note: Some(
266 "every .NET project on the machine restores from here; re-downloaded on the next restore",
267 ),
268 },
269 Probe {
270 manager: "vcpkg",
271 kind: "binary cache",
272 query: None,
273 clear_command: VCPKG_ARCHIVES_CLEAR,
274 clear: Clear::Directory,
275 note: Some("prebuilt package archives; vcpkg rebuilds from source what it cannot re-fetch"),
276 },
277 Probe {
278 manager: "conan",
279 kind: "package cache",
280 query: None,
281 clear_command: "conan remove \"*\" --confirm",
282 clear: Clear::Command("conan", &["remove", "*", "--confirm"]),
283 note: Some(
284 "recipes and binaries shared by every Conan project; re-fetched on the next install",
285 ),
286 },
287 Probe {
292 manager: "composer",
293 kind: "cache",
294 query: Some(("composer", &["config", "--global", "cache-dir"])),
295 clear_command: "composer clear-cache",
296 clear: Clear::Command("composer", &["clear-cache"]),
297 note: Some(
298 "downloaded package archives and repository metadata; re-fetched by the next composer install",
299 ),
300 },
301 Probe {
306 manager: "cocoapods",
307 kind: "cache",
308 query: None,
309 clear_command: "pod cache clean --all",
310 clear: Clear::Command("pod", &["cache", "clean", "--all"]),
311 note: Some("downloaded pod sources, re-fetched by the next pod install"),
312 },
313 Probe {
314 manager: "hex",
315 kind: "package cache",
316 query: None,
317 clear_command: HEX_CACHE_CLEAR,
318 clear: Clear::Directory,
319 note: Some(
320 "package tarballs shared by every Mix project on the machine; re-fetched by the next mix deps.get",
321 ),
322 },
323];
324
325pub fn run(json_output: bool) -> Result<()> {
327 let reports = collect(!json_output);
328
329 if json_output {
330 return json::emit(&json::caches_document(&reports));
331 }
332
333 print_report(&reports);
334 Ok(())
335}
336
337fn collect(spinner: bool) -> Vec<CacheReport> {
339 let pb = spinner.then(|| output::create_spinner("Measuring package manager caches..."));
340 let from = query_dir();
341
342 let mut seen: HashSet<PathBuf> = HashSet::new();
343 let mut reports = Vec::new();
344
345 for probe in PROBES {
346 let Some(path) = locate(probe, &from) else {
347 continue;
348 };
349 let path = path.canonicalize().unwrap_or(path);
356 if !seen.insert(path.clone()) {
357 continue;
358 }
359 reports.push(CacheReport {
360 manager: probe.manager,
361 kind: probe.kind,
362 bytes: adapters::dir_size(&path),
363 path,
364 clear_command: probe.clear_command,
365 clear: probe.clear,
366 note: probe.note,
367 });
368 }
369
370 if let Some(pb) = pb {
371 pb.finish_and_clear();
372 }
373
374 reports.sort_by_key(|r| std::cmp::Reverse(r.bytes));
375 reports
376}
377
378fn query_dir() -> PathBuf {
385 dirs::home_dir()
386 .or_else(|| std::env::current_dir().ok())
387 .unwrap_or_else(|| PathBuf::from("."))
388}
389
390fn locate(probe: &Probe, from: &Path) -> Option<PathBuf> {
392 if let Some((program, args)) = probe.query
393 && adapters::binary_available(program)
394 {
395 let answered = adapters::capture_command_with_timeout(
396 program,
397 args,
398 from,
399 std::time::Duration::from_secs(constants::CACHE_QUERY_TIMEOUT_SECS),
400 )
401 .ok()
402 .and_then(|raw| path_from_output(&raw))
403 .filter(|p| p.is_dir());
404 if answered.is_some() {
405 return answered;
406 }
407 }
408
409 fallbacks(probe.manager, probe.kind)
414 .into_iter()
415 .find(|p| p.is_dir())
416}
417
418fn path_from_output(raw: &str) -> Option<PathBuf> {
423 let line = raw.lines().map(str::trim).rfind(|l| !l.is_empty())?;
424 let line = line.trim_matches('"');
425 if line.is_empty() || line == "undefined" || !Path::new(line).is_absolute() {
428 return None;
429 }
430 Some(PathBuf::from(line))
431}
432
433fn fallbacks(manager: &str, kind: &str) -> Vec<PathBuf> {
435 let home = dirs::home_dir();
436 let local = dirs::data_local_dir();
437 let cache = dirs::cache_dir();
438 let under = |base: &Option<PathBuf>, rel: &str| {
442 base.as_ref()
443 .map(|b| rel.split('/').fold(b.clone(), |p, seg| p.join(seg)))
444 };
445
446 let candidates = match (manager, kind) {
447 ("npm", _) => vec![under(&local, "npm-cache"), under(&home, ".npm")],
450 ("pnpm", _) => vec![
451 under(&local, "pnpm/store"),
452 under(&home, ".local/share/pnpm/store"),
453 under(&home, "Library/pnpm/store"),
454 under(&home, ".pnpm-store"),
455 ],
456 ("yarn", _) => vec![
457 under(&home, ".yarn/berry/cache"),
458 under(&local, "Yarn/Cache"),
459 under(&cache, "yarn"),
460 ],
461 ("bun", _) => vec![under(&home, ".bun/install/cache")],
462 ("uv", _) => vec![under(&cache, "uv"), under(&local, "uv/cache")],
463 ("pip", _) => vec![under(&cache, "pip"), under(&local, "pip/Cache")],
464 ("cargo", "registry cache") => vec![Some(cargo_home().join("registry").join("cache"))],
465 ("cargo", _) => vec![Some(cargo_home().join("registry").join("src"))],
466 ("go", "module cache") => vec![
467 std::env::var_os("GOMODCACHE").map(PathBuf::from),
468 std::env::var_os("GOPATH").map(|p| PathBuf::from(p).join("pkg").join("mod")),
469 under(&home, "go/pkg/mod"),
470 ],
471 ("go", _) => vec![
472 std::env::var_os("GOCACHE").map(PathBuf::from),
473 under(&cache, "go-build"),
474 under(&local, "go-build"),
475 ],
476 ("maven", _) => vec![under(&home, ".m2/repository")],
477 ("gradle", "caches") => vec![
479 std::env::var_os("GRADLE_USER_HOME").map(|p| PathBuf::from(p).join("caches")),
480 under(&home, ".gradle/caches"),
481 ],
482 ("gradle", _) => vec![
483 std::env::var_os("GRADLE_USER_HOME")
484 .map(|p| PathBuf::from(p).join("wrapper").join("dists")),
485 under(&home, ".gradle/wrapper/dists"),
486 ],
487 ("nuget", _) => vec![
488 std::env::var_os("NUGET_PACKAGES").map(PathBuf::from),
489 under(&home, ".nuget/packages"),
490 ],
491 ("vcpkg", _) => vec![
492 std::env::var_os("VCPKG_DEFAULT_BINARY_CACHE").map(PathBuf::from),
493 under(&local, "vcpkg/archives"),
494 under(&cache, "vcpkg/archives"),
495 ],
496 ("conan", _) => vec![
499 std::env::var_os("CONAN_HOME").map(|p| PathBuf::from(p).join("p")),
500 under(&home, ".conan2/p"),
501 ],
502 ("composer", _) => vec![
505 std::env::var_os("COMPOSER_CACHE_DIR").map(PathBuf::from),
506 std::env::var_os("COMPOSER_HOME").map(|p| PathBuf::from(p).join("cache")),
507 under(&local, "Composer"),
508 under(&cache, "composer"),
509 under(&home, ".composer/cache"),
510 ],
511 ("cocoapods", _) => vec![
515 std::env::var_os("CP_CACHE_DIR").map(PathBuf::from),
516 under(&home, "Library/Caches/CocoaPods"),
517 ],
518 ("hex", _) => vec![
521 std::env::var_os("HEX_HOME").map(|p| PathBuf::from(p).join("packages")),
522 under(&home, ".hex/packages"),
523 under(&cache, "hex/packages"),
524 ],
525 _ => vec![],
526 };
527
528 candidates.into_iter().flatten().collect()
529}
530
531fn cargo_home() -> PathBuf {
533 std::env::var_os("CARGO_HOME")
534 .map(PathBuf::from)
535 .or_else(|| dirs::home_dir().map(|h| h.join(".cargo")))
536 .unwrap_or_else(|| PathBuf::from(".cargo"))
537}
538
539fn print_report(reports: &[CacheReport]) {
540 output::print_header("Package manager caches");
541
542 if reports.is_empty() {
543 println!();
544 output::print_info("No package manager caches found on this machine.");
545 return;
546 }
547
548 println!();
549 for r in reports {
550 let label = format!("{} {}", r.manager, r.kind);
551 println!(
552 " {:<30} {:>10} {}",
553 label,
554 output::format_bytes(r.bytes),
555 output::clean_path(&r.path)
556 );
557 println!(" {:<30} {:>10} clear: {}", "", "", r.clear_command);
558 if let Some(note) = r.note {
559 println!(" {:<30} {:>10} {}", "", "", note);
560 }
561 println!();
562 }
563
564 let total: u64 = reports.iter().map(|r| r.bytes).sum();
565 println!(
566 " {:<30} {:>10} across {} {}",
567 "Total",
568 output::format_bytes(total),
569 reports.len(),
570 output::plural(reports.len(), "cache", "caches")
571 );
572
573 println!();
574 output::print_info(
575 "Nothing above was deleted. A cache is shared by every project on the machine, so \
576 no single repository's lockfile can prove it is recoverable — and it is what \
577 makes `devp restore` fast, which is why nothing dev-prune runs on a schedule \
578 will ever touch one. When you want the space more than the speed, run a clear \
579 command yourself, or `devp caches clear <manager>`.",
580 );
581}
582
583pub struct ClearOutcome {
585 pub manager: &'static str,
587 pub kind: &'static str,
589 pub path: PathBuf,
591 pub before: u64,
593 pub after: u64,
597 pub problem: Option<String>,
599}
600
601impl ClearOutcome {
602 pub fn freed(&self) -> u64 {
604 self.before.saturating_sub(self.after)
605 }
606}
607
608pub fn run_clear(target: &str, yes: bool, dry_run: bool, json_output: bool) -> Result<()> {
613 let all = target.eq_ignore_ascii_case("all");
614 if !all
615 && !PROBES
616 .iter()
617 .any(|p| p.manager.eq_ignore_ascii_case(target))
618 {
619 return Err(anyhow::Error::new(crate::UsageError(format!(
620 "`{target}` is not a manager dev-prune knows a cache for. Try one of: {}, or `all`.",
621 known_managers().join(", ")
622 ))));
623 }
624 if json_output && !yes && !dry_run {
627 return Err(anyhow::Error::new(crate::UsageError(
628 "`--json` cannot ask for confirmation — pass `--yes` as well, or `--dry-run` \
629 to see what would go."
630 .to_string(),
631 )));
632 }
633
634 let reports: Vec<CacheReport> = collect(!json_output)
635 .into_iter()
636 .filter(|r| all || r.manager.eq_ignore_ascii_case(target))
637 .collect();
638
639 if reports.is_empty() {
640 if json_output {
641 return json::emit(&json::caches_clear_plan_document(&reports));
642 }
643 output::print_info(&format!(
644 "No {} cache on this machine — nothing to clear.",
645 if all { "package manager" } else { target }
646 ));
647 return Ok(());
648 }
649
650 if dry_run {
651 if json_output {
652 return json::emit(&json::caches_clear_plan_document(&reports));
653 }
654 print_clear_plan(&reports, true);
655 return Ok(());
656 }
657
658 if !json_output {
659 print_clear_plan(&reports, false);
660 if !confirm_clear(yes) {
661 output::print_info("Nothing was cleared.");
662 return Ok(());
663 }
664 }
665
666 let outcomes: Vec<ClearOutcome> = reports.iter().map(clear_one).collect();
667
668 if json_output {
669 json::emit(&json::caches_clear_document(&outcomes))?;
670 } else {
671 print_clear_result(&outcomes);
672 }
673
674 let failed = outcomes.iter().filter(|o| o.problem.is_some()).count();
677 if failed > 0 {
678 anyhow::bail!(
679 "{failed} {} could not be cleared.",
680 output::plural(failed, "cache", "caches")
681 );
682 }
683 Ok(())
684}
685
686fn known_managers() -> Vec<&'static str> {
688 let mut names: Vec<&'static str> = Vec::new();
689 for probe in PROBES {
690 if !names.contains(&probe.manager) {
691 names.push(probe.manager);
692 }
693 }
694 names
695}
696
697fn clear_one(report: &CacheReport) -> ClearOutcome {
699 let problem = match report.clear {
700 Clear::Command(program, args) => run_clear_command(program, args),
701 Clear::Directory => remove_cache_dir(&report.path),
702 };
703 ClearOutcome {
704 manager: report.manager,
705 kind: report.kind,
706 path: report.path.clone(),
707 before: report.bytes,
708 after: adapters::dir_size(&report.path),
711 problem,
712 }
713}
714
715fn run_clear_command(program: &str, args: &[&str]) -> Option<String> {
717 if !adapters::binary_available(program) {
718 return Some(format!(
719 "`{program}` is not on PATH — only it knows what in this cache is still \
720 referenced, so dev-prune will not delete the directory in its place."
721 ));
722 }
723 adapters::run_command_with_timeout(
724 program,
725 args,
726 &query_dir(),
727 std::time::Duration::from_secs(constants::CACHE_CLEAR_TIMEOUT_SECS),
728 )
729 .err()
730 .map(|e| format!("{e:#}"))
731}
732
733fn remove_cache_dir(path: &Path) -> Option<String> {
735 std::fs::remove_dir_all(path)
739 .or_else(|_| {
740 std::thread::sleep(std::time::Duration::from_millis(250));
741 std::fs::remove_dir_all(path)
742 })
743 .err()
744 .filter(|e| e.kind() != std::io::ErrorKind::NotFound)
746 .map(|e| format!("{} could not be removed: {e}", output::clean_path(path)))
747}
748
749fn print_clear_plan(reports: &[CacheReport], dry_run: bool) {
751 output::print_header(if dry_run {
752 "Would clear"
753 } else {
754 "About to clear"
755 });
756
757 println!();
758 for r in reports {
759 println!(
760 " {:<30} {:>10} {}",
761 format!("{} {}", r.manager, r.kind),
762 output::format_bytes(r.bytes),
763 output::clean_path(&r.path)
764 );
765 println!(" {:<30} {:>10} via: {}", "", "", r.clear_command);
766 }
767
768 println!();
769 let total: u64 = reports.iter().map(|r| r.bytes).sum();
770 println!(
771 " {:<30} {:>10} across {} {}",
772 "Total",
773 output::format_bytes(total),
774 reports.len(),
775 output::plural(reports.len(), "cache", "caches")
776 );
777
778 println!();
779 output::print_info(
780 "Nothing in a cache is lost — every manager above re-downloads what it needs. \
781 The cost is time: the next install, and the next `devp restore`, in every \
782 project on this machine.",
783 );
784}
785
786fn print_clear_result(outcomes: &[ClearOutcome]) {
788 println!();
789 for o in outcomes {
790 let label = format!("{} {}", o.manager, o.kind);
791 println!(
792 " {:<30} {:>10} {}",
793 label,
794 output::format_bytes(o.freed()),
795 if o.problem.is_some() {
796 "not cleared"
797 } else {
798 "cleared"
799 }
800 );
801 if let Some(why) = &o.problem {
802 println!(" {:<30} {:>10} {why}", "", "");
803 }
804 }
805
806 println!();
807 let freed: u64 = outcomes.iter().map(ClearOutcome::freed).sum();
808 output::print_success(&format!("Freed {}.", output::format_bytes(freed)));
809}
810
811fn confirm_clear(yes: bool) -> bool {
814 use std::io::{IsTerminal, Write};
815 if yes {
816 return true;
817 }
818 if !std::io::stdin().is_terminal() {
819 output::print_info("Not running in a terminal — pass `--yes` to clear these.");
820 return false;
821 }
822 eprint!("Clear them? [y/N]: ");
826 if std::io::stderr().flush().is_err() {
827 return false;
828 }
829 let mut input = String::new();
830 if std::io::stdin().read_line(&mut input).is_err() {
831 return false;
832 }
833 matches!(input.trim().to_lowercase().as_str(), "y" | "yes")
834}
835
836#[cfg(test)]
837mod tests {
838 use super::*;
839
840 #[test]
841 fn every_probe_can_be_found_without_its_manager_installed() {
842 for probe in PROBES {
845 assert!(
846 !fallbacks(probe.manager, probe.kind).is_empty(),
847 "{} {} has no conventional location",
848 probe.manager,
849 probe.kind
850 );
851 }
852 }
853
854 #[test]
855 fn every_probe_names_the_command_that_clears_it() {
856 for probe in PROBES {
857 assert!(
858 !probe.clear_command.trim().is_empty(),
859 "{} {} reports a size with no way to act on it",
860 probe.manager,
861 probe.kind
862 );
863 }
864 }
865
866 #[test]
867 fn no_two_probes_describe_the_same_cache() {
868 let mut keys: Vec<(&str, &str)> = PROBES.iter().map(|p| (p.manager, p.kind)).collect();
869 let count = keys.len();
870 keys.sort_unstable();
871 keys.dedup();
872 assert_eq!(keys.len(), count, "two probes share a manager and kind");
873 }
874
875 #[test]
876 fn a_managers_answer_is_read_off_the_last_line() {
877 let raw = if cfg!(windows) {
879 "npm warn config global deprecated\nC:\\Users\\dev\\AppData\\Local\\npm-cache\n"
880 } else {
881 "npm warn config global deprecated\n/home/dev/.npm\n"
882 };
883 assert!(path_from_output(raw).is_some());
884 }
885
886 #[test]
887 fn quoted_paths_lose_their_quotes() {
888 let raw = if cfg!(windows) {
889 "\"C:\\Program Files\\go\\pkg\\mod\"\n"
890 } else {
891 "\"/opt/go path/pkg/mod\"\n"
892 };
893 let path = path_from_output(raw).expect("a quoted path is still a path");
894 assert!(!path.to_string_lossy().contains('"'));
895 }
896
897 #[test]
898 fn a_non_answer_is_not_mistaken_for_a_path() {
899 for raw in [
902 "",
903 "\n \n",
904 "undefined\n",
905 "not a command\n",
906 "./relative\n",
907 ] {
908 assert!(
909 path_from_output(raw).is_none(),
910 "{raw:?} was accepted as a cache path"
911 );
912 }
913 }
914
915 #[test]
916 fn the_cargo_rows_point_inside_the_registry() {
917 for kind in ["registry cache", "registry sources"] {
920 let path = fallbacks("cargo", kind).remove(0);
921 assert!(
922 path.starts_with(cargo_home().join("registry")),
923 "{kind} resolved outside the cargo registry: {}",
924 path.display()
925 );
926 }
927 }
928
929 #[test]
930 fn the_report_is_ordered_by_what_is_worth_clearing() {
931 let mut reports = [
932 CacheReport {
933 manager: "npm",
934 kind: "cache",
935 path: PathBuf::from("/a"),
936 bytes: 10,
937 clear_command: "x",
938 clear: Clear::Command("npm", &["cache"]),
939 note: None,
940 },
941 CacheReport {
942 manager: "go",
943 kind: "module cache",
944 path: PathBuf::from("/b"),
945 bytes: 4_000,
946 clear_command: "y",
947 clear: Clear::Directory,
948 note: None,
949 },
950 ];
951 reports.sort_by_key(|r| std::cmp::Reverse(r.bytes));
952 assert_eq!(reports[0].manager, "go");
953 }
954
955 #[test]
956 fn every_probe_clears_with_the_command_it_prints() {
957 for probe in PROBES {
960 let printed = probe.clear_command;
961 match probe.clear {
962 Clear::Command(program, args) => {
963 assert!(
964 printed.starts_with(program),
965 "{} {} prints `{printed}` but runs `{program}`",
966 probe.manager,
967 probe.kind
968 );
969 for arg in args {
970 assert!(
973 printed.contains(arg.trim_matches('"')),
974 "{} {} prints `{printed}` but passes `{arg}`",
975 probe.manager,
976 probe.kind
977 );
978 }
979 }
980 Clear::Directory => assert!(
981 printed.contains("rm -rf") || printed.contains("Remove-Item"),
982 "{} {} deletes a directory but prints `{printed}`",
983 probe.manager,
984 probe.kind
985 ),
986 }
987 }
988 }
989
990 #[test]
991 fn every_manager_in_the_report_can_be_named_to_clear() {
992 let names = known_managers();
993 for probe in PROBES {
994 assert!(
995 names.contains(&probe.manager),
996 "{} is reported but `devp caches clear {}` would not find it",
997 probe.manager,
998 probe.manager
999 );
1000 }
1001 let mut sorted = names.clone();
1004 sorted.sort_unstable();
1005 sorted.dedup();
1006 assert_eq!(sorted.len(), names.len(), "repeated manager in {names:?}");
1007 }
1008
1009 #[test]
1010 fn an_unknown_manager_is_a_usage_error() {
1011 let err = run_clear("nonesuch", true, true, false).unwrap_err();
1013 assert!(err.downcast_ref::<crate::UsageError>().is_some());
1014 }
1015
1016 #[test]
1017 fn json_without_yes_is_a_usage_error_rather_than_a_prompt() {
1018 let err = run_clear("npm", false, false, true).unwrap_err();
1019 assert!(err.downcast_ref::<crate::UsageError>().is_some());
1020 }
1021
1022 #[test]
1023 fn removing_a_directory_reports_nothing_when_it_worked() {
1024 let dir = tempfile::tempdir().unwrap();
1025 let cache = dir.path().join("cache");
1026 std::fs::create_dir(&cache).unwrap();
1027 std::fs::write(cache.join("blob"), b"x").unwrap();
1028
1029 assert!(remove_cache_dir(&cache).is_none());
1030 assert!(!cache.exists());
1031 assert!(remove_cache_dir(&cache).is_none());
1034 }
1035
1036 #[test]
1037 fn clearing_a_directory_reports_what_actually_went() {
1038 let dir = tempfile::tempdir().unwrap();
1039 let cache = dir.path().join("store");
1040 std::fs::create_dir(&cache).unwrap();
1041 std::fs::write(cache.join("blob"), vec![0u8; 4096]).unwrap();
1042 let before = adapters::dir_size(&cache);
1043
1044 let outcome = clear_one(&CacheReport {
1045 manager: "cargo",
1046 kind: "registry cache",
1047 path: cache.clone(),
1048 bytes: before,
1049 clear_command: "rm -rf",
1050 clear: Clear::Directory,
1051 note: None,
1052 });
1053
1054 assert!(outcome.problem.is_none());
1055 assert_eq!(outcome.after, 0);
1056 assert_eq!(outcome.freed(), before);
1059 assert!(!cache.exists());
1060 }
1061
1062 #[test]
1063 fn a_manager_that_is_not_installed_is_reported_rather_than_deleted_around() {
1064 let problem = run_clear_command("dev-prune-no-such-manager", &["cache", "clean"]);
1067 assert!(problem.is_some_and(|p| p.contains("not on PATH")));
1068 }
1069}