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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
# code but applies no lints to it, so without this the suite — by now
# a good half of the crate — drifts unlinted while CI stays green.
- run: cargo fmt --check
- run: cargo clippy -p diskern-core -p diskern-cli --all-targets -- -D warnings
- run: cargo clippy -p diskern-core -p diskern-cli --all-targets --all-features -- -D warnings

# The engine's behaviour is platform-specific in ways a run on one OS
# cannot reach, and it ships on more than one: `default_excludes` has a
Expand All @@ -73,7 +73,7 @@ jobs:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: swatinem/rust-cache@v2
- run: cargo test -p diskern-core -p diskern-cli
- run: cargo test -p diskern-core -p diskern-cli --all-features

# Type-checks app/src-tauri against the current diskern-core. Until this
# existed nothing compiled the Tauri crate between releases: a breaking
Expand Down
22 changes: 1 addition & 21 deletions crates/diskern-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anyhow::{Context, Result};
use clap::{Parser, Subcommand, ValueEnum};
use diskern_core::{report, rules::RulesDb, scanner, Category, Finding, Verdict};
use diskern_core::{human_bytes, report, rules::RulesDb, scanner, Category, Finding, Verdict};
use std::path::PathBuf;
use std::sync::Arc;

Expand Down Expand Up @@ -56,26 +56,6 @@ impl From<VerdictFilter> for Verdict {
}
}

/// Bytes at the largest unit that keeps the number short. Decimal units,
/// matching what disk vendors and the rest of the UI report.
fn human_bytes(n: u64) -> String {
const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
let mut value = n as f64;
let mut unit = 0;
// 999.95, not 1000.0: at one decimal place anything at or above that
// rounds to "1000.0", which belongs in the next unit up. Choosing the
// unit before rounding printed 999_999 as "1000.0 KB".
while value >= 999.95 && unit < UNITS.len() - 1 {
value /= 1000.0;
unit += 1;
}
if unit == 0 {
format!("{n} B")
} else {
format!("{value:.1} {}", UNITS[unit])
}
}

/// Suffix for English pluralization: empty for 1, "s" for any other count.
fn plural(n: usize) -> &'static str {
if n == 1 {
Expand Down
59 changes: 56 additions & 3 deletions crates/diskern-core/src/ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! - `AnthropicProvider` / `OpenAiProvider`: user pastes their own API key.
//! - `OllamaProvider`: local models via http://localhost:11434.

use crate::Finding;
use crate::{human_bytes, Finding};

pub trait AiProvider: Send + Sync {
/// Turn a set of findings into a short, plain-language explanation.
Expand All @@ -32,9 +32,9 @@ impl AiProvider for TemplateNarrator {
fn narrate(&self, findings: &[Finding]) -> Result<String, AiError> {
let total: u64 = findings.iter().map(|f| f.reclaimable).sum();
Ok(format!(
"Found {} items totalling {:.1} GB reclaimable. Top reasons: {}",
"Found {} items totalling {} reclaimable. Top reasons: {}",
findings.len(),
total as f64 / 1e9,
human_bytes(total),
findings
.iter()
.take(3)
Expand All @@ -44,3 +44,56 @@ impl AiProvider for TemplateNarrator {
))
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{Category, FileEntry, Verdict};
use std::path::PathBuf;

fn sample_finding(path: &str, reclaimable: u64, reason: &str) -> Finding {
Finding {
entry: FileEntry {
path: PathBuf::from(path),
size: reclaimable,
modified: None,
accessed: None,
is_symlink: false,
hash: None,
},
category: Category::BrowserCache,
verdict: Verdict::Safe,
risk_score: 0.1,
reasons: vec![reason.to_string()],
reclaimable,
}
}

#[test]
fn template_narrator_narrates_facts_and_formats_bytes() {
let findings = vec![
sample_finding("/cache/a", 50_000_000, "chrome cache expired"),
sample_finding("/cache/b", 30_000_000, "firefox cache expired"),
sample_finding("/cache/c", 20_000_000, "safari cache expired"),
sample_finding("/cache/d", 10_000_000, "edge cache expired"),
];

let narrator = TemplateNarrator;
let narrative = narrator.narrate(&findings).unwrap();

assert_eq!(
narrative,
"Found 4 items totalling 110.0 MB reclaimable. Top reasons: chrome cache expired; firefox cache expired; safari cache expired"
);
}

#[test]
fn template_narrator_empty_findings() {
let narrator = TemplateNarrator;
let narrative = narrator.narrate(&[]).unwrap();
assert_eq!(
narrative,
"Found 0 items totalling 0 B reclaimable. Top reasons: "
);
}
}
37 changes: 37 additions & 0 deletions crates/diskern-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,40 @@ pub enum GenomeError {
}

pub type Result<T> = std::result::Result<T, GenomeError>;

/// Format a byte count into a human-readable decimal string (e.g. "1.5 GB", "420.0 MB"),
/// matching what disk vendors and the rest of the UI report.
pub fn human_bytes(n: u64) -> String {
const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
let mut value = n as f64;
let mut unit = 0;
// 999.95, not 1000.0: at one decimal place anything at or above that
// rounds to "1000.0", which belongs in the next unit up. Choosing the
// unit before rounding printed 999_999 as "1000.0 KB".
while value >= 999.95 && unit < UNITS.len() - 1 {
value /= 1000.0;
unit += 1;
}
if unit == 0 {
format!("{n} B")
} else {
format!("{value:.1} {}", UNITS[unit])
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn human_bytes_formatting() {
assert_eq!(human_bytes(0), "0 B");
assert_eq!(human_bytes(500), "500 B");
assert_eq!(human_bytes(999), "999 B");
assert_eq!(human_bytes(1_000), "1.0 KB");
assert_eq!(human_bytes(999_950), "1.0 MB");
assert_eq!(human_bytes(10_000_000), "10.0 MB");
assert_eq!(human_bytes(1_500_000_000), "1.5 GB");
assert_eq!(human_bytes(2_000_000_000_000), "2.0 TB");
}
}
Loading