use std::path::{Path, PathBuf};
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;
fn cli(env_dir: &Path) -> Command {
let mut cmd = Command::cargo_bin("bca").unwrap();
cmd.env("HOME", env_dir)
.env("XDG_CONFIG_HOME", env_dir)
.env("GIT_CONFIG_GLOBAL", "/dev/null");
cmd
}
fn make_tree(dir: &Path) -> (PathBuf, PathBuf, PathBuf) {
let src = dir.join("src");
std::fs::create_dir_all(&src).unwrap();
let keep = src.join("keep.py");
let drop_a = src.join("drop_a.py");
let drop_b = src.join("drop_b.py");
std::fs::write(&keep, "def k(): return 1\n").unwrap();
std::fs::write(&drop_a, "def a(): return 2\n").unwrap();
std::fs::write(&drop_b, "def b(): return 3\n").unwrap();
(keep, drop_a, drop_b)
}
fn json_files(dir: &Path) -> Vec<String> {
fn visit(dir: &Path, found: &mut Vec<String>) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir() {
visit(&p, found);
} else if p.extension().and_then(|e| e.to_str()) == Some("json") {
found.push(p.file_name().unwrap().to_string_lossy().into_owned());
}
}
}
}
let mut found = Vec::new();
visit(dir, &mut found);
found.sort();
found
}
#[test]
fn exclude_from_file_drops_listed_patterns() {
let dir = TempDir::new().unwrap();
let (_keep, _drop_a, _drop_b) = make_tree(dir.path());
let bcaignore = dir.path().join(".bcaignore");
std::fs::write(&bcaignore, "**/drop_*.py\n").unwrap();
let out = dir.path().join("out");
std::fs::create_dir(&out).unwrap();
cli(dir.path())
.args([
"--paths",
dir.path().join("src").to_str().unwrap(),
"--exclude-from",
bcaignore.to_str().unwrap(),
"metrics",
"-O",
"json",
"-o",
out.to_str().unwrap(),
])
.assert()
.success();
let names = json_files(&out);
assert_eq!(
names,
vec!["keep.py.json".to_string()],
"exclude-from should drop both drop_a.py and drop_b.py"
);
}
#[test]
fn exclude_from_unions_with_exclude_flag() {
let dir = TempDir::new().unwrap();
let _ = make_tree(dir.path());
let bcaignore = dir.path().join(".bcaignore");
std::fs::write(&bcaignore, "**/drop_a.py\n").unwrap();
let out = dir.path().join("out");
std::fs::create_dir(&out).unwrap();
cli(dir.path())
.args([
"--paths",
dir.path().join("src").to_str().unwrap(),
"--exclude-from",
bcaignore.to_str().unwrap(),
"--exclude=**/drop_b.py",
"metrics",
"-O",
"json",
"-o",
out.to_str().unwrap(),
])
.assert()
.success();
let names = json_files(&out);
assert_eq!(
names,
vec!["keep.py.json".to_string()],
"--exclude and --exclude-from patterns should union into one deny-set"
);
}
#[test]
fn exclude_from_skips_blank_and_comment_lines() {
let dir = TempDir::new().unwrap();
let _ = make_tree(dir.path());
let bcaignore = dir.path().join(".bcaignore");
std::fs::write(
&bcaignore,
"\
# unclosed [bracket — malformed glob, must be skipped
**/drop_a.py
# indented comment, must be skipped
\t
**/drop_b.py
",
)
.unwrap();
let out = dir.path().join("out");
std::fs::create_dir(&out).unwrap();
cli(dir.path())
.args([
"--paths",
dir.path().join("src").to_str().unwrap(),
"--exclude-from",
bcaignore.to_str().unwrap(),
"metrics",
"-O",
"json",
"-o",
out.to_str().unwrap(),
])
.assert()
.success();
let names = json_files(&out);
assert_eq!(
names,
vec!["keep.py.json".to_string()],
"blank and `#`-comment lines must not be parsed as glob patterns"
);
}
#[test]
fn exclude_from_missing_file_dies_with_path_in_message() {
let dir = TempDir::new().unwrap();
let _ = make_tree(dir.path());
let missing = dir.path().join("does-not-exist.bcaignore");
cli(dir.path())
.args([
"--paths",
dir.path().join("src").to_str().unwrap(),
"--exclude-from",
missing.to_str().unwrap(),
"metrics",
"-O",
"json",
])
.assert()
.code(1)
.stderr(
predicate::str::contains("--exclude-from")
.and(predicate::str::contains("does-not-exist.bcaignore")),
);
}
#[test]
fn exclude_from_invalid_glob_in_file_dies_like_exclude_flag() {
let dir = TempDir::new().unwrap();
let _ = make_tree(dir.path());
let bcaignore = dir.path().join(".bcaignore");
std::fs::write(&bcaignore, "[\n").unwrap();
cli(dir.path())
.args([
"--paths",
dir.path().join("src").to_str().unwrap(),
"--exclude-from",
bcaignore.to_str().unwrap(),
"metrics",
"-O",
"json",
])
.assert()
.code(1)
.stderr(predicate::str::contains("invalid glob pattern"));
}
#[test]
fn exclude_from_stdin_reads_patterns() {
let dir = TempDir::new().unwrap();
let _ = make_tree(dir.path());
let out = dir.path().join("out");
std::fs::create_dir(&out).unwrap();
let stdin = "# piped via stdin\n**/drop_a.py\n\n**/drop_b.py\n";
cli(dir.path())
.args([
"--paths",
dir.path().join("src").to_str().unwrap(),
"--exclude-from",
"-",
"metrics",
"-O",
"json",
"-o",
out.to_str().unwrap(),
])
.write_stdin(stdin)
.assert()
.success();
let names = json_files(&out);
assert_eq!(
names,
vec!["keep.py.json".to_string()],
"--exclude-from - should consume patterns from stdin the same way it consumes them from a file"
);
}
#[test]
fn exclude_from_empty_file_leaves_inline_excludes_intact() {
let dir = TempDir::new().unwrap();
let _ = make_tree(dir.path());
let bcaignore = dir.path().join(".bcaignore");
std::fs::write(&bcaignore, "").unwrap();
let out = dir.path().join("out");
std::fs::create_dir(&out).unwrap();
cli(dir.path())
.args([
"--paths",
dir.path().join("src").to_str().unwrap(),
"--exclude-from",
bcaignore.to_str().unwrap(),
"--exclude=**/drop_a.py",
"metrics",
"-O",
"json",
"-o",
out.to_str().unwrap(),
])
.assert()
.success();
let names = json_files(&out);
assert_eq!(
names,
vec!["drop_b.py.json".to_string(), "keep.py.json".to_string()],
"empty .bcaignore must not perturb inline `--exclude` semantics"
);
}
#[test]
fn exclude_from_strips_utf8_bom_on_first_line() {
let dir = TempDir::new().unwrap();
let _ = make_tree(dir.path());
let bcaignore = dir.path().join(".bcaignore");
let mut bytes: Vec<u8> = vec![0xEF, 0xBB, 0xBF];
bytes.extend_from_slice(b"# unclosed [bracket - malformed if not skipped\n**/drop_a.py\n");
std::fs::write(&bcaignore, bytes).unwrap();
let out = dir.path().join("out");
std::fs::create_dir(&out).unwrap();
cli(dir.path())
.args([
"--paths",
dir.path().join("src").to_str().unwrap(),
"--exclude-from",
bcaignore.to_str().unwrap(),
"metrics",
"-O",
"json",
"-o",
out.to_str().unwrap(),
])
.assert()
.success();
let names = json_files(&out);
assert_eq!(
names,
vec!["drop_b.py.json".to_string(), "keep.py.json".to_string()],
"the BOM must be stripped so the `#`-comment is recognized and skipped, leaving only `**/drop_a.py` as an active exclude"
);
}