Files
sized/tests/scanning.rs
T

255 lines
8.3 KiB
Rust

#![cfg(unix)]
use sized::{build_tree, scan_tree, EntryType, ScanControl, ScanError, ScanOptions};
use std::{
fs,
io::Write,
os::unix::fs::{symlink, MetadataExt, PermissionsExt},
};
use tempfile::tempdir;
fn scan(path: &std::path::Path) -> sized::ScanReport {
scan_tree(path, &ScanOptions::default(), &ScanControl::default()).unwrap()
}
#[test]
fn hard_links_count_allocation_once_but_keep_apparent_sizes() {
let dir = tempdir().unwrap();
let original = dir.path().join("a-original");
fs::write(&original, [9; 65536]).unwrap();
fs::hard_link(&original, dir.path().join("z-alias")).unwrap();
let expected =
fs::metadata(&original).unwrap().blocks() + fs::metadata(dir.path()).unwrap().blocks();
for threads in [1, 4] {
let report = scan_tree(
dir.path(),
&ScanOptions {
threads: Some(threads),
..ScanOptions::default()
},
&ScanControl::default(),
)
.unwrap();
assert_eq!(report.root.blocks, expected);
assert_eq!(report.root.children[0].size_bytes, 65536);
assert_eq!(report.root.children[1].size_bytes, 65536);
assert!(!report.root.children[0].hard_link_duplicate);
assert!(report.root.children[1].hard_link_duplicate);
assert_eq!(report.root.children[1].blocks, 0);
assert_eq!(report.entries_scanned, 3);
assert!(report.root.complete);
}
}
#[test]
fn ignored_hard_link_does_not_steal_filtered_allocation() {
let dir = tempdir().unwrap();
fs::write(dir.path().join(".ignore"), "a-ignored\n").unwrap();
let original = dir.path().join("a-ignored");
fs::write(&original, [9; 65536]).unwrap();
fs::hard_link(&original, dir.path().join("z-included")).unwrap();
let report = scan_tree(
dir.path(),
&ScanOptions {
compare_ignore: true,
..ScanOptions::default()
},
&ScanControl::default(),
)
.unwrap();
let expected = fs::metadata(&original).unwrap().blocks()
+ fs::metadata(dir.path()).unwrap().blocks()
+ fs::metadata(dir.path().join(".ignore")).unwrap().blocks();
assert_eq!(report.root.blocks, expected);
assert_eq!(report.root.blocks_filtered, expected);
assert_eq!(report.root.children.last().unwrap().blocks, 0);
assert!(report.root.children.last().unwrap().blocks_filtered > 0);
}
#[test]
fn hidden_files_are_counted_without_ignore_filtering() {
let dir = tempdir().unwrap();
fs::write(dir.path().join(".hidden"), b"hidden").unwrap();
fs::write(dir.path().join(".ignore"), "build\n").unwrap();
fs::write(dir.path().join("build"), b"build").unwrap();
assert_eq!(scan(dir.path()).root.children.len(), 3);
let report = scan_tree(
dir.path(),
&ScanOptions {
respect_ignore: true,
..ScanOptions::default()
},
&ScanControl::default(),
)
.unwrap();
assert_eq!(report.root.children.len(), 2);
assert!(report
.root
.children
.iter()
.any(|n| n.path.ends_with(".hidden")));
}
#[test]
fn ignore_rules_are_inherited_by_nested_directories() {
let dir = tempdir().unwrap();
fs::write(dir.path().join(".ignore"), "*.tmp\n").unwrap();
let nested = dir.path().join("nested");
fs::create_dir(&nested).unwrap();
fs::write(nested.join("skip.tmp"), b"ignored").unwrap();
fs::write(nested.join("keep.bin"), b"included").unwrap();
let report = scan_tree(
dir.path(),
&ScanOptions {
respect_ignore: true,
..ScanOptions::default()
},
&ScanControl::default(),
)
.unwrap();
let children = &report
.root
.children
.iter()
.find(|n| n.path == nested)
.unwrap()
.children;
assert_eq!(children.len(), 1);
assert!(children[0].path.ends_with("keep.bin"));
let compared = scan_tree(
dir.path(),
&ScanOptions {
compare_ignore: true,
..ScanOptions::default()
},
&ScanControl::default(),
)
.unwrap();
assert_eq!(compared.root.size_bytes_filtered, report.root.size_bytes);
assert_eq!(compared.root.blocks_filtered, report.root.blocks);
}
#[test]
fn sparse_file_reports_blocks_and_logical_length_separately() {
let dir = tempdir().unwrap();
let path = dir.path().join("sparse");
let mut file = fs::File::create(&path).unwrap();
file.write_all(&[7; 4096]).unwrap();
file.set_len(128 * 1024 * 1024).unwrap();
file.sync_all().unwrap();
let node = scan(&path).root;
assert_eq!(node.size_bytes, 128 * 1024 * 1024);
assert_eq!(node.blocks, fs::metadata(path).unwrap().blocks());
assert!(node.blocks * 512 < node.size_bytes);
}
#[test]
fn symlinks_including_loop_and_dangling_link_are_leaves() {
let dir = tempdir().unwrap();
symlink(dir.path(), dir.path().join("loop")).unwrap();
symlink(dir.path().join("missing"), dir.path().join("dangling")).unwrap();
let report = scan(dir.path());
assert_eq!(report.root.children.len(), 2);
for node in report.root.children {
assert!(node.symlink);
assert!(node.children.is_empty());
assert!(node.complete);
}
}
#[test]
fn missing_root_returns_error_and_legacy_helper_marks_unavailable() {
let dir = tempdir().unwrap();
let path = dir.path().join("vanished");
assert!(
matches!(scan_tree(&path, &ScanOptions::default(), &ScanControl::default()),
Err(ScanError::Root { source, .. }) if source.kind() == std::io::ErrorKind::NotFound)
);
let node = build_tree(&path, false, false);
assert_eq!(node.entry_type, EntryType::Unknown);
assert!(!node.accessible);
assert!(!node.complete);
}
#[test]
fn permission_error_propagates_incomplete_status_to_root() {
let dir = tempdir().unwrap();
let blocked = dir.path().join("blocked");
fs::create_dir(&blocked).unwrap();
fs::write(blocked.join("unreadable"), b"data").unwrap();
fs::set_permissions(&blocked, fs::Permissions::from_mode(0o000)).unwrap();
let result = std::panic::catch_unwind(|| {
assert!(
fs::read_dir(&blocked).is_err(),
"permission tests require an unprivileged user"
);
let report = scan(dir.path());
assert!(report.root.accessible);
assert!(!report.root.complete);
assert!(!report.root.children[0].accessible);
assert_eq!(report.issues.len(), 1);
assert_eq!(report.issues[0].path, blocked);
assert_eq!(report.issues[0].kind, std::io::ErrorKind::PermissionDenied);
});
fs::set_permissions(blocked, fs::Permissions::from_mode(0o700)).unwrap();
result.unwrap();
}
#[test]
fn cancelled_scan_never_returns_completed_tree() {
let dir = tempdir().unwrap();
let control = ScanControl::default();
control.cancel();
assert!(matches!(
scan_tree(dir.path(), &ScanOptions::default(), &control),
Err(ScanError::Cancelled)
));
assert_eq!(control.entries_scanned(), 0);
}
#[test]
fn cancellation_and_progress_work_during_scan() {
let dir = tempdir().unwrap();
for i in 0..2000 {
fs::write(dir.path().join(format!("file-{i}")), b"x").unwrap();
}
let control = ScanControl::default();
let watcher = control.clone();
let cancel = std::thread::spawn(move || {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while watcher.entries_scanned() < 10 {
assert!(
std::time::Instant::now() < deadline,
"scan made no progress"
);
std::thread::yield_now();
}
watcher.cancel();
});
let result = scan_tree(
dir.path(),
&ScanOptions {
threads: Some(1),
..ScanOptions::default()
},
&control,
);
cancel.join().unwrap();
assert!(matches!(result, Err(ScanError::Cancelled)));
assert!(control.entries_scanned() >= 10);
}
#[test]
fn special_file_is_not_treated_as_directory() {
let dir = tempdir().unwrap();
let path = dir.path().join("fifo");
assert!(std::process::Command::new("mkfifo")
.arg(&path)
.status()
.unwrap()
.success());
let node = scan(&path).root;
assert_eq!(node.entry_type, EntryType::Special);
assert!(node.children.is_empty());
assert!(node.complete);
}