use std::{
cell::Cell,
collections::{BTreeMap, BTreeSet},
};
use proptest::{collection::vec, prelude::*, test_runner::TestRunner};
use super::{
CandidatePath,
PathKind,
PathProbe,
select_files,
tests::{FakeProbe, at, regular},
};
use crate::select::extensions::ExtensionFilter;
const POOL: [&str; 12] = [
"docs/guide.md",
"docs/deep/nested/notes.markdown",
"README.mdc",
"top.md",
"docs/UPPER.MD",
"src/lib.rs",
"Cargo.toml",
"assets/logo.png",
"Makefile",
".hidden",
".config/notes.md",
"docs/notes.mdx",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Verdict {
Regular,
Missing,
Symlink,
Other,
OutsideRoot,
}
fn verdict() -> impl Strategy<Value = Verdict> {
prop_oneof![
Just(Verdict::Regular),
Just(Verdict::Missing),
Just(Verdict::Symlink),
Just(Verdict::Other),
Just(Verdict::OutsideRoot),
]
}
fn candidate() -> impl Strategy<Value = CandidatePath> {
(0..POOL.len()).prop_map(|index| at(POOL[index]))
}
fn candidate_and_verdict() -> impl Strategy<Value = Vec<(CandidatePath, Verdict)>> {
vec((candidate(), verdict()), 0..=30)
}
fn kind_for(verdict: Verdict, path: &CandidatePath) -> PathKind {
match verdict {
Verdict::Regular => regular(path.as_str()),
Verdict::Missing => PathKind::Missing,
Verdict::Symlink => PathKind::Symlink,
Verdict::Other => PathKind::Other,
Verdict::OutsideRoot => PathKind::OutsideRoot,
}
}
fn verdicts_for(candidates: &[(CandidatePath, Verdict)]) -> BTreeMap<CandidatePath, Verdict> {
candidates.iter().cloned().collect()
}
fn probe_for(verdicts: &BTreeMap<CandidatePath, Verdict>) -> FakeProbe {
FakeProbe::new(
verdicts
.iter()
.map(|(path, verdict)| (path.clone(), kind_for(*verdict, path))),
)
}
#[test]
fn a_path_is_selected_exactly_when_it_matches_and_probes_as_a_regular_file() {
let filter = ExtensionFilter::default();
let mut runner = TestRunner::default();
let saw_empty = Cell::new(false);
let saw_non_empty = Cell::new(false);
let saw_probe_only_exclusion = Cell::new(false);
runner
.run(&candidate_and_verdict(), |candidates| {
let paths: Vec<CandidatePath> =
candidates.iter().map(|(path, _)| path.clone()).collect();
let verdicts = verdicts_for(&candidates);
let probe = probe_for(&verdicts);
let selected = select_files(&paths, &filter, &probe)
.expect("the fake probe answers every candidate it is handed");
for path in &selected {
prop_assert!(
filter.matches(path.as_str()),
"{} does not carry a configured extension",
path
);
prop_assert!(
matches!(probe.probe(path), Ok(PathKind::RegularFile(_))),
"{} was selected without a regular-file verdict",
path
);
}
let expected: BTreeSet<&CandidatePath> = verdicts
.iter()
.filter(|(path, verdict)| {
**verdict == Verdict::Regular && filter.matches(path.as_str())
})
.map(|(path, _)| path)
.collect();
let actual: BTreeSet<&CandidatePath> = selected.iter().collect();
prop_assert_eq!(&actual, &expected);
prop_assert_eq!(selected.len(), actual.len(), "a path was selected twice");
if selected.is_empty() {
saw_empty.set(true);
} else {
saw_non_empty.set(true);
}
if verdicts.iter().any(|(path, verdict)| {
*verdict != Verdict::Regular && filter.matches(path.as_str())
}) {
saw_probe_only_exclusion.set(true);
}
Ok(())
})
.expect("matching extension plus a regular-file verdict decide selection");
assert!(
saw_empty.get() && saw_non_empty.get() && saw_probe_only_exclusion.get(),
concat!(
"the run must observe an empty selection ({}), a non-empty one ({}), and a ",
"matching-extension candidate excluded by its verdict alone ({})"
),
saw_empty.get(),
saw_non_empty.get(),
saw_probe_only_exclusion.get()
);
}
fn permute(items: &[CandidatePath], keys: &[u8]) -> Vec<CandidatePath> {
let mut order: Vec<usize> = (0..items.len()).collect();
order.sort_by_key(|index| (keys[*index], *index));
order
.into_iter()
.map(|index| items[index].clone())
.collect()
}
fn candidates_and_keys() -> impl Strategy<Value = (Vec<CandidatePath>, Vec<u8>)> {
vec(candidate(), 0..=12).prop_flat_map(|paths| {
let shared = paths.clone();
vec(any::<u8>(), paths.len()).prop_map(move |keys| (shared.clone(), keys))
})
}
#[test]
fn selection_does_not_depend_on_the_order_the_listing_arrives_in() {
let filter = ExtensionFilter::default();
let mut runner = TestRunner::default();
let saw_a_different_order = Cell::new(false);
let saw_several = Cell::new(false);
runner
.run(&candidates_and_keys(), |(paths, keys)| {
let shuffled = permute(&paths, &keys);
if shuffled != paths {
saw_a_different_order.set(true);
}
let probe = FakeProbe::new(
paths
.iter()
.map(|path| (path.clone(), regular(path.as_str()))),
);
let in_listing_order = select_files(&paths, &filter, &probe)
.expect("the fake probe answers every candidate it is handed");
let in_shuffled_order = select_files(&shuffled, &filter, &probe)
.expect("the fake probe answers every candidate it is handed");
prop_assert_eq!(
&in_listing_order,
&in_shuffled_order,
"the selection must not depend on listing order"
);
if in_listing_order.len() >= 2 {
saw_several.set(true);
}
Ok(())
})
.expect("selection is a function of the candidate multiset");
assert!(
saw_a_different_order.get(),
"the generated permutation must differ from the listing, or the property is vacuous"
);
assert!(
saw_several.get(),
"at least one case must select two or more paths, or ordering carries no information"
);
}