use super::env::{DEFAULT_PATHEXT, parse_pathext};
use rstest::rstest;
use std::ffi::OsStr;
fn parse(raw: &str) -> Vec<String> {
parse_pathext(Some(OsStr::new(raw)))
}
fn expected_default_pathext() -> Vec<String> {
[
".com", ".exe", ".bat", ".cmd", ".vbs", ".vbe", ".js", ".jse", ".wsf", ".wsh", ".msc",
]
.iter()
.copied()
.map(String::from)
.collect()
}
#[test]
fn unset_pathext_yields_the_default_list() {
let expected = expected_default_pathext();
assert_eq!(parse_pathext(None), expected);
let constant: Vec<String> = DEFAULT_PATHEXT.iter().copied().map(String::from).collect();
assert_eq!(constant, expected);
}
#[test]
fn the_default_list_is_well_formed() {
assert!(
!DEFAULT_PATHEXT.is_empty(),
"an empty default disables which"
);
for ext in DEFAULT_PATHEXT {
assert!(ext.starts_with('.'), "{ext} should carry a leading dot");
assert_eq!(*ext, ext.to_ascii_lowercase(), "{ext} should be lowercase");
}
for required in [".com", ".exe", ".bat", ".cmd"] {
assert!(
DEFAULT_PATHEXT.contains(&required),
"the default list should include {required}"
);
}
}
#[rstest]
#[case::empty("")]
#[case::separators_only(";;;")]
#[case::whitespace_only(" ; ; ")]
fn valueless_pathext_falls_back_to_the_default_list(#[case] raw: &str) {
assert_eq!(parse(raw), DEFAULT_PATHEXT, "{raw:?} should fall back");
}
#[test]
fn extensions_are_lowercased() {
assert_eq!(parse(".COM;.EXE"), vec![".com", ".exe"]);
}
#[test]
fn missing_leading_dots_are_inserted() {
assert_eq!(parse("COM;EXE"), vec![".com", ".exe"]);
}
#[test]
fn surrounding_whitespace_is_trimmed() {
assert_eq!(parse(" .BAT ;\t.CMD\t"), vec![".bat", ".cmd"]);
}
#[test]
fn duplicates_collapse_after_normalization() {
assert_eq!(parse("COM;.com;.COM; com "), vec![".com"]);
}
#[test]
fn declaration_order_is_preserved() {
assert_eq!(parse(".exe;.bat;.com"), vec![".exe", ".bat", ".com"]);
}
#[test]
fn entries_that_normalize_to_nothing_are_skipped() {
assert_eq!(parse(".exe;; ;.bat"), vec![".exe", ".bat"]);
}
mod properties {
use super::{DEFAULT_PATHEXT, parse};
use proptest::collection::vec;
use proptest::prelude::*;
fn segment() -> impl Strategy<Value = String> {
prop_oneof![
3 => ("[ \t]*", prop::bool::ANY, "com|exe|bat|Com|EXE|Bat", "[ \t]*")
.prop_map(|(lead, with_dot, stem, trail)| {
let dot = if with_dot { "." } else { "" };
format!("{lead}{dot}{stem}{trail}")
}),
1 => "[ \t]*".prop_map(|s: String| s),
]
}
fn raw_value() -> impl Strategy<Value = String> {
vec(segment(), 0..8).prop_map(|parts| parts.join(";"))
}
proptest! {
#[test]
fn entries_are_normalized(raw in raw_value()) {
for ext in parse(&raw) {
prop_assert!(ext.starts_with('.'), "missing dot: {ext:?}");
prop_assert!(ext.len() > 1, "nothing after the dot: {ext:?}");
prop_assert_eq!(&ext, &ext.to_ascii_lowercase());
}
}
#[test]
fn entries_are_unique(raw in raw_value()) {
let parsed = parse(&raw);
let mut seen = std::collections::HashSet::new();
for ext in &parsed {
prop_assert!(
seen.insert(ext.to_ascii_lowercase()),
"{ext:?} repeats an earlier entry in {parsed:?} bar case"
);
}
}
#[test]
fn parsing_is_idempotent(raw in raw_value()) {
let once = parse(&raw);
let twice = parse(&once.join(";"));
prop_assert_eq!(once, twice);
}
#[test]
fn unusable_input_falls_back(parts in vec("[ \t]*", 0..6)) {
prop_assert_eq!(parse(&parts.join(";")), DEFAULT_PATHEXT.to_vec());
}
#[test]
fn first_occurrence_fixes_order(stems in vec("[a-z][a-z0-9]{0,3}", 1..5)) {
let mut expected: Vec<String> = Vec::new();
for stem in &stems {
let ext = format!(".{stem}");
if !expected.contains(&ext) {
expected.push(ext);
}
}
let raw = stems
.iter()
.map(|s| format!(".{s}"))
.chain(stems.iter().map(|s| s.to_ascii_uppercase()))
.collect::<Vec<_>>()
.join(";");
prop_assert_eq!(parse(&raw), expected);
}
}
}