Adopt pinned public core snapshot and prepare Sized 2.0.0
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use sized::{scan_tree, ScanControl, ScanOptions};
|
||||
use std::{fs, io::Write, os::unix::fs::MetadataExt, path::PathBuf};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires the two owned tmpfs mounts from scripts/check-linux.sh"]
|
||||
fn real_mount_boundary_is_respected_by_scanner() {
|
||||
let root = PathBuf::from(
|
||||
std::env::var_os("SIZED_TEST_MOUNT_ROOT").expect("missing mounted Linux fixture"),
|
||||
);
|
||||
let foreign = root.join("foreign");
|
||||
let root_metadata = fs::metadata(&root).unwrap();
|
||||
let foreign_metadata = fs::metadata(&foreign).unwrap();
|
||||
assert_ne!(
|
||||
root_metadata.dev(),
|
||||
foreign_metadata.dev(),
|
||||
"not a real boundary"
|
||||
);
|
||||
let mut local_file = NamedTempFile::new_in(&root).unwrap();
|
||||
let mut mounted_file = NamedTempFile::new_in(&foreign).unwrap();
|
||||
local_file.write_all(b"local allocation").unwrap();
|
||||
mounted_file.write_all(&[7; 8192]).unwrap();
|
||||
local_file.as_file().sync_all().unwrap();
|
||||
mounted_file.as_file().sync_all().unwrap();
|
||||
|
||||
for bounded in [false, true] {
|
||||
let report = scan_tree(
|
||||
&root,
|
||||
&ScanOptions {
|
||||
stay_on_filesystem: bounded,
|
||||
threads: Some(2),
|
||||
..ScanOptions::default()
|
||||
},
|
||||
&ScanControl::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(report.root.complete);
|
||||
assert!(report.issues.is_empty());
|
||||
let mount = report
|
||||
.root
|
||||
.children
|
||||
.iter()
|
||||
.find(|n| n.path == foreign)
|
||||
.unwrap();
|
||||
assert_eq!(mount.identity.unwrap().device, foreign_metadata.dev());
|
||||
assert_eq!(mount.skipped_mount, bounded);
|
||||
assert_eq!(report.entries_scanned, if bounded { 3 } else { 4 });
|
||||
let expected_blocks =
|
||||
root_metadata.blocks() + local_file.as_file().metadata().unwrap().blocks();
|
||||
if bounded {
|
||||
assert!(mount.children.is_empty());
|
||||
assert_eq!(mount.blocks, 0);
|
||||
assert_eq!(mount.size_bytes, 0);
|
||||
assert_eq!(report.root.blocks, expected_blocks);
|
||||
} else {
|
||||
assert_eq!(mount.children.len(), 1);
|
||||
assert_eq!(mount.children[0].path, mounted_file.path());
|
||||
assert_eq!(mount.children[0].size_bytes, 8192);
|
||||
assert_eq!(
|
||||
report.root.blocks,
|
||||
expected_blocks
|
||||
+ foreign_metadata.blocks()
|
||||
+ mounted_file.as_file().metadata().unwrap().blocks()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
#![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);
|
||||
}
|
||||
Reference in New Issue
Block a user