#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::fs;
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use pristine::{Claim, Found, Hit, Kind, Ruleset, Size, SizeMode, Walker};
use tempfile::TempDir;
const OVER: usize = 256 * 1024;
const FLOOR: u64 = 128 * 1024;
fn write(path: &Path, bytes: usize) {
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, vec![b'x'; bytes]).unwrap();
}
fn touch(path: &Path) {
write(path, 0);
}
fn ruleset() -> Arc<Ruleset> {
Arc::new(Ruleset::builtin().unwrap())
}
fn git(repo: &Path, args: &[&str]) {
let mut command = Command::new("git");
command
.current_dir(repo)
.args(args)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.stdin(Stdio::null());
for variable in [
"GIT_DIR",
"GIT_INDEX_FILE",
"GIT_WORK_TREE",
"GIT_COMMON_DIR",
"GIT_OBJECT_DIRECTORY",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_CEILING_DIRECTORIES",
"GIT_NAMESPACE",
] {
command.env_remove(variable);
}
let output = command.output().unwrap();
assert!(
output.status.success(),
"git {args:?} in {}: {}",
repo.display(),
String::from_utf8_lossy(&output.stderr)
);
}
fn init_repo(path: &Path) {
fs::create_dir_all(path).unwrap();
git(path, &["init", "--quiet"]);
}
fn walker(root: &Path) -> Walker {
Walker::new(root, ruleset()).min_size(FLOOR)
}
fn scan_with(walker: &Walker) -> Vec<Hit> {
let hits = Mutex::new(Vec::new());
let sizes = Mutex::new(Vec::new());
let outcome = walker.run(|found| match found {
Found::Claim(hit) => hits.lock().unwrap().push(hit),
Found::Pricing(_) => {}
Found::Priced(priced) => sizes.lock().unwrap().push(priced),
});
assert!(
outcome.errors.is_empty(),
"unexpected: {:?}",
outcome.errors
);
let mut hits = hits.into_inner().unwrap();
for priced in sizes.into_inner().unwrap() {
let hit = hits
.iter_mut()
.find(|hit| hit.path == priced.path)
.expect("a price arrived for a claim nobody reported");
hit.size = priced.size;
}
hits.sort_by(|a, b| a.path.cmp(&b.path));
hits
}
fn scan(root: &Path) -> Vec<Hit> {
scan_with(&walker(root))
}
fn claimed(root: &Path) -> Vec<String> {
scan(root)
.iter()
.map(|hit| {
hit.path
.strip_prefix(root)
.unwrap()
.to_string_lossy()
.into_owned()
})
.collect()
}
#[test]
fn an_ignored_directory_with_nothing_tracked_under_it_is_reclaimable() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("sediment/blob.bin"), OVER);
let hits = scan(tmp.path());
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].path, tmp.path().join("sediment"));
let Claim::Ignored(ref claim) = hits[0].claim else {
panic!("expected a tier-two claim, got {:?}", hits[0].claim)
};
assert_eq!(claim.work_tree, tmp.path());
}
#[test]
fn a_tier_two_hit_admits_it_does_not_know_what_the_directory_is() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("sediment/blob.bin"), OVER);
touch(&tmp.path().join("app/package.json"));
write(&tmp.path().join("app/node_modules/dep/index.js"), OVER);
let hits = scan(tmp.path());
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].path, tmp.path().join("app/node_modules"));
assert_eq!(hits[0].label(), "Node Dependencies");
assert_eq!(hits[1].path, tmp.path().join("sediment"));
assert_eq!(hits[1].label(), "Gitignored, kind unknown");
assert!(hits[1].rule().is_none());
}
#[test]
fn an_ignored_directory_holding_a_tracked_file_is_never_claimed() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("sediment/blob.bin"), OVER);
touch(&tmp.path().join("sediment/deep/nested/keep.txt"));
git(tmp.path(), &["add", "-f", "sediment/deep/nested/keep.txt"]);
assert!(claimed(tmp.path()).is_empty());
}
#[test]
fn a_sibling_of_a_tracked_file_inside_an_ignored_directory_is_still_claimed() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
touch(&tmp.path().join("sediment/keep.txt"));
git(tmp.path(), &["add", "-f", "sediment/keep.txt"]);
write(&tmp.path().join("sediment/scratch/blob.bin"), OVER);
assert_eq!(claimed(tmp.path()), ["sediment/scratch"]);
}
#[test]
fn an_ignored_directory_below_the_floor_is_left_alone() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("sediment/crumb.bin"), 1024);
assert!(claimed(tmp.path()).is_empty());
}
#[test]
fn the_floor_defaults_to_ten_mebibytes() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("sediment/blob.bin"), OVER);
assert_eq!(claimed(tmp.path()), ["sediment"]);
let hits = scan_with(&Walker::new(tmp.path(), ruleset()));
assert!(hits.is_empty(), "the default floor let {hits:?} through");
assert_eq!(pristine::DEFAULT_MIN_SIZE, 10 * 1024 * 1024);
}
#[test]
fn an_untracked_directory_that_nothing_ignores_is_left_alone() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
write(&tmp.path().join("manuscript/draft.bin"), OVER);
assert!(claimed(tmp.path()).is_empty());
}
#[test]
fn the_whole_ignore_stack_is_consulted_not_just_the_root_file() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::create_dir_all(tmp.path().join("svc")).unwrap();
fs::write(tmp.path().join("svc/.gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("svc/sediment/blob.bin"), OVER);
write(&tmp.path().join("other/sediment/blob.bin"), OVER);
assert_eq!(claimed(tmp.path()), ["svc/sediment"]);
}
#[test]
fn a_negation_puts_a_directory_back() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "out/\n!out/keep/\n").unwrap();
write(&tmp.path().join("out/blob.bin"), OVER);
write(&tmp.path().join("out/keep/blob.bin"), OVER);
assert_eq!(claimed(tmp.path()), ["out"]);
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "out/*\n!out/keep\n").unwrap();
write(&tmp.path().join("out/sediment/blob.bin"), OVER);
write(&tmp.path().join("out/keep/blob.bin"), OVER);
assert_eq!(claimed(tmp.path()), ["out/sediment"]);
}
#[test]
fn info_exclude_is_part_of_the_stack() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::create_dir_all(tmp.path().join(".git/info")).unwrap();
fs::write(tmp.path().join(".git/info/exclude"), "scratch/\n").unwrap();
write(&tmp.path().join("scratch/blob.bin"), OVER);
assert_eq!(claimed(tmp.path()), ["scratch"]);
}
#[test]
fn a_tier_one_rule_keeps_what_it_claims() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "node_modules/\n").unwrap();
touch(&tmp.path().join("package.json"));
write(&tmp.path().join("node_modules/dep/index.js"), OVER);
let hits = scan(tmp.path());
assert_eq!(hits.len(), 1);
assert!(matches!(hits[0].claim, Claim::Rule(_)));
assert_eq!(hits[0].label(), "Node Dependencies");
}
#[test]
fn what_tier_two_claims_it_does_not_descend_into() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
touch(&tmp.path().join("sediment/vendored/package.json"));
write(
&tmp.path()
.join("sediment/vendored/node_modules/dep/index.js"),
OVER,
);
assert_eq!(claimed(tmp.path()), ["sediment"]);
}
#[test]
fn outside_a_git_work_tree_the_fallback_is_inert_and_says_so() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("sediment/blob.bin"), OVER);
let outcome = walker(tmp.path()).run(drop);
assert_eq!(outcome.hits, 0);
assert_eq!(outcome.fallback.work_trees, 0);
assert!(outcome.fallback.enabled);
assert!(
outcome.fallback.is_inert(),
"inertness has to be reported, not left to look like an empty result: {:?}",
outcome.fallback
);
assert!(outcome.fallback.outside_work_tree > 0);
}
#[test]
fn a_sweep_reports_the_work_trees_it_consulted_and_the_ground_it_could_not_judge() {
let tmp = TempDir::new().unwrap();
for repo in ["one", "two"] {
let root = tmp.path().join(repo);
init_repo(&root);
fs::write(root.join(".gitignore"), "sediment/\n").unwrap();
write(&root.join("sediment/blob.bin"), OVER);
}
write(&tmp.path().join("loose/blob.bin"), OVER);
let outcome = walker(tmp.path()).run(drop);
assert_eq!(outcome.hits, 2);
assert_eq!(outcome.fallback.hits, 2);
assert_eq!(outcome.fallback.work_trees, 2);
assert!(!outcome.fallback.is_inert());
assert!(outcome.fallback.outside_work_tree > 0);
}
#[test]
fn the_work_tree_may_sit_above_the_scan_root() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("packages/ui/sediment/blob.bin"), OVER);
let hits = scan(&tmp.path().join("packages/ui"));
assert_eq!(hits.len(), 1);
let Claim::Ignored(ref claim) = hits[0].claim else {
panic!("expected a tier-two claim, got {:?}", hits[0].claim)
};
assert_eq!(claim.work_tree, tmp.path());
}
#[test]
fn an_outer_repositorys_ignore_rules_stop_at_a_nested_repository() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
init_repo(&tmp.path().join("inner"));
write(&tmp.path().join("inner/sediment/blob.bin"), OVER);
write(&tmp.path().join("sediment/blob.bin"), OVER);
assert_eq!(claimed(tmp.path()), ["sediment"]);
}
#[test]
fn a_tracked_file_in_the_outer_repository_does_not_shield_a_nested_one() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
init_repo(&tmp.path().join("inner"));
fs::write(tmp.path().join("inner/.gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("inner/sediment/blob.bin"), OVER);
touch(&tmp.path().join("inner/sediment/decoy.txt"));
assert_eq!(claimed(tmp.path()), ["inner/sediment"]);
}
#[test]
fn a_submodule_inside_an_ignored_directory_bars_the_claim() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("sediment/blob.bin"), OVER);
fs::create_dir_all(tmp.path().join("sediment/sub")).unwrap();
git(
tmp.path(),
&[
"update-index",
"--add",
"--cacheinfo",
"160000,4b825dc642cb6eb9a060e54bf8d69288fbee4904,sediment/sub",
],
);
assert!(claimed(tmp.path()).is_empty());
}
#[test]
fn a_repository_that_will_not_answer_is_reported_rather_than_guessed_at() {
let tmp = TempDir::new().unwrap();
fs::create_dir_all(tmp.path().join(".git")).unwrap();
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("sediment/blob.bin"), OVER);
let outcome = walker(tmp.path()).run(drop);
assert_eq!(outcome.hits, 0);
assert!(outcome.fallback.is_inert());
assert_eq!(outcome.errors.len(), 1, "{:?}", outcome.errors);
assert_eq!(outcome.errors[0].path.as_deref(), Some(tmp.path()));
}
#[cfg(unix)]
#[test]
fn a_subtree_that_cannot_be_read_through_is_not_claimed() {
use std::os::unix::fs::PermissionsExt;
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("sediment/blob.bin"), OVER);
let sealed = tmp.path().join("sediment/sealed");
fs::create_dir(&sealed).unwrap();
fs::set_permissions(&sealed, fs::Permissions::from_mode(0o000)).unwrap();
if fs::read_dir(&sealed).is_ok() {
fs::set_permissions(&sealed, fs::Permissions::from_mode(0o755)).unwrap();
return; }
let outcome = walker(tmp.path()).run(drop);
fs::set_permissions(&sealed, fs::Permissions::from_mode(0o755)).unwrap();
assert_eq!(outcome.hits, 0);
assert!(!outcome.errors.is_empty());
assert!(
outcome
.errors
.iter()
.all(|error| error.path.as_deref() == Some(sealed.as_path())),
"{:?}",
outcome.errors
);
}
#[test]
fn a_tracked_file_under_a_non_ascii_path_still_bars_the_claim() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "build/\n").unwrap();
let cafe = "cafe\u{301}";
write(&tmp.path().join(cafe).join("build/blob.bin"), OVER);
touch(&tmp.path().join(cafe).join("build/keep.txt"));
git(
tmp.path(),
&["add", "-f", &format!("{cafe}/build/keep.txt")],
);
assert!(
claimed(tmp.path()).is_empty(),
"a tracked file went missing behind a normalization mismatch"
);
}
#[test]
fn a_decoy_git_dir_in_the_environment_cannot_redirect_the_index_lookup() {
let decoy = TempDir::new().unwrap();
init_repo(decoy.path());
touch(&decoy.path().join("decoy.txt"));
git(decoy.path(), &["add", "decoy.txt"]);
let status = Command::new(std::env::current_exe().unwrap())
.args([
"the_scan_that_runs_under_a_decoy_git_dir",
"--exact",
"--ignored",
"--nocapture",
])
.env("GIT_DIR", decoy.path().join(".git"))
.env("GIT_INDEX_FILE", decoy.path().join(".git/index"))
.env("GIT_WORK_TREE", decoy.path())
.status()
.unwrap();
assert!(
status.success(),
"the scan read the decoy repository's index instead of the one it was pointed at"
);
}
#[test]
#[ignore = "run by its parent with a decoy GIT_DIR in the environment"]
fn the_scan_that_runs_under_a_decoy_git_dir() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("sediment/blob.bin"), OVER);
touch(&tmp.path().join("sediment/keep.txt"));
git(tmp.path(), &["add", "-f", "sediment/keep.txt"]);
assert!(claimed(tmp.path()).is_empty());
}
#[test]
fn the_fallback_can_be_switched_off() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("sediment/blob.bin"), OVER);
let outcome = walker(tmp.path()).fallback(false).run(drop);
assert_eq!(outcome.hits, 0);
assert!(!outcome.fallback.enabled);
assert!(
!outcome.fallback.is_inert(),
"a tier switched off is not a tier with nothing to work with"
);
}
#[test]
fn a_tier_two_claim_carries_a_real_size_even_on_a_default_scan() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\nnode_modules/\n").unwrap();
for part in 0..8 {
write(&tmp.path().join(format!("sediment/{part}.bin")), OVER);
}
touch(&tmp.path().join("app/package.json"));
write(&tmp.path().join("app/node_modules/dep/index.js"), OVER);
let hits = scan(tmp.path());
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].size, Size::Unmeasured, "tier one");
let bytes = hits[1].size.bytes().expect("tier two");
assert!(bytes >= 8 * OVER as u64, "{bytes}");
}
#[test]
fn a_directory_holding_a_checkout_is_not_collapsed_into_one_removal() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sandboxes/\n").unwrap();
init_repo(&tmp.path().join("sandboxes/box/checkout"));
write(&tmp.path().join("sandboxes/box/checkout/draft.bin"), OVER);
write(&tmp.path().join("sandboxes/spill/blob.bin"), OVER);
let outcome = walker(tmp.path()).run(drop);
assert_eq!(claimed(tmp.path()), ["sandboxes/spill"]);
assert!(outcome.fallback.holding_a_checkout >= 1);
}
#[test]
fn a_linked_work_trees_dot_git_file_counts_as_a_checkout() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\n").unwrap();
write(&tmp.path().join("sediment/blob.bin"), OVER);
fs::create_dir_all(tmp.path().join("sediment/worktree")).unwrap();
fs::write(
tmp.path().join("sediment/worktree/.git"),
"gitdir: /elsewhere/.git/worktrees/w\n",
)
.unwrap();
let outcome = walker(tmp.path()).run(drop);
assert_eq!(outcome.hits, 0);
assert_eq!(outcome.fallback.holding_a_checkout, 1);
assert_eq!(outcome.errors.len(), 1, "{:?}", outcome.errors);
}
#[test]
fn the_rollup_tree_carries_tier_two_claims_alongside_tier_one() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "sediment/\nnode_modules/\n").unwrap();
write(&tmp.path().join("app/sediment/blob.bin"), OVER);
touch(&tmp.path().join("app/package.json"));
write(&tmp.path().join("app/node_modules/dep/index.js"), OVER);
let (tree, outcome) = walker(tmp.path())
.size_mode(SizeMode::Breakdown)
.run_to_tree();
assert_eq!(outcome.hits, 2);
let app = tree.find(&tmp.path().join("app")).unwrap();
let sediment = tree.find(&tmp.path().join("app/sediment")).unwrap();
let modules = tree.find(&tmp.path().join("app/node_modules")).unwrap();
assert_eq!(
tree.node(app).reclaimable,
tree.node(sediment).reclaimable + tree.node(modules).reclaimable
);
assert_eq!(tree.reclaimable(), outcome.reclaimable_bytes);
}
fn with_files(root: &Path) -> Walker {
walker(root).ignored_files(true)
}
fn repo_with_ignored_files() -> TempDir {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), ".env*\n*.log\nscraps/\n").unwrap();
write(&tmp.path().join(".env"), 40);
write(&tmp.path().join(".env.local"), 40);
write(&tmp.path().join("build.log"), 40);
write(&tmp.path().join("scraps/leftover.bin"), 16);
touch(&tmp.path().join("kept.txt"));
git(tmp.path(), &["add", "kept.txt", ".gitignore"]);
tmp
}
#[test]
fn a_gitignored_file_is_invisible_until_the_walk_is_asked_for_files() {
let tmp = repo_with_ignored_files();
assert!(
claimed(tmp.path()).is_empty(),
"a default sweep claims no files, and `scraps/` is under the floor"
);
let mut found: Vec<String> = scan_with(&with_files(tmp.path()))
.iter()
.map(|hit| {
hit.path
.strip_prefix(tmp.path())
.unwrap()
.to_string_lossy()
.into_owned()
})
.collect();
found.sort();
assert_eq!(
found,
[".env", ".env.local", "build.log", "scraps/leftover.bin"]
);
}
#[test]
fn a_directory_the_floor_refused_is_descended_into_and_its_files_claimed_one_by_one() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "small/\nbig/\n").unwrap();
write(&tmp.path().join("small/.env"), 40);
write(&tmp.path().join("big/.env"), 40);
write(&tmp.path().join("big/blob.bin"), OVER);
let mut found: Vec<String> = scan_with(&with_files(tmp.path()))
.iter()
.map(|hit| {
hit.path
.strip_prefix(tmp.path())
.unwrap()
.to_string_lossy()
.into_owned()
})
.collect();
found.sort();
assert_eq!(found, ["big", "small/.env"]);
}
#[test]
fn the_size_floor_does_not_reach_a_file() {
let tmp = repo_with_ignored_files();
let hits = scan_with(&with_files(tmp.path()).min_size(1024 * 1024));
assert_eq!(hits.len(), 4, "{hits:?}");
assert!(
hits.iter()
.all(|hit| hit.size.bytes().unwrap() < 1024 * 1024),
"every one of these is far below the floor it was found under"
);
assert!(
!hits.iter().any(|hit| hit.path.ends_with("scraps")),
"the floor still keeps a small ignored DIRECTORY off the list"
);
}
#[test]
fn a_file_is_always_priced_even_on_a_scan_that_prices_nothing_else() {
let tmp = repo_with_ignored_files();
touch(&tmp.path().join("app/package.json"));
write(&tmp.path().join("app/node_modules/dep/index.js"), OVER);
let hits = scan_with(&with_files(tmp.path()).size_mode(SizeMode::Skip));
let env = hits.iter().find(|hit| hit.path.ends_with(".env")).unwrap();
assert!(env.size.bytes().is_some(), "a file arrived unpriced");
let modules = hits
.iter()
.find(|hit| hit.path.ends_with("node_modules"))
.unwrap();
assert_eq!(
modules.size,
Size::Unmeasured,
"a default scan still prices no tier-one directory"
);
}
#[test]
fn a_files_kind_is_read_off_its_name_and_says_what_losing_it_costs() {
let tmp = repo_with_ignored_files();
let hits = scan_with(&with_files(tmp.path()));
let kind_of = |name: &str| {
hits.iter()
.find(|hit| hit.path.ends_with(name))
.unwrap_or_else(|| panic!("no claim for {name}"))
.kind()
};
assert_eq!(kind_of(".env"), Some(Kind::Unrecoverable));
assert_eq!(kind_of(".env.local"), Some(Kind::Unrecoverable));
assert_eq!(kind_of("build.log"), Some(Kind::Noise));
let env = hits.iter().find(|hit| hit.path.ends_with(".env")).unwrap();
assert_eq!(env.label(), "Gitignored, unrecoverable");
assert!(env.is_ignored_file());
assert!(env.rule().is_none(), "no rule named it, and none could");
}
#[test]
fn a_gitignored_file_whose_name_says_nothing_is_still_claimed_and_still_unnamed() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "dump.sql\n").unwrap();
write(&tmp.path().join("dump.sql"), 4096);
let hits = scan_with(&with_files(tmp.path()));
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].kind(), None);
assert_eq!(hits[0].label(), "Gitignored, kind unknown");
assert!(hits[0].is_ignored_file());
}
#[test]
fn a_tracked_file_matching_an_ignore_pattern_is_never_claimed() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "*.env\n").unwrap();
write(&tmp.path().join("committed.env"), 40);
write(&tmp.path().join("scratch.env"), 40);
git(tmp.path(), &["add", "-f", "committed.env"]);
let hits = scan_with(&with_files(tmp.path()));
assert_eq!(hits.len(), 1, "{hits:?}");
assert_eq!(hits[0].path, tmp.path().join("scratch.env"));
}
#[test]
fn a_file_inside_a_claimed_directory_is_not_claimed_again() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "node_modules/\n").unwrap();
touch(&tmp.path().join("package.json"));
write(&tmp.path().join("node_modules/dep/.env"), 40);
write(&tmp.path().join("node_modules/dep/index.js"), OVER);
let hits = scan_with(&with_files(tmp.path()));
assert_eq!(hits.len(), 1, "{hits:?}");
assert_eq!(hits[0].path, tmp.path().join("node_modules"));
}
#[test]
fn a_file_a_pattern_only_matches_as_a_directory_is_left_alone() {
let tmp = TempDir::new().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join(".gitignore"), "scraps/\n").unwrap();
write(&tmp.path().join("scraps"), 40);
let hits = scan_with(&with_files(tmp.path()));
assert!(hits.is_empty(), "{hits:?}");
}
#[test]
fn the_report_says_whether_files_were_looked_for_at_all() {
let tmp = repo_with_ignored_files();
let quiet = walker(tmp.path()).run(|_| {});
assert!(!quiet.fallback.files_enabled);
assert_eq!(quiet.fallback.files, 0);
let asked = with_files(tmp.path()).run(|_| {});
assert!(asked.fallback.files_enabled);
assert_eq!(asked.fallback.files, 4);
assert_eq!(asked.fallback.hits, 4);
}