use std::path::{Path, PathBuf};
use cbh_command::{CacheSelection, LocalStorageSelection};
use crate::{Config, ConfigError};
pub const STORAGE_ENV_VAR: &str = "CARGO_BENCH_HISTORY_STORAGE";
pub const CACHE_ENV_VAR: &str = "CARGO_BENCH_HISTORY_CACHE";
#[must_use]
pub fn rebase(base: &Path, path: PathBuf) -> PathBuf {
if path.is_absolute() {
path
} else {
base.join(path)
}
}
#[must_use]
pub fn default_config_path() -> PathBuf {
PathBuf::from(".cargo").join("bench_history.toml")
}
#[must_use]
pub fn resolve_config_path(workspace_dir: &Path, explicit: Option<&Path>) -> PathBuf {
let path = explicit.map_or_else(default_config_path, Path::to_path_buf);
rebase(workspace_dir, path)
}
#[must_use]
pub fn resolve_repo(workspace_dir: &Path, repo: Option<&Path>) -> PathBuf {
repo.map_or_else(
|| workspace_dir.to_path_buf(),
|repo| rebase(workspace_dir, repo.to_path_buf()),
)
}
#[cfg_attr(test, mutants::skip)]
#[must_use]
pub fn storage_env() -> Option<String> {
std::env::var(STORAGE_ENV_VAR).ok()
}
pub fn resolve_local_path(
selection: Option<&LocalStorageSelection>,
env: Option<&str>,
) -> Result<Option<PathBuf>, ConfigError> {
match selection {
None => Ok(None),
Some(LocalStorageSelection::Path(path)) => Ok(Some(path.clone())),
Some(LocalStorageSelection::FromEnv) => match env {
Some(value) if !value.is_empty() => Ok(Some(PathBuf::from(value))),
_ => Err(ConfigError::new(format!(
"--local was given without a path and {STORAGE_ENV_VAR} is unset or empty"
))),
},
}
}
#[cfg_attr(test, mutants::skip)]
#[must_use]
pub fn cache_env() -> Option<String> {
std::env::var(CACHE_ENV_VAR).ok()
}
pub fn resolve_cache_path(
selection: Option<&CacheSelection>,
env: Option<&str>,
) -> Result<Option<PathBuf>, ConfigError> {
match selection {
None => Ok(None),
Some(CacheSelection::Path(path)) => Ok(Some(path.clone())),
Some(CacheSelection::FromEnv) => match env {
Some(value) if !value.is_empty() => Ok(Some(PathBuf::from(value))),
_ => Err(ConfigError::new(format!(
"--cache was given without a path and {CACHE_ENV_VAR} is unset or empty"
))),
},
}
}
#[must_use]
pub fn resolve_project_id(config: &Config, workspace_dir: &Path) -> String {
if let Some(id) = &config.project.id {
return id.clone();
}
workspace_dir
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("project")
.to_owned()
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::parse_config;
fn config_with(extra: &str) -> Config {
let text = format!("[storage.azure]\naccount = \"a\"\ncontainer = \"c\"\n\n{extra}");
parse_config(&text).unwrap()
}
#[test]
fn rebase_joins_relative_paths_and_keeps_absolute_ones() {
let base = Path::new("/work/folo");
assert_eq!(
rebase(base, PathBuf::from("store")),
PathBuf::from("/work/folo/store")
);
let absolute = if cfg!(windows) {
PathBuf::from(r"C:\elsewhere\store")
} else {
PathBuf::from("/elsewhere/store")
};
assert_eq!(rebase(base, absolute.clone()), absolute);
}
#[test]
fn resolve_local_path_returns_none_when_not_selected() {
assert_eq!(resolve_local_path(None, Some("/env/store")).unwrap(), None);
}
#[test]
fn resolve_local_path_uses_explicit_path() {
let selection = LocalStorageSelection::Path(PathBuf::from("./store"));
assert_eq!(
resolve_local_path(Some(&selection), None).unwrap(),
Some(PathBuf::from("./store"))
);
}
#[test]
fn resolve_local_path_reads_env_for_bare_local() {
assert_eq!(
resolve_local_path(Some(&LocalStorageSelection::FromEnv), Some("/env/store")).unwrap(),
Some(PathBuf::from("/env/store"))
);
}
#[test]
fn resolve_local_path_errors_when_env_unset() {
let error = resolve_local_path(Some(&LocalStorageSelection::FromEnv), None).unwrap_err();
let message = error.to_string();
assert!(message.contains(STORAGE_ENV_VAR), "{message}");
}
#[test]
fn resolve_local_path_treats_empty_env_as_unset() {
let error =
resolve_local_path(Some(&LocalStorageSelection::FromEnv), Some("")).unwrap_err();
assert!(error.to_string().contains(STORAGE_ENV_VAR), "{error}");
}
#[test]
fn resolve_cache_path_returns_none_when_not_selected() {
assert_eq!(resolve_cache_path(None, Some("/env/cache")).unwrap(), None);
}
#[test]
fn resolve_cache_path_uses_explicit_path() {
let selection = CacheSelection::Path(PathBuf::from("./cache"));
assert_eq!(
resolve_cache_path(Some(&selection), None).unwrap(),
Some(PathBuf::from("./cache"))
);
}
#[test]
fn resolve_cache_path_reads_env_for_bare_cache() {
assert_eq!(
resolve_cache_path(Some(&CacheSelection::FromEnv), Some("/env/cache")).unwrap(),
Some(PathBuf::from("/env/cache"))
);
}
#[test]
fn resolve_cache_path_errors_when_env_unset() {
let error = resolve_cache_path(Some(&CacheSelection::FromEnv), None).unwrap_err();
let message = error.to_string();
assert!(message.contains(CACHE_ENV_VAR), "{message}");
}
#[test]
fn resolve_cache_path_treats_empty_env_as_unset() {
let error = resolve_cache_path(Some(&CacheSelection::FromEnv), Some("")).unwrap_err();
assert!(error.to_string().contains(CACHE_ENV_VAR), "{error}");
}
#[test]
fn default_config_path_is_under_dot_cargo() {
assert_eq!(
default_config_path(),
PathBuf::from(".cargo").join("bench_history.toml")
);
}
#[test]
fn resolve_project_id_prefers_explicit_then_directory_name() {
let explicit = config_with("[project]\nid = \"explicit\"\n");
assert_eq!(
resolve_project_id(&explicit, Path::new("/work/folo")),
"explicit"
);
let implicit = config_with("");
assert_eq!(
resolve_project_id(&implicit, Path::new("/work/folo")),
"folo"
);
}
#[test]
fn resolve_project_id_falls_back_when_directory_name_is_unavailable() {
let config = config_with("");
assert_eq!(resolve_project_id(&config, Path::new("/")), "project");
}
#[test]
fn resolve_config_path_uses_the_default_under_the_workspace() {
let base = Path::new("/work/folo");
assert_eq!(
resolve_config_path(base, None),
base.join(".cargo").join("bench_history.toml")
);
assert_eq!(
resolve_config_path(base, Some(Path::new("custom.toml"))),
base.join("custom.toml")
);
}
#[test]
fn resolve_repo_defaults_to_the_workspace_directory() {
let base = Path::new("/work/folo");
assert_eq!(resolve_repo(base, None), base.to_path_buf());
assert_eq!(
resolve_repo(base, Some(Path::new("nested"))),
base.join("nested")
);
}
}