pub mod migration;
use std::ffi::OsString;
use std::path::PathBuf;
pub const TEST_HOME_VAR: &str = "SCOPE_TEST_HOME";
pub const HOME_VAR: &str = "SCOPE_BCA_HOME";
pub const HOME_DIR_NAME: &str = ".scope-bca";
pub fn home() -> PathBuf {
#[cfg(any(test, feature = "test-support"))]
return sandbox().join(HOME_DIR_NAME);
#[cfg(not(any(test, feature = "test-support")))]
home_from(std::env::var_os(HOME_VAR), user_home())
}
pub fn home_from(override_root: Option<OsString>, user_home: Option<PathBuf>) -> PathBuf {
match override_root.filter(|r| !r.is_empty()) {
Some(root) => PathBuf::from(root),
None => user_home
.map(|h| h.join(HOME_DIR_NAME))
.unwrap_or_else(|| PathBuf::from(HOME_DIR_NAME)),
}
}
pub fn config_file() -> PathBuf {
home().join("config.yaml")
}
pub fn venues_dir() -> PathBuf {
home().join("venues")
}
pub fn watchlists_dir() -> PathBuf {
home().join("watchlists")
}
pub fn data_dir() -> PathBuf {
home().join("data")
}
pub fn state_dir() -> PathBuf {
home().join("state")
}
#[allow(clippy::disallowed_methods)]
pub fn user_home() -> Option<PathBuf> {
#[cfg(any(test, feature = "test-support"))]
return Some(sandbox());
#[cfg(not(any(test, feature = "test-support")))]
dirs::home_dir()
}
#[allow(clippy::disallowed_methods)]
pub(crate) fn legacy_config_dir() -> Option<PathBuf> {
#[cfg(any(test, feature = "test-support"))]
return Some(sandbox().join("legacy-config"));
#[cfg(not(any(test, feature = "test-support")))]
dirs::config_dir()
}
#[allow(clippy::disallowed_methods)]
pub(crate) fn legacy_data_dir() -> Option<PathBuf> {
#[cfg(any(test, feature = "test-support"))]
return Some(sandbox().join("legacy-data"));
#[cfg(not(any(test, feature = "test-support")))]
dirs::data_dir()
}
#[cfg(any(test, feature = "test-support"))]
fn sandbox() -> PathBuf {
static FRESH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
if let Some(dir) = std::env::var_os(TEST_HOME_VAR) {
return PathBuf::from(dir);
}
FRESH
.get_or_init(|| {
let dir = std::env::temp_dir().join(format!("scope-test-home-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
seed_catalog(&dir.join(HOME_DIR_NAME).join("venues").join("catalog"));
dir
})
.clone()
}
#[cfg(any(test, feature = "test-support"))]
fn seed_catalog(dest: &std::path::Path) {
let repo = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("venues");
let Ok(entries) = std::fs::read_dir(&repo) else {
return;
};
if std::fs::create_dir_all(dest).is_err() {
return;
}
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
let _ = std::fs::copy(&path, dest.join(entry.file_name()));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsString;
#[test]
fn test_test_builds_never_resolve_the_real_home() {
let temp = std::env::temp_dir();
let mut all = vec![home(), config_file(), venues_dir(), data_dir(), state_dir()];
all.extend(user_home());
all.extend(legacy_config_dir());
all.extend(legacy_data_dir());
for dir in all {
assert!(
dir.starts_with(&temp),
"{} is outside the sandbox",
dir.display()
);
}
}
#[test]
fn test_the_layout_is_under_one_home() {
let root = home();
assert_eq!(config_file(), root.join("config.yaml"));
assert_eq!(venues_dir(), root.join("venues"));
assert_eq!(data_dir(), root.join("data"));
assert_eq!(state_dir(), root.join("state"));
}
#[test]
fn test_home_from_uses_the_override_else_the_user_home() {
let user = Some(PathBuf::from("/u/jon"));
assert_eq!(
home_from(None, user.clone()),
PathBuf::from("/u/jon/.scope-bca")
);
assert_eq!(
home_from(Some(OsString::from("/profiles/two")), user),
PathBuf::from("/profiles/two")
);
}
#[test]
fn test_home_from_ignores_an_empty_override() {
let user = Some(PathBuf::from("/u/jon"));
assert_eq!(
home_from(Some(OsString::new()), user),
PathBuf::from("/u/jon/.scope-bca")
);
}
#[test]
fn test_home_from_without_a_user_home_is_relative() {
assert_eq!(home_from(None, None), PathBuf::from(HOME_DIR_NAME));
}
}