use super::path_utils::{HomeSource, posix_home_from, windows_home_from};
use crate::stdlib::config_types::HomeDirectory;
use rstest::rstest;
fn as_pair(resolved: Option<&HomeSource>) -> Option<(&str, &str)> {
resolved.map(|(home, source)| (home.as_str(), *source))
}
mod expanduser_behaviour {
use super::super::path_utils::expanduser;
use super::HomeDirectory;
use crate::test_tracing_capture::with_test_subscriber;
use rstest::rstest;
use tracing_subscriber::filter::LevelFilter;
#[rstest]
#[case::tilde_alone("~", "/home/a")]
#[case::tilde_with_path("~/notes", "/home/a/notes")]
fn expands_against_the_resolved_home(#[case] raw: &str, #[case] expected: &str) {
let home = HomeDirectory::Explicit("/home/a".to_owned());
let expanded = expanduser(raw, &home, |_| None).expect("expansion should succeed");
assert_eq!(expanded, expected);
}
#[test]
fn ambient_resolves_through_the_injected_reader() {
let read_env = |key: &str| (key == "HOME").then(|| "/injected/home".to_owned());
let expanded = expanduser("~/x", &HomeDirectory::Ambient, read_env)
.expect("the injected reader should supply the home");
assert_eq!(expanded, "/injected/home/x");
}
#[test]
fn ambient_with_an_empty_reader_is_an_error() {
let error = expanduser("~/x", &HomeDirectory::Ambient, |_| None)
.expect_err("an empty reader should leave no home");
assert_eq!(error.kind(), minijinja::ErrorKind::InvalidOperation);
}
fn events_for(
raw: &str,
home: &HomeDirectory,
read_env: impl Fn(&str) -> Option<String>,
) -> Vec<String> {
with_test_subscriber(LevelFilter::DEBUG, |captured| {
drop(expanduser(raw, home, read_env));
captured.snapshot()
})
.into_iter()
.filter(|event| event.contains("stdlib.expanduser.home"))
.collect()
}
#[test]
fn resolution_reports_its_bounded_source() {
let read_env = |key: &str| (key == "USERPROFILE").then(|| "/injected/home".to_owned());
let events = events_for("~/x", &HomeDirectory::Ambient, read_env);
let event = events.first().expect("one home-resolution event");
assert!(
event.contains("source=\"userprofile\"") && event.contains("found=true"),
"the event should name the rung and the outcome: {event}"
);
assert!(
!events.iter().any(|other| other.contains("/injected/home")),
"no event may carry the resolved home: {events:?}"
);
}
#[test]
fn an_unresolvable_home_reports_a_bounded_outcome() {
let events = events_for("~/x", &HomeDirectory::Missing, |_| None);
assert!(
events.iter().any(|event| {
event.contains("outcome=\"home_unavailable\"")
&& event.contains("source=\"missing\"")
}),
"the failure event should carry the bounded outcome: {events:?}"
);
}
#[test]
fn a_non_tilde_path_passes_through() {
let expanded = expanduser("/etc/hosts", &HomeDirectory::Missing, |_| None)
.expect("a non-tilde path should pass through");
assert_eq!(expanded, "/etc/hosts");
}
#[test]
fn named_user_forms_are_rejected() {
let error = expanduser("~alice/notes", &HomeDirectory::Missing, |_| None)
.expect_err("~alice should be rejected");
assert_eq!(error.kind(), minijinja::ErrorKind::InvalidOperation);
}
#[test]
fn a_missing_home_is_an_error() {
let error = expanduser("~/notes", &HomeDirectory::Missing, |_| None)
.expect_err("no home should be an error");
assert_eq!(error.kind(), minijinja::ErrorKind::InvalidOperation);
}
}
fn env_of<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
move |key| {
pairs
.iter()
.find(|(name, _)| *name == key)
.map(|(_, value)| (*value).to_owned())
}
}
#[rstest]
#[case::home_wins(&[("HOME", "/home/a"), ("USERPROFILE", "/users/b")], Some(("/home/a", "home")))]
#[case::falls_back_to_userprofile(&[("USERPROFILE", "/users/b")], Some(("/users/b", "userprofile")))]
#[case::nothing_set(&[], None)]
#[case::empty_home_is_passed_through(&[("HOME", "")], Some(("", "home")))]
#[case::empty_userprofile_is_passed_through(&[("USERPROFILE", "")], Some(("", "userprofile")))]
fn posix_ladder(#[case] pairs: &[(&str, &str)], #[case] expected: Option<(&str, &str)>) {
let resolved = posix_home_from(env_of(pairs));
assert_eq!(as_pair(resolved.as_ref()), expected);
}
#[rstest]
#[case::home_wins(
&[("HOME", "H:\\home"), ("USERPROFILE", "U:\\users"), ("HOMEDRIVE", "C:"), ("HOMEPATH", "\\me")],
Some(("H:\\home", "home"))
)]
#[case::userprofile_before_the_pair(
&[("USERPROFILE", "U:\\users"), ("HOMEDRIVE", "C:"), ("HOMEPATH", "\\me")],
Some(("U:\\users", "userprofile"))
)]
#[case::drive_and_path_pair(
&[("HOMEDRIVE", "C:"), ("HOMEPATH", "\\me")],
Some(("C:\\me", "drive_path"))
)]
#[case::homeshare_last(
&[("HOMESHARE", "\\\\server\\share")],
Some(("\\\\server\\share", "homeshare"))
)]
#[case::nothing_set(&[], None)]
fn windows_ladder(#[case] pairs: &[(&str, &str)], #[case] expected: Option<(&str, &str)>) {
let resolved = windows_home_from(env_of(pairs));
assert_eq!(as_pair(resolved.as_ref()), expected);
}
#[rstest]
#[case::falls_through_to_homeshare(
&[("HOMEDRIVE", "C:"), ("HOMEPATH", ""), ("HOMESHARE", "\\\\server\\share")],
Some(("\\\\server\\share", "homeshare"))
)]
#[case::no_homeshare_yields_none(&[("HOMEDRIVE", "C:"), ("HOMEPATH", "")], None)]
#[case::empty_homedrive_falls_through_to_homeshare(
&[("HOMEDRIVE", ""), ("HOMEPATH", "\\me"), ("HOMESHARE", "\\\\server\\share")],
Some(("\\\\server\\share", "homeshare"))
)]
#[case::empty_homedrive_without_homeshare_yields_none(
&[("HOMEDRIVE", ""), ("HOMEPATH", "\\me")],
None
)]
#[case::both_halves_empty_yields_none(&[("HOMEDRIVE", ""), ("HOMEPATH", "")], None)]
fn windows_incomplete_drive_pair_is_treated_as_unset(
#[case] pairs: &[(&str, &str)],
#[case] expected: Option<(&str, &str)>,
) {
let resolved = windows_home_from(env_of(pairs));
assert_eq!(as_pair(resolved.as_ref()), expected);
}
#[test]
fn windows_homedrive_without_homepath_falls_through() {
let pairs = [("HOMEDRIVE", "C:")];
assert_eq!(windows_home_from(env_of(&pairs)), None);
}
mod properties {
use super::{posix_home_from, windows_home_from};
use proptest::option;
use proptest::prelude::*;
fn value() -> impl Strategy<Value = String> {
prop_oneof![
1 => Just(String::new()),
3 => "[a-z]{1,3}".prop_map(|s| s),
]
}
fn reader<'a>(
pairs: &'a [(&'a str, &'a Option<String>)],
) -> impl Fn(&str) -> Option<String> + 'a {
move |key| {
pairs
.iter()
.find(|(name, _)| *name == key)
.and_then(|(_, value)| (*value).clone())
}
}
proptest! {
#[test]
fn windows_ladder_matches_its_documented_precedence(
home in option::of(value()),
userprofile in option::of(value()),
homedrive in option::of(value()),
homepath in option::of(value()),
homeshare in option::of(value()),
) {
let expected = home.clone().map(|value| (value, "home"))
.or_else(|| userprofile.clone().map(|value| (value, "userprofile")))
.or_else(|| {
match (&homedrive, &homepath) {
(Some(drive), Some(path)) if !drive.is_empty() && !path.is_empty() => {
Some((format!("{drive}{path}"), "drive_path"))
}
_ => homeshare.clone().map(|value| (value, "homeshare")),
}
});
let pairs = [
("HOME", &home),
("USERPROFILE", &userprofile),
("HOMEDRIVE", &homedrive),
("HOMEPATH", &homepath),
("HOMESHARE", &homeshare),
];
prop_assert_eq!(windows_home_from(reader(&pairs)), expected);
}
#[test]
fn posix_ladder_reads_only_home_and_userprofile(
home in option::of(value()),
userprofile in option::of(value()),
homedrive in option::of(value()),
homepath in option::of(value()),
homeshare in option::of(value()),
) {
let expected = home.clone().map(|value| (value, "home"))
.or_else(|| userprofile.clone().map(|value| (value, "userprofile")));
let all = [
("HOME", &home),
("USERPROFILE", &userprofile),
("HOMEDRIVE", &homedrive),
("HOMEPATH", &homepath),
("HOMESHARE", &homeshare),
];
prop_assert_eq!(posix_home_from(reader(&all)), expected);
}
}
}