use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DeadSwapfile {
pub(crate) path: PathBuf,
pub(crate) bytes: u64,
pub(crate) needs_root: bool,
}
impl DeadSwapfile {
pub(crate) fn removal_command(&self) -> String {
if self.needs_root {
format!("sudo rm -f {}", self.path.display())
} else {
format!("rm -f {}", self.path.display())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StrayItem {
pub(crate) path: PathBuf,
pub(crate) bytes: u64,
pub(crate) kind: StrayKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StrayKind {
BackupArchive,
BackupRepo,
ScratchDir,
}
impl StrayKind {
pub(crate) fn label(self) -> &'static str {
match self {
Self::BackupArchive => "backup-archive",
Self::BackupRepo => "backup-repo",
Self::ScratchDir => "scratch-dir",
}
}
}
const SCRATCH_DIR_MARKERS: [&str; 6] = [
"-recovery-",
"-proof",
"-audit-new",
"tmp-shield",
"-orphans",
"-pre-filterrepo-backup",
];
const BACKUP_ARCHIVE_SUFFIXES: [&str; 3] = [".bundle", ".tar.gz", ".tar.zst"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SwapEntry {
pub(crate) path: PathBuf,
pub(crate) size_kib: u64,
}
pub(crate) fn active_swap_table(swaps_contents: &str) -> Vec<SwapEntry> {
swaps_contents
.lines()
.skip(1)
.filter_map(|line| {
let mut cols = line.split_whitespace();
let path = cols.next()?;
let size_kib = cols.nth(1).and_then(|s| s.parse().ok()).unwrap_or(0);
Some(SwapEntry {
path: PathBuf::from(path),
size_kib,
})
})
.collect()
}
pub(crate) fn active_swap_paths(swaps_contents: &str) -> Vec<PathBuf> {
active_swap_table(swaps_contents)
.into_iter()
.map(|entry| entry.path)
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct WrongSwap {
pub(crate) path: PathBuf,
pub(crate) bytes: u64,
}
fn under_prefix(path: &Path, prefix: &str) -> bool {
if prefix.is_empty() {
return false;
}
let trimmed = prefix.trim_end_matches('/');
if trimmed.is_empty() {
return true; }
let path_str = path.to_string_lossy();
if path_str == trimmed {
return true;
}
if path_str.starts_with(trimmed)
&& path_str.as_bytes().get(trimmed.len()) == Some(&b'/')
{
return true;
}
if !path_str.starts_with("/dev/") {
return false;
}
let (Some(prefix_parent), Some(prefix_name)) = (
Path::new(trimmed).parent().and_then(|p| p.to_str()),
Path::new(trimmed).file_name().and_then(|n| n.to_str()),
) else {
return false;
};
let (Some(path_parent), Some(path_name)) = (
path.parent().and_then(|p| p.to_str()),
path.file_name().and_then(|n| n.to_str()),
) else {
return false;
};
if prefix_parent != path_parent {
return false;
}
match path_name.strip_prefix(prefix_name) {
Some(rest) => !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()),
None => false,
}
}
pub(crate) fn check_active_swap_placement(
active: &[SwapEntry],
expected: &[String],
) -> Vec<WrongSwap> {
active
.iter()
.filter(|entry| !expected.iter().any(|p| under_prefix(&entry.path, p)))
.map(|entry| WrongSwap {
path: entry.path.clone(),
bytes: entry.size_kib.saturating_mul(1024),
})
.collect()
}
fn blank_char(out: &mut String, c: char) {
if c == '\n' {
out.push('\n');
} else {
for _ in 0..c.len_utf8() {
out.push(' ');
}
}
}
fn code_only(config: &str) -> String {
let chars: Vec<char> = config.chars().collect();
let mut out = String::with_capacity(config.len());
let mut k = 0;
while k < chars.len() {
let c = chars[k];
if c == '#' {
while k < chars.len() && chars[k] != '\n' {
blank_char(&mut out, chars[k]);
k += 1;
}
continue;
}
if c == '/' && k + 1 < chars.len() && chars[k + 1] == '*' {
out.push(' ');
out.push(' ');
k += 2;
while k + 1 < chars.len() && !(chars[k] == '*' && chars[k + 1] == '/') {
blank_char(&mut out, chars[k]);
k += 1;
}
out.push(' ');
out.push(' ');
k += 2;
continue;
}
if c == '"' {
out.push(c);
k += 1;
while k < chars.len() && chars[k] != '"' {
if chars[k] == '\\' && k + 1 < chars.len() {
blank_char(&mut out, chars[k]);
blank_char(&mut out, chars[k + 1]);
k += 2;
} else {
blank_char(&mut out, chars[k]);
k += 1;
}
}
if k < chars.len() {
out.push('"');
k += 1;
}
continue;
}
if c == '\'' && k + 1 < chars.len() && chars[k + 1] == '\'' {
out.push(' ');
out.push(' ');
k += 2;
loop {
if k >= chars.len() {
break;
}
if chars[k] == '\''
&& k + 1 < chars.len()
&& chars[k + 1] == '\''
&& k + 2 < chars.len()
&& (chars[k + 2] == '\'' || chars[k + 2] == '$' || chars[k + 2] == '\\')
{
out.push(' ');
out.push(' ');
out.push(' ');
k += 3;
continue;
}
if chars[k] == '\'' && k + 1 < chars.len() && chars[k + 1] == '\'' {
out.push(' ');
out.push(' ');
k += 2;
break;
}
blank_char(&mut out, chars[k]);
k += 1;
}
continue;
}
out.push(c);
k += 1;
}
out
}
pub(crate) fn nixos_swap_devices(config: &str) -> Vec<PathBuf> {
let code = code_only(config);
let (start, end) = match swap_devices_block(&code) {
Some(range) => range,
None => return Vec::new(),
};
let block = &config[start..end];
let mut devices = Vec::new();
let bytes = block.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'"' {
i += 1;
while i < bytes.len() && bytes[i] != b'"' {
if bytes[i] == b'\\' {
i += 1;
}
i += 1;
}
i += 1;
continue;
}
if bytes[i] == b'#' {
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
continue;
}
if bytes[i] == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
i += 2;
while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
i += 1;
}
i += 2;
continue;
}
if block[i..].starts_with("device")
&& {
let before = block[..i].chars().next_back();
before.map(|c| !(c.is_alphanumeric() || c == '_' || c == '.')).unwrap_or(true)
}
{
let mut j = i + "device".len();
while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t' || bytes[j] == b'\n') {
j += 1;
}
if j < bytes.len() && bytes[j] == b'=' {
j += 1;
while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t' || bytes[j] == b'\n') {
j += 1;
}
if j < bytes.len() && bytes[j] == b'"' {
j += 1;
let start = j;
while j < bytes.len() && bytes[j] != b'"' {
j += 1;
}
devices.push(PathBuf::from(&block[start..j]));
i = j + 1;
continue;
}
}
}
i += 1;
}
devices
}
pub(crate) fn has_swap_devices_block(config: &str) -> bool {
swap_devices_block(&code_only(config)).is_some()
}
fn swap_devices_block(code: &str) -> Option<(usize, usize)> {
let key = "swapDevices";
let mut search_from = 0;
while let Some(rel) = code[search_from..].find(key) {
let key_pos = search_from + rel;
search_from = key_pos + key.len();
if let Some(before) = code[..key_pos].chars().next_back() {
if before.is_alphanumeric() || before == '_' || before == '.' {
continue;
}
}
if let Some(range) = block_after(code, key_pos + key.len()) {
return Some(range);
}
}
None
}
fn block_after(config: &str, key_end: usize) -> Option<(usize, usize)> {
let after_key = &config[key_end..];
let eq = after_key.find('=')?;
let after_eq = &after_key[eq + 1..];
let base = key_end + eq + 1;
let open_rel = after_eq.find('[')?;
if after_eq[..open_rel].contains(';') {
return None;
}
let mut depth = 0;
for (offset, c) in after_eq[open_rel..].char_indices() {
match c {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
return Some((base + open_rel, base + open_rel + offset + 1));
}
}
_ => {}
}
}
None
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct UnreferencedSwap {
pub(crate) path: PathBuf,
pub(crate) bytes: u64,
pub(crate) is_active: bool,
}
pub(crate) fn check_swap_references(
on_disk: &[(PathBuf, u64)],
config_devices: &[PathBuf],
active: &[PathBuf],
) -> Vec<UnreferencedSwap> {
on_disk
.iter()
.filter(|(path, _)| {
!under_prefix(path, "/dev/")
&& !config_devices.iter().any(|d| d == path)
})
.map(|(path, bytes)| UnreferencedSwap {
path: path.clone(),
bytes: *bytes,
is_active: active.iter().any(|a| a == path),
})
.collect()
}
#[derive(Debug, Clone, Default)]
pub(crate) struct SwapCheckOutcome {
pub(crate) probe_ok: bool,
pub(crate) wrong: Vec<WrongSwap>,
pub(crate) unreferenced: Vec<UnreferencedSwap>,
pub(crate) config_known: bool,
}
impl SwapCheckOutcome {
pub(crate) fn has_findings(&self) -> bool {
!self.wrong.is_empty() || !self.unreferenced.is_empty()
}
pub(crate) fn describe(&self) -> Vec<String> {
let mut lines = Vec::new();
for w in &self.wrong {
lines.push(format!(
"ACTIVE swap on unexpected device: {} ({} configured)",
w.path.display(),
crate::policy::human_bytes(w.bytes),
));
}
for u in &self.unreferenced {
lines.push(format!(
"swapfile not in NixOS swapDevices: {} ({}){}",
u.path.display(),
crate::policy::human_bytes(u.bytes),
if u.is_active {
" — ACTIVE now, will not survive a rebuild/reboot"
} else {
""
},
));
}
lines
}
}
pub(crate) fn check_swap_health(
expected_prefixes: &[String],
candidates: &[PathBuf],
config_path: &Path,
) -> SwapCheckOutcome {
let swaps = std::fs::read_to_string("/proc/swaps").ok();
let table = swaps.as_deref().map(active_swap_table).unwrap_or_default();
let mut on_disk: Vec<(PathBuf, u64)> = Vec::new();
for candidate in candidates {
if let Ok(metadata) = std::fs::metadata(candidate) {
if !metadata.is_dir() && !on_disk.iter().any(|(p, _)| p == candidate) {
on_disk.push((candidate.clone(), metadata.len()));
}
}
}
for entry in &table {
if !under_prefix(&entry.path, "/dev/")
&& !on_disk.iter().any(|(p, _)| p == &entry.path)
{
let bytes = std::fs::metadata(&entry.path)
.map(|m| m.len())
.unwrap_or_else(|_| entry.size_kib.saturating_mul(1024));
on_disk.push((entry.path.clone(), bytes));
}
}
let config = std::fs::read_to_string(config_path).ok();
evaluate_swap_health(
swaps.as_deref(),
config.as_deref(),
expected_prefixes,
&on_disk,
)
}
pub(crate) fn evaluate_swap_health(
swaps_contents: Option<&str>,
config_text: Option<&str>,
expected_prefixes: &[String],
on_disk: &[(PathBuf, u64)],
) -> SwapCheckOutcome {
let mut outcome = SwapCheckOutcome::default();
let Some(swaps) = swaps_contents else {
return outcome;
};
outcome.probe_ok = true;
let table = active_swap_table(swaps);
outcome.wrong = check_active_swap_placement(&table, expected_prefixes);
let active_paths: Vec<PathBuf> = table.iter().map(|e| e.path.clone()).collect();
let Some(config) = config_text else {
return outcome;
};
if !has_swap_devices_block(config) {
return outcome;
}
outcome.config_known = true;
let devices = nixos_swap_devices(config);
outcome.unreferenced = check_swap_references(on_disk, &devices, &active_paths);
outcome
}
pub(crate) fn classify_swapfile(path: &Path, bytes: u64, active: &[PathBuf]) -> Option<DeadSwapfile> {
if active.iter().any(|a| a == path) {
return None;
}
let needs_root = !is_writable_by_current_user(path);
Some(DeadSwapfile {
path: path.to_path_buf(),
bytes,
needs_root,
})
}
pub(crate) fn is_writable_by_current_user(path: &Path) -> bool {
path.parent()
.map(|parent| {
std::fs::metadata(parent)
.map(|m| {
use std::os::unix::fs::PermissionsExt;
let mode = m.permissions().mode();
let uid = unsafe_geteuid();
if uid == 0 {
return true;
}
let owner = std::os::unix::fs::MetadataExt::uid(&m);
let group = std::os::unix::fs::MetadataExt::gid(&m);
let in_group = group_in_current_groups(group);
if owner == uid {
mode & 0o200 != 0
} else if in_group {
mode & 0o020 != 0
} else {
mode & 0o002 != 0
}
})
.unwrap_or(false)
})
.unwrap_or(false)
}
extern "C" {
fn geteuid() -> u32;
}
fn unsafe_geteuid() -> u32 {
unsafe { geteuid() }
}
fn group_in_current_groups(gid: u32) -> bool {
std::fs::read_to_string("/proc/self/status")
.ok()
.and_then(|status| {
status
.lines()
.find(|line| line.starts_with("Groups:"))
.map(|line| {
line.trim_start_matches("Groups:")
.split_whitespace()
.filter_map(|g| g.parse::<u32>().ok())
.any(|g| g == gid)
})
})
.unwrap_or(false)
}
pub(crate) fn classify_stray(path: &Path, name: &str, bytes: u64, is_dir: bool) -> Option<StrayItem> {
if !is_dir {
if BACKUP_ARCHIVE_SUFFIXES.iter().any(|suffix| name.ends_with(suffix)) {
return Some(StrayItem {
path: path.to_path_buf(),
bytes,
kind: StrayKind::BackupArchive,
});
}
return None;
}
if name.ends_with(".git") && name.contains("backup") {
return Some(StrayItem {
path: path.to_path_buf(),
bytes,
kind: StrayKind::BackupRepo,
});
}
if SCRATCH_DIR_MARKERS.iter().any(|marker| name.contains(marker)) {
return Some(StrayItem {
path: path.to_path_buf(),
bytes,
kind: StrayKind::ScratchDir,
});
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_proc_swaps_header_and_entries() {
let contents = "Filename\t\t\t\tType\t\tSize\t\tUsed\t\tPriority\n\
/dev/zram0 partition\t63897596\t0\t5\n\
/mnt/data/swapfile file\t\t67108860\t131704\t-1\n";
let active = active_swap_paths(contents);
assert_eq!(
active,
vec![PathBuf::from("/dev/zram0"), PathBuf::from("/mnt/data/swapfile")]
);
}
#[test]
fn empty_swaps_file_has_no_active_paths() {
assert!(active_swap_paths("").is_empty());
assert!(active_swap_paths("Filename Type Size Used Priority\n").is_empty());
}
#[test]
fn an_active_swapfile_is_never_reported_as_dead() {
let active = vec![PathBuf::from("/mnt/data/swapfile")];
assert!(
classify_swapfile(Path::new("/mnt/data/swapfile"), 1024, &active).is_none(),
"live swap must not be reported"
);
}
#[test]
fn an_inactive_swapfile_is_reported() {
let active = vec![PathBuf::from("/dev/zram0")];
let dead = classify_swapfile(Path::new("/var/lib/swapfile"), 34359738368, &active)
.expect("inactive swapfile should be reported");
assert_eq!(dead.bytes, 34359738368);
assert!(dead.needs_root, "/var/lib is root-owned");
assert_eq!(dead.removal_command(), "sudo rm -f /var/lib/swapfile");
}
#[test]
fn a_user_owned_swapfile_needs_no_sudo() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("swapfile");
std::fs::write(&path, b"x").expect("write");
let dead = classify_swapfile(&path, 1, &[]).expect("reported");
assert!(!dead.needs_root);
assert!(dead.removal_command().starts_with("rm -f "));
}
#[test]
fn backup_archives_are_recognised() {
for name in [
"dracon-platform-pre-slim-20260930.bundle",
"junk-runner-current-tree.tar.gz",
"holdout.tar.zst",
] {
let item = classify_stray(Path::new(name), name, 10, false)
.unwrap_or_else(|| panic!("{name} should be a backup archive"));
assert_eq!(item.kind, StrayKind::BackupArchive);
}
}
#[test]
fn ordinary_files_are_not_strays() {
for name in ["README.md", "Cargo.toml", "main.rs"] {
assert!(classify_stray(Path::new(name), name, 10, false).is_none());
}
}
#[test]
fn scratch_and_backup_directories_are_recognised() {
let cases = [
("hegemon-pre-filterrepo-backup-20260706.git", StrayKind::BackupRepo),
("dracon-platform-orphans", StrayKind::ScratchDir),
("dracon-lfs-proof", StrayKind::ScratchDir),
("eve-audit-new", StrayKind::ScratchDir),
("tmp-shield", StrayKind::ScratchDir),
(".dracon-git-recovery-20260831-1610", StrayKind::ScratchDir),
];
for (name, expected) in cases {
let item = classify_stray(Path::new(name), name, 10, true)
.unwrap_or_else(|| panic!("{name} should be a stray"));
assert_eq!(item.kind, expected, "wrong kind for {name}");
}
}
#[test]
fn swap_table_parses_size_column() {
let contents = "Filename\t\t\t\tType\t\tSize\t\tUsed\t\tPriority\n\
/dev/zram0 partition\t63897596\t0\t5\n\
/mnt/data/swapfile file\t\t67108860\t131704\t-1\n";
let table = active_swap_table(contents);
assert_eq!(table.len(), 2);
assert_eq!(table[0].path, PathBuf::from("/dev/zram0"));
assert_eq!(table[0].size_kib, 63897596);
assert_eq!(table[1].path, PathBuf::from("/mnt/data/swapfile"));
assert_eq!(table[1].size_kib, 67108860);
assert_eq!(
active_swap_paths(contents),
vec![PathBuf::from("/dev/zram0"), PathBuf::from("/mnt/data/swapfile")]
);
}
#[test]
fn misplaced_active_swap_is_flagged() {
let table = active_swap_table(
"Filename Type Size Used Priority\n\
/dev/zram0 partition 63897596 0 5\n\
/var/lib/swapfile file 33554432 0 -1\n",
);
let expected = vec!["/dev/zram".to_string(), "/mnt/data".to_string()];
let wrong = check_active_swap_placement(&table, &expected);
assert_eq!(wrong.len(), 1);
assert_eq!(wrong[0].path, PathBuf::from("/var/lib/swapfile"));
assert_eq!(wrong[0].bytes, 33554432 * 1024);
}
#[test]
fn prefix_matching_respects_component_boundaries() {
assert!(under_prefix(Path::new("/mnt/data/swapfile"), "/mnt/data"));
assert!(under_prefix(Path::new("/mnt/data/swapfile"), "/mnt/data/"));
assert!(under_prefix(Path::new("/mnt/data"), "/mnt/data"));
assert!(!under_prefix(
Path::new("/mnt/database/swapfile"),
"/mnt/data"
));
assert!(!under_prefix(Path::new("/mnt/data2"), "/mnt/data"));
assert!(!under_prefix(Path::new("/mnt/data2/swapfile"), "/mnt/data"));
assert!(under_prefix(Path::new("/dev/zram0"), "/dev/zram"));
assert!(under_prefix(Path::new("/dev/zram11"), "/dev/zram"));
assert!(!under_prefix(Path::new("/dev/zram-control"), "/dev/zram"));
assert!(!under_prefix(Path::new("/dev/sda"), "/dev/zram"));
assert!(!under_prefix(Path::new("/home/u/data2"), "/home/u/data"));
}
#[test]
fn expected_active_swap_is_quiet() {
let table = active_swap_table(
"Filename Type Size Used Priority\n\
/dev/zram0 partition 63897596 0 5\n\
/mnt/data/swapfile file 67108860 0 -1\n",
);
let expected = vec!["/dev/zram".to_string(), "/mnt/data".to_string()];
assert!(check_active_swap_placement(&table, &expected).is_empty());
assert!(check_active_swap_placement(&[], &expected).is_empty());
}
#[test]
fn swapdevices_block_parses_devices_only() {
let config = r#"
fileSystems."/" = { device = "/dev/disk/by-label/nixos"; };
swapDevices = [
{ device = "/mnt/data/swapfile"; size = 64 * 1024; }
# { device = "/old/swapfile"; } -- commented out, must not match
];
"#;
let devices = nixos_swap_devices(config);
assert_eq!(devices, vec![PathBuf::from("/mnt/data/swapfile")]);
}
#[test]
fn swapdevices_ignores_comment_mentions() {
let config = "# Swap managed explicitly via zram and swapDevices below.\n\
boot.kernelParams = [ \"systemd.swap=0\" ];\n\
swapDevices = lib.mkForce [\n\
{\n\
device = \"/dev/disk/by-uuid/5021c67f-4e1f-4a52-94a4-52ca9325d4ef\";\n\
priority = 1;\n\
}\n\
{\n\
device = \"/mnt/data/swapfile\";\n\
size = 65536;\n\
}\n\
];\n";
let devices = nixos_swap_devices(config);
assert_eq!(
devices,
vec![
PathBuf::from("/dev/disk/by-uuid/5021c67f-4e1f-4a52-94a4-52ca9325d4ef"),
PathBuf::from("/mnt/data/swapfile"),
]
);
assert!(has_swap_devices_block(config));
}
#[test]
fn unicode_before_block_parses_and_never_panics() {
let config = "# Tauschger\u{E4}te: \u{E4}\u{F6}\u{FC} \u{65E5}\u{672C}\u{8A9E} swapDevices erw\u{E4}hnt\n\
swapDevices = [ { device = \"/mnt/data/swapfile\"; } ];\n";
assert_eq!(
code_only(config).len(),
config.len(),
"blanked text must be byte-identical in length"
);
assert!(has_swap_devices_block(config));
assert_eq!(
nixos_swap_devices(config),
vec![PathBuf::from("/mnt/data/swapfile")]
);
}
#[test]
fn swapdevices_without_block_is_empty() {
assert!(nixos_swap_devices("fileSystems = {};\n").is_empty());
assert!(nixos_swap_devices("swapDevices = [ { device = \"/x\"; };").is_empty());
}
#[test]
fn unreferenced_swapfiles_are_flagged() {
let on_disk = vec![
(PathBuf::from("/var/lib/swapfile"), 1),
(PathBuf::from("/mnt/data/swapfile"), 2),
(PathBuf::from("/dev/zram0"), 3),
];
let config = vec![PathBuf::from("/mnt/data/swapfile")];
let active = vec![PathBuf::from("/var/lib/swapfile")];
let flagged = check_swap_references(&on_disk, &config, &active);
assert_eq!(flagged.len(), 1);
assert_eq!(flagged[0].path, PathBuf::from("/var/lib/swapfile"));
assert!(flagged[0].is_active, "active-but-unreferenced must be marked");
}
#[test]
fn evaluator_combines_placement_and_references() {
let swaps = "Filename Type Size Used Priority\n\
/dev/zram0 partition 63897596 0 5\n\
/var/lib/swapfile file 33554432 0 -1\n";
let config = "swapDevices = [ { device = \"/mnt/data/swapfile\"; } ];\n";
let expected = vec!["/dev/zram".to_string(), "/mnt/data".to_string()];
let on_disk = vec![(PathBuf::from("/var/lib/swapfile"), 42)];
let outcome = evaluate_swap_health(Some(swaps), Some(config), &expected, &on_disk);
assert!(outcome.probe_ok);
assert!(outcome.config_known);
assert!(outcome.has_findings());
assert_eq!(outcome.wrong.len(), 1);
assert_eq!(outcome.unreferenced.len(), 1);
assert!(outcome.unreferenced[0].is_active);
assert_eq!(outcome.describe().len(), 2);
}
#[test]
fn evaluator_unknown_means_no_findings() {
let expected = vec!["/dev/zram".to_string()];
let outcome = evaluate_swap_health(None, Some("swapDevices = [ ];"), &expected, &[]);
assert!(!outcome.probe_ok);
assert!(!outcome.has_findings());
let swaps = "Filename Type Size Used Priority\n/dev/zram0 partition 1 0 5\n";
let outcome = evaluate_swap_health(
Some(swaps),
Some("fileSystems = {};"),
&expected,
&[(PathBuf::from("/var/lib/swapfile"), 9)],
);
assert!(outcome.probe_ok);
assert!(!outcome.config_known);
assert!(!outcome.has_findings());
}
#[test]
fn ordinary_directories_are_not_strays() {
for name in ["Dev", "Documents", "Pictures", "Videos"] {
assert!(
classify_stray(Path::new(name), name, 10, true).is_none(),
"{name} must never be reported as stray"
);
}
}
}