mod common;
use common::{TmpDir, find_in_tree};
use std::fs;
use iced_swdir_tree::{DirectoryFilter, DirectoryTree};
#[test]
fn prefetch_disabled_keeps_child_folders_unloaded() {
let td = TmpDir::new("prefetch-off");
fs::create_dir(td.path().join("a")).unwrap();
fs::create_dir(td.path().join("b")).unwrap();
fs::write(td.path().join("a/inside.txt"), b"").unwrap();
fs::write(td.path().join("b/inside.txt"), b"").unwrap();
let mut tree =
DirectoryTree::new(td.path().to_path_buf()).with_filter(DirectoryFilter::FilesAndFolders);
tree.__test_expand_blocking(td.path().to_path_buf());
for name in &["a", "b"] {
let node = find_in_tree(&tree, &td.path().join(name)).expect("child exists");
assert!(node.is_dir);
assert!(
!node.is_loaded,
"{name}: prefetch is disabled, so child folder must not \
have been pre-scanned"
);
assert!(node.children.is_empty());
}
}
#[test]
fn prefetch_loads_child_folders_after_root_expansion() {
let td = TmpDir::new("prefetch-on");
fs::create_dir(td.path().join("alpha")).unwrap();
fs::create_dir(td.path().join("beta")).unwrap();
fs::write(td.path().join("alpha/x.txt"), b"").unwrap();
fs::write(td.path().join("beta/y.txt"), b"").unwrap();
let mut tree = DirectoryTree::new(td.path().to_path_buf())
.with_filter(DirectoryFilter::FilesAndFolders)
.with_prefetch_limit(10);
tree.__test_expand_blocking(td.path().to_path_buf());
for name in &["alpha", "beta"] {
let node = find_in_tree(&tree, &td.path().join(name)).expect("child exists");
assert!(
node.is_loaded,
"{name}: prefetch should have populated this folder"
);
assert_eq!(
node.children.len(),
1,
"{name}: prefetched children should include its own file"
);
}
}
#[test]
fn prefetch_does_not_cascade() {
let td = TmpDir::new("prefetch-nocascade");
fs::create_dir_all(td.path().join("outer/inner")).unwrap();
fs::write(td.path().join("outer/inner/deep.txt"), b"").unwrap();
let mut tree = DirectoryTree::new(td.path().to_path_buf())
.with_filter(DirectoryFilter::FilesAndFolders)
.with_prefetch_limit(5);
tree.__test_expand_blocking(td.path().to_path_buf());
let outer = find_in_tree(&tree, &td.path().join("outer")).expect("outer exists");
assert!(outer.is_loaded, "outer should be prefetched");
let inner_node = outer
.children
.iter()
.find(|c| c.path.file_name().is_some_and(|n| n == "inner"))
.expect("inner must appear as a child of outer");
assert!(
!inner_node.is_loaded,
"cascade: inner must NOT have been prefetched by outer's prefetch"
);
}
#[test]
fn prefetch_respects_max_depth() {
let td = TmpDir::new("prefetch-depth");
fs::create_dir(td.path().join("a")).unwrap();
fs::write(td.path().join("a/file"), b"").unwrap();
let mut tree = DirectoryTree::new(td.path().to_path_buf())
.with_filter(DirectoryFilter::FilesAndFolders)
.with_max_depth(0)
.with_prefetch_limit(5);
tree.__test_expand_blocking(td.path().to_path_buf());
let a = find_in_tree(&tree, &td.path().join("a")).expect("child exists");
assert!(
!a.is_loaded,
"max_depth=0 must prevent prefetch from scanning a/"
);
}
#[test]
fn prefetch_limit_caps_the_number_of_concurrent_scans() {
let td = TmpDir::new("prefetch-limit");
for name in &["aa", "mm", "zz"] {
fs::create_dir(td.path().join(name)).unwrap();
fs::write(td.path().join(name).join("payload"), b"").unwrap();
}
let mut tree = DirectoryTree::new(td.path().to_path_buf())
.with_filter(DirectoryFilter::FilesAndFolders)
.with_prefetch_limit(1);
tree.__test_expand_blocking(td.path().to_path_buf());
let aa = find_in_tree(&tree, &td.path().join("aa")).unwrap();
assert!(
aa.is_loaded,
"first folder in sort order should be prefetched"
);
let zz = find_in_tree(&tree, &td.path().join("zz")).unwrap();
assert!(
!zz.is_loaded,
"with limit=1, only the first folder is prefetched; \
later ones stay un-loaded until the user opens them"
);
}
#[test]
fn clicking_a_prefetched_folder_is_instant() {
use iced_swdir_tree::DirectoryTreeEvent;
let td = TmpDir::new("prefetch-fastpath");
fs::create_dir(td.path().join("ready")).unwrap();
fs::write(td.path().join("ready/file"), b"").unwrap();
let mut tree = DirectoryTree::new(td.path().to_path_buf())
.with_filter(DirectoryFilter::FilesAndFolders)
.with_prefetch_limit(10);
tree.__test_expand_blocking(td.path().to_path_buf());
let ready = td.path().join("ready");
let task = tree.update(DirectoryTreeEvent::Toggled(ready.clone()));
assert_eq!(
task.units(),
0,
"expanding a prefetched folder must not spawn a new scan"
);
let node = find_in_tree(&tree, &ready).unwrap();
assert!(node.is_expanded);
assert!(!node.children.is_empty());
}
fn build_repo_fixture(tag: &str) -> TmpDir {
let td = TmpDir::new(tag);
fs::create_dir(td.path().join(".git")).unwrap();
fs::write(td.path().join(".git/HEAD"), b"ref: refs/heads/main\n").unwrap();
fs::create_dir(td.path().join(".git/objects")).unwrap();
fs::create_dir(td.path().join("node_modules")).unwrap();
fs::write(td.path().join("node_modules/.package-lock.json"), b"{}").unwrap();
fs::create_dir(td.path().join("target")).unwrap();
fs::create_dir(td.path().join("target/debug")).unwrap();
fs::create_dir(td.path().join("src")).unwrap();
fs::write(td.path().join("src/main.rs"), b"fn main() {}\n").unwrap();
fs::create_dir(td.path().join("docs")).unwrap();
fs::write(td.path().join("docs/readme.md"), b"docs\n").unwrap();
td
}
#[test]
fn default_skip_list_prevents_prefetching_dot_git() {
let td = build_repo_fixture("skip-default-git");
let mut tree = DirectoryTree::new(td.path().to_path_buf())
.with_filter(DirectoryFilter::AllIncludingHidden)
.with_prefetch_limit(10);
tree.__test_expand_blocking(td.path().to_path_buf());
let dot_git = find_in_tree(&tree, &td.path().join(".git")).expect(".git row exists");
assert!(
!dot_git.is_loaded,
"default skip list must have prevented prefetch of .git"
);
assert!(dot_git.children.is_empty(), "no children scanned");
let src = find_in_tree(&tree, &td.path().join("src")).unwrap();
assert!(
src.is_loaded,
"ordinary src/ should have been prefetched normally"
);
}
#[test]
fn default_skip_list_prevents_prefetching_node_modules_and_target() {
let td = build_repo_fixture("skip-default-all");
let mut tree = DirectoryTree::new(td.path().to_path_buf())
.with_filter(DirectoryFilter::AllIncludingHidden)
.with_prefetch_limit(10);
tree.__test_expand_blocking(td.path().to_path_buf());
for skipped in &["node_modules", "target"] {
let node = find_in_tree(&tree, &td.path().join(skipped))
.unwrap_or_else(|| panic!("{skipped} row missing"));
assert!(
!node.is_loaded,
"{skipped} must not be prefetched by default"
);
}
}
#[test]
fn user_click_still_scans_a_skipped_folder() {
use iced_swdir_tree::DirectoryTreeEvent;
let td = build_repo_fixture("skip-user-click");
let mut tree = DirectoryTree::new(td.path().to_path_buf())
.with_filter(DirectoryFilter::AllIncludingHidden)
.with_prefetch_limit(10);
tree.__test_expand_blocking(td.path().to_path_buf());
let dot_git = td.path().join(".git");
assert!(!find_in_tree(&tree, &dot_git).unwrap().is_loaded);
let task = tree.update(DirectoryTreeEvent::Toggled(dot_git.clone()));
assert_ne!(
task.units(),
0,
"user click on a skipped folder must produce a real scan Task"
);
}
#[test]
fn custom_skip_list_replaces_defaults() {
let td = build_repo_fixture("skip-custom");
let mut tree = DirectoryTree::new(td.path().to_path_buf())
.with_filter(DirectoryFilter::AllIncludingHidden)
.with_prefetch_limit(10)
.with_prefetch_skip(vec!["docs"]);
tree.__test_expand_blocking(td.path().to_path_buf());
let docs = find_in_tree(&tree, &td.path().join("docs")).unwrap();
assert!(!docs.is_loaded, "docs/ is in the custom skip list");
let dot_git = find_in_tree(&tree, &td.path().join(".git")).unwrap();
assert!(
dot_git.is_loaded,
"custom list replaces the default — .git is no longer skipped"
);
}
#[test]
fn empty_skip_list_disables_the_safety_valve() {
let td = build_repo_fixture("skip-disabled");
let mut tree = DirectoryTree::new(td.path().to_path_buf())
.with_filter(DirectoryFilter::AllIncludingHidden)
.with_prefetch_limit(10)
.with_prefetch_skip(Vec::<String>::new());
tree.__test_expand_blocking(td.path().to_path_buf());
let dot_git = find_in_tree(&tree, &td.path().join(".git")).unwrap();
assert!(
dot_git.is_loaded,
"an empty skip list disables the safety valve entirely"
);
}
#[test]
fn default_prefetch_skip_is_reexported_from_crate_root() {
use iced_swdir_tree::DEFAULT_PREFETCH_SKIP;
assert!(DEFAULT_PREFETCH_SKIP.contains(&".git"));
assert!(DEFAULT_PREFETCH_SKIP.contains(&"node_modules"));
assert!(DEFAULT_PREFETCH_SKIP.contains(&"target"));
}