255 lines
8.3 KiB
Rust
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);
|
|
}
|