Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions crates/diskern-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ fn human_bytes(n: u64) -> String {
}
}

/// Suffix for English pluralization: empty for 1, "s" for any other count.
fn plural(n: usize) -> &'static str {
if n == 1 {
""
} else {
"s"
}
}

/// Display strings live in the CLI, not in diskern-core — the engine is
/// deliberately UI-agnostic. These mirror the labels the desktop app uses
/// so the two front ends describe the same finding the same way.
Expand Down Expand Up @@ -146,7 +155,7 @@ fn print_findings(findings: &[&Finding], top: usize) {
"{} — {} finding{} · {}",
verdict_label(verdict),
group.len(),
if group.len() == 1 { "" } else { "s" },
plural(group.len()),
human_bytes(total)
);

Expand Down Expand Up @@ -239,10 +248,12 @@ fn main() -> Result<()> {
println!("Rules: external database — {}", path.display());
}
println!(
"Reclaimable: {} across {} findings and {} duplicate sets.",
"Reclaimable: {} across {} finding{} and {} duplicate set{}.",
human_bytes(report.total_reclaimable),
report.findings.len(),
report.duplicate_sets.len()
plural(report.findings.len()),
report.duplicate_sets.len(),
plural(report.duplicate_sets.len())
);
// Filter after the summary line, so the headline totals
// still describe the whole scan rather than the slice.
Expand All @@ -260,8 +271,9 @@ fn main() -> Result<()> {
let wasted: u64 = report.duplicate_sets.iter().map(|d| d.wasted).sum();
println!();
println!(
"Duplicate files — {} sets · {} wasted",
"Duplicate files — {} set{} · {} wasted",
report.duplicate_sets.len(),
plural(report.duplicate_sets.len()),
human_bytes(wasted)
);
let dup_shown = if top == 0 {
Expand Down Expand Up @@ -293,7 +305,15 @@ fn main() -> Result<()> {

#[cfg(test)]
mod tests {
use super::human_bytes;
use super::{human_bytes, plural};

#[test]
fn plural_returns_empty_only_for_singular() {
assert_eq!(plural(0), "s");
assert_eq!(plural(1), "");
assert_eq!(plural(2), "s");
assert_eq!(plural(10), "s");
}

#[test]
fn scales_to_a_readable_unit() {
Expand Down
59 changes: 59 additions & 0 deletions crates/diskern-cli/tests/singular_summary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::fs;
use std::process::Command;
use tempfile::tempdir;

fn run_scan(root: &std::path::Path) -> std::process::Output {
let mut command = Command::new(env!("CARGO_BIN_EXE_diskern"));
command.arg("scan").arg(root).arg("--top").arg("0");
command.output().expect("diskern should start")
}

#[test]
fn scan_with_single_duplicate_set_prints_singular_summary() {
let root = tempdir().unwrap();
// Two identical files with non-zero size form exactly 1 duplicate set.
fs::write(root.path().join("first.txt"), b"duplicate payload here").unwrap();
fs::write(root.path().join("second.txt"), b"duplicate payload here").unwrap();

let output = run_scan(root.path());
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);

let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("1 duplicate set."),
"expected '1 duplicate set.', got: {stdout}"
);
assert!(
stdout.contains("Duplicate files — 1 set ·"),
"expected 'Duplicate files — 1 set ·', got: {stdout}"
);
}

#[test]
fn scan_with_single_finding_prints_singular_summary() {
let root = tempdir().unwrap();
let cache = root.path().join(".cache/google-chrome/Default/Cache");
fs::create_dir_all(&cache).unwrap();
fs::write(cache.join("entry"), b"cached-single-entry").unwrap();

let output = run_scan(root.path());
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);

let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("across 1 finding and 0 duplicate sets."),
"expected 'across 1 finding and 0 duplicate sets.', got: {stdout}"
);
assert!(
stdout.contains("1 finding ·"),
"expected '1 finding ·', got: {stdout}"
);
}
Loading