use std::collections::BTreeMap;
use rstest::rstest;
use super::{CandidatePath, FileIdentity, PathKind, PathProbe, ProbeFailure, select_files};
use crate::select::extensions::ExtensionFilter;
pub(super) fn at(path: &str) -> CandidatePath { CandidatePath::new(path.to_owned()) }
pub(super) fn regular(key: &str) -> PathKind {
PathKind::RegularFile(FileIdentity::from_canonical_name(format!(
"/canonical/{key}"
)))
}
pub(super) struct FakeProbe(BTreeMap<CandidatePath, PathKind>);
impl FakeProbe {
pub(super) fn new(entries: impl IntoIterator<Item = (CandidatePath, PathKind)>) -> Self {
Self(entries.into_iter().collect())
}
}
impl PathProbe for FakeProbe {
fn probe(&self, candidate: &CandidatePath) -> Result<PathKind, ProbeFailure> {
Ok(self.0.get(candidate).cloned().unwrap_or(PathKind::Missing))
}
}
struct FixedProbe(PathKind);
impl PathProbe for FixedProbe {
fn probe(&self, _candidate: &CandidatePath) -> Result<PathKind, ProbeFailure> {
Ok(self.0.clone())
}
}
struct UnreadableProbe {
unreadable: &'static str,
}
impl PathProbe for UnreadableProbe {
fn probe(&self, candidate: &CandidatePath) -> Result<PathKind, ProbeFailure> {
if candidate.as_str() == self.unreadable {
Err(ProbeFailure::unreadable(
candidate.clone(),
"permission denied".to_owned(),
))
} else {
Ok(regular(candidate.as_str()))
}
}
}
fn selecting<P: PathProbe + ?Sized>(candidates: &[CandidatePath], probe: &P) -> Vec<CandidatePath> {
select_files(candidates, &ExtensionFilter::default(), probe)
.expect("the fixture's probe answers for every path it is asked about")
}
#[rstest]
#[case(
PathKind::RegularFile(FileIdentity::from_canonical_name("/canonical/guide.md".to_owned())),
true
)]
#[case(PathKind::Missing, false)]
#[case(PathKind::Symlink, false)]
#[case(PathKind::Other, false)]
#[case(PathKind::OutsideRoot, false)]
fn the_probe_verdict_alone_decides(#[case] verdict: PathKind, #[case] expected: bool) {
let candidates = [at("docs/guide.md")];
let selected = selecting(&candidates, &FixedProbe(verdict.clone()));
assert_eq!(
!selected.is_empty(),
expected,
"a candidate with a configured extension under {verdict:?}: selected {selected:?}"
);
}
#[test]
fn a_path_reported_once_per_merge_stage_is_selected_once() {
let paths = ["docs/guide.md", "docs/guide.md", "docs/guide.md"];
let probe = FakeProbe::new(paths.map(|path| (at(path), regular(path))));
let candidates = paths.map(at).to_vec();
assert_eq!(selecting(&candidates, &probe), vec![at("docs/guide.md")]);
}
#[test]
fn two_spellings_of_one_entry_collapse_to_the_first_path() {
let paths = ["Readme.md", "README.md"];
let probe = FakeProbe::new(paths.map(|path| (at(path), regular("readme.md"))));
assert_eq!(
probe.probe(&at(paths[0])).expect("the fake probe answers"),
probe.probe(&at(paths[1])).expect("the fake probe answers"),
"the two spellings must share one identity for this case to mean anything"
);
for candidates in [
paths.map(at).to_vec(),
paths.iter().rev().copied().map(at).collect::<Vec<_>>(),
] {
assert_eq!(
selecting(&candidates, &probe),
vec![at("README.md")],
"the lexicographically first spelling is the one retained"
);
}
}
#[test]
fn distinct_entries_are_all_selected_sorted_byte_wise() {
let paths = ["notes.md", "docs/guide.md", "README.mdc"];
let probe = FakeProbe::new(paths.iter().map(|path| (at(path), regular(path))));
let candidates = paths.map(at).to_vec();
assert_eq!(
selecting(&candidates, &probe),
vec![at("README.mdc"), at("docs/guide.md"), at("notes.md")],
"byte-wise: uppercase sorts before lowercase, and a prefix before its extension"
);
}
#[test]
fn a_candidate_the_probe_cannot_read_fails_the_selection() {
let candidates = [at("docs/guide.md"), at("docs/notes.md"), at("docs/api.md")];
let error = select_files(
&candidates,
&ExtensionFilter::default(),
&UnreadableProbe {
unreadable: "docs/notes.md",
},
)
.expect_err("an unreadable candidate must not be silently skipped");
assert_eq!(error.path, at("docs/notes.md"));
assert_eq!(error.detail, "permission denied");
}