use crate::cli::Cli;
use crate::config::ALL_TOML_CONFIG_FILES;
use crate::duration;
use crate::file::FindUp;
use crate::platform::Platform;
use crate::{dirs, env, file};
#[allow(unused_imports)]
use confique::env::parse::{list_by_colon, list_by_comma};
use confique::{Config, Layer};
use eyre::{Result, bail};
use indexmap::{IndexMap, indexmap};
use itertools::Itertools;
use serde::ser::Error;
use serde::{Deserialize, Deserializer, Serializer};
use serde_derive::Serialize;
use std::env::consts::{ARCH, OS};
use std::fmt::{Debug, Display, Formatter};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::LazyLock as Lazy;
use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;
use std::{
collections::{BTreeSet, HashSet},
sync::atomic::Ordering,
};
use url::Url;
include!(concat!(env!("OUT_DIR"), "/settings.rs"));
pub enum SettingsType {
Bool,
String,
Integer,
Duration,
Path,
Url,
ListString,
ListPath,
SetString,
IndexMap,
BoolOrString,
}
pub struct SettingsMeta {
pub type_: SettingsType,
pub description: &'static str,
pub deprecated: Option<&'static str>,
pub deprecated_warn_at: Option<&'static str>,
pub deprecated_remove_at: Option<&'static str>,
}
#[derive(
Debug,
Clone,
Copy,
Serialize,
Deserialize,
Default,
strum::EnumString,
strum::Display,
PartialEq,
Eq,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum SettingsStatusMissingTools {
Never,
#[default]
IfOtherVersionsInstalled,
Always,
}
#[derive(
Debug,
Clone,
Copy,
Serialize,
Deserialize,
Default,
strum::EnumString,
strum::Display,
PartialEq,
Eq,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum NpmPackageManager {
#[default]
Npm,
Bun,
Pnpm,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PythonUvVenvAuto {
#[default]
Off,
Source,
CreateSource,
LegacyTrue,
}
impl PythonUvVenvAuto {
pub fn should_source(self) -> bool {
matches!(self, Self::Source | Self::CreateSource | Self::LegacyTrue)
}
pub fn should_create(self) -> bool {
matches!(self, Self::CreateSource | Self::LegacyTrue)
}
pub fn is_legacy_true(self) -> bool {
matches!(self, Self::LegacyTrue)
}
}
impl<'de> Deserialize<'de> for PythonUvVenvAuto {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
use std::fmt;
struct PythonUvVenvAutoVisitor;
impl<'de> Visitor<'de> for PythonUvVenvAutoVisitor {
type Value = PythonUvVenvAuto;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a boolean, \"source\", or \"create|source\"")
}
fn visit_bool<E>(self, value: bool) -> Result<PythonUvVenvAuto, E>
where
E: de::Error,
{
if value {
deprecated_at!(
"2026.7.0",
"2027.7.0",
"python.uv_venv_auto.true",
"python.uv_venv_auto=true is deprecated. Use python.uv_venv_auto=\"create|source\" or \"source\" instead."
);
}
Ok(if value {
PythonUvVenvAuto::LegacyTrue
} else {
PythonUvVenvAuto::Off
})
}
fn visit_str<E>(self, value: &str) -> Result<PythonUvVenvAuto, E>
where
E: de::Error,
{
let normalized = value.trim().to_ascii_lowercase();
match normalized.as_str() {
"source" => Ok(PythonUvVenvAuto::Source),
"create|source" => Ok(PythonUvVenvAuto::CreateSource),
"true" | "yes" | "1" => self.visit_bool(true),
"false" | "no" | "0" => self.visit_bool(false),
_ => Err(E::invalid_value(de::Unexpected::Str(value), &self)),
}
}
fn visit_string<E>(self, value: String) -> Result<PythonUvVenvAuto, E>
where
E: de::Error,
{
self.visit_str(&value)
}
}
deserializer.deserialize_any(PythonUvVenvAutoVisitor)
}
}
impl serde::Serialize for PythonUvVenvAuto {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
PythonUvVenvAuto::Off => serializer.serialize_bool(false),
PythonUvVenvAuto::LegacyTrue => serializer.serialize_bool(true),
PythonUvVenvAuto::Source => serializer.serialize_str("source"),
PythonUvVenvAuto::CreateSource => serializer.serialize_str("create|source"),
}
}
}
pub type SettingsPartial = <Settings as Config>::Layer;
static BASE_SETTINGS: RwLock<Option<Arc<Settings>>> = RwLock::new(None);
static CLI_SETTINGS: Mutex<Option<SettingsPartial>> = Mutex::new(None);
static DEFAULT_SETTINGS: Lazy<SettingsPartial> = Lazy::new(|| {
let mut s = SettingsPartial::empty();
s.python.default_packages_file = Some(env::HOME.join(".default-python-packages"));
if let Some("alpine" | "nixos") = env::LINUX_DISTRO.as_ref().map(|s| s.as_str())
&& !cfg!(test)
{
s.all_compile = Some(true);
}
s
});
pub fn is_loaded() -> bool {
BASE_SETTINGS.read().unwrap().is_some()
}
#[derive(Serialize, Deserialize)]
pub struct SettingsFile {
#[serde(default)]
pub settings: SettingsPartial,
}
fn warn_deprecated(key: &str) {
if let Some(meta) = SETTINGS_META.get(key)
&& let (Some(msg), Some(warn_at), Some(remove_at)) = (
meta.deprecated,
meta.deprecated_warn_at,
meta.deprecated_remove_at,
)
{
use versions::Versioning;
let warn_version = Versioning::new(warn_at).unwrap();
let remove_version = Versioning::new(remove_at).unwrap();
debug_assert!(
*crate::cli::version::V < remove_version,
"Deprecated setting [{key}] should have been removed in {remove_at}. Please remove this deprecated setting.",
);
if *crate::cli::version::V >= warn_version {
let id = Box::leak(format!("setting.{key}").into_boxed_str());
if crate::output::DEPRECATED.lock().unwrap().insert(id) {
warn!(
"deprecated [setting.{key}]: {msg} This will be removed in mise {remove_at}."
);
}
}
}
}
impl Settings {
pub fn get() -> Arc<Self> {
Self::try_get().unwrap()
}
pub fn try_get() -> Result<Arc<Self>> {
if let Some(settings) = BASE_SETTINGS.read().unwrap().as_ref() {
return Ok(settings.clone());
}
time!("try_get");
let mut sb = Self::builder()
.preloaded(CLI_SETTINGS.lock().unwrap().clone().unwrap_or_default())
.env();
let mut settings = sb.load()?;
if let Some(mut cd) = settings.cd {
static ORIG_PATH: Lazy<std::io::Result<PathBuf>> = Lazy::new(env::current_dir);
if cd.is_relative() {
cd = ORIG_PATH.as_ref()?.join(cd);
}
env::set_current_dir(cd)?;
}
sb = Self::builder()
.preloaded(CLI_SETTINGS.lock().unwrap().clone().unwrap_or_default())
.env();
for file in Self::all_settings_files() {
sb = sb.preloaded(file);
}
sb = sb.preloaded(DEFAULT_SETTINGS.clone());
settings = sb.load()?;
if !settings.legacy_version_file {
settings.idiomatic_version_file = Some(false);
}
if settings.raw {
settings.jobs = 1;
}
if *env::NO_COLOR {
settings.color = false;
}
if settings.debug {
settings.log_level = "debug".to_string();
}
if settings.trace {
settings.log_level = "trace".to_string();
}
if settings.quiet {
settings.log_level = "error".to_string();
}
if settings.log_level == "trace" || settings.log_level == "debug" {
settings.verbose = true;
settings.debug = true;
if settings.log_level == "trace" {
settings.trace = true;
}
}
let args = env::args().collect_vec();
if settings.verbose && !(args.len() == 2 && args[1] == "-v") {
settings.quiet = false;
if settings.log_level != "trace" {
settings.log_level = "debug".to_string();
}
}
if !settings.color {
console::set_colors_enabled(false);
console::set_colors_enabled_stderr(false);
} else if *env::CLICOLOR_FORCE == Some(true) {
console::set_colors_enabled(true);
console::set_colors_enabled_stderr(true);
} else if *env::CLICOLOR == Some(false) {
console::set_colors_enabled(false);
console::set_colors_enabled_stderr(false);
} else if ci_info::is_ci() && !cfg!(test) {
console::set_colors_enabled_stderr(true);
}
if settings.ci {
settings.yes = true;
}
if settings.all_compile {
if settings.node.compile.is_none() {
settings.node.compile = Some(true);
}
if settings.python.compile.is_none() {
settings.python.compile = Some(true);
}
if settings.erlang.compile.is_none() {
settings.erlang.compile = Some(true);
}
if settings.ruby.compile.is_none() {
settings.ruby.compile = Some(true);
}
}
if settings.gpg_verify.is_some() {
settings.node.gpg_verify = settings.node.gpg_verify.or(settings.gpg_verify);
settings.swift.gpg_verify = settings.swift.gpg_verify.or(settings.gpg_verify);
}
settings.set_hidden_configs();
if cfg!(test) {
settings.experimental = true;
}
let settings = Arc::new(settings);
*BASE_SETTINGS.write().unwrap() = Some(settings.clone());
time!("try_get done");
trace!("Settings: {:#?}", settings);
Ok(settings)
}
fn set_hidden_configs(&mut self) {
if let Some(v) = self.task_disable_paths.take()
&& !v.is_empty()
{
warn_deprecated("task_disable_paths");
self.task.disable_paths.extend(v);
}
if let Some(v) = self.task_output.take() {
warn_deprecated("task_output");
self.task.output = Some(v);
}
if let Some(v) = self.task_remote_no_cache {
warn_deprecated("task_remote_no_cache");
self.task.remote_no_cache = Some(v);
}
if let Some(v) = self.task_run_auto_install {
warn_deprecated("task_run_auto_install");
self.task.run_auto_install = v;
}
if let Some(v) = self.task_show_full_cmd {
warn_deprecated("task_show_full_cmd");
self.task.show_full_cmd = v;
}
if let Some(v) = self.task_skip.take()
&& !v.is_empty()
{
warn_deprecated("task_skip");
self.task.skip.extend(v);
}
if let Some(v) = self.task_skip_depends {
warn_deprecated("task_skip_depends");
self.task.skip_depends = v;
}
if let Some(v) = self.task_timeout.take() {
warn_deprecated("task_timeout");
self.task.timeout = Some(v);
}
if let Some(v) = self.task_timings {
warn_deprecated("task_timings");
self.task.timings = Some(v);
}
if !self.auto_install {
self.exec_auto_install = false;
self.not_found_auto_install = false;
self.task.run_auto_install = false;
}
if let Some(go_default_packages_file) = &self.go_default_packages_file {
self.go.default_packages_file = go_default_packages_file.clone();
}
if let Some(go_download_mirror) = &self.go_download_mirror {
self.go.download_mirror = go_download_mirror.clone();
}
if let Some(go_repo) = &self.go_repo {
self.go.repo = go_repo.clone();
}
if let Some(go_set_gobin) = self.go_set_gobin {
self.go.set_gobin = Some(go_set_gobin);
}
if let Some(go_set_gopath) = self.go_set_gopath {
self.go.set_gopath = go_set_gopath;
}
if let Some(go_set_goroot) = self.go_set_goroot {
self.go.set_goroot = go_set_goroot;
}
if let Some(go_skip_checksum) = self.go_skip_checksum {
self.go.skip_checksum = go_skip_checksum;
}
if self.npm.bun {
self.npm.package_manager = NpmPackageManager::Bun;
}
}
pub fn add_cli_matches(cli: &Cli) {
let mut s = SettingsPartial::empty();
if *crate::env::IS_RUNNING_AS_SHIM {
Self::reset(Some(s));
return;
}
if cli.raw {
s.raw = Some(true);
}
if cli.locked {
s.locked = Some(true);
}
if let Some(cd) = &cli.cd {
s.cd = Some(cd.clone());
}
if cli.profile.is_some() {
s.env = cli.profile.clone();
}
if cli.env.is_some() {
s.env = cli.env.clone();
}
if cli.yes {
s.yes = Some(true);
}
if cli.quiet || cli.silent {
s.quiet = Some(true);
}
if cli.silent {
s.silent = Some(true);
}
if cli.trace {
s.log_level = Some("trace".to_string());
}
if cli.debug {
s.log_level = Some("debug".to_string());
}
if let Some(log_level) = &cli.log_level {
s.log_level = Some(log_level.to_string());
}
if cli.verbose > 0 {
s.verbose = Some(true);
}
if cli.verbose > 1 {
s.log_level = Some("trace".to_string());
}
Self::reset(Some(s));
}
pub fn parse_settings_file(path: &Path) -> Result<SettingsPartial> {
let raw = file::read_to_string(path)?;
let settings_file: SettingsFile = toml::from_str(&raw)?;
Ok(settings_file.settings)
}
fn all_settings_files() -> Vec<SettingsPartial> {
ALL_TOML_CONFIG_FILES
.iter()
.map(|p| Self::parse_settings_file(p))
.filter_map(|cfg| match cfg {
Ok(cfg) => Some(cfg),
Err(e) => {
eprintln!("Error loading settings file: {e}");
None
}
})
.collect()
}
pub fn hidden_configs() -> &'static HashSet<&'static str> {
static HIDDEN_CONFIGS: Lazy<HashSet<&'static str>> =
Lazy::new(|| ["ci", "cd", "debug", "env_file", "trace", "log_level"].into());
&HIDDEN_CONFIGS
}
pub fn reset(cli_settings: Option<SettingsPartial>) {
*CLI_SETTINGS.lock().unwrap() = cli_settings;
*BASE_SETTINGS.write().unwrap() = None;
crate::config::config_file::config_root::reset();
}
pub fn lockfile_enabled(&self) -> bool {
self.lockfile.unwrap_or(true)
}
pub fn lockfile_platforms(&self) -> Result<Option<Vec<Platform>>> {
match &self.lockfile_platforms {
Some(platforms) if !platforms.is_empty() => {
Ok(Some(Platform::parse_multiple(platforms)?))
}
_ => Ok(None),
}
}
pub fn force_provenance_verify(&self) -> bool {
self.locked_verify_provenance || self.paranoid
}
pub fn ensure_experimental(&self, what: &str) -> Result<()> {
if !self.experimental {
bail!("{what} is experimental. Enable it with `mise settings experimental=true`");
}
Ok(())
}
pub fn trusted_config_paths(&self) -> impl Iterator<Item = PathBuf> + '_ {
self.trusted_config_paths
.iter()
.filter(|p| !p.to_string_lossy().is_empty())
.map(file::replace_path)
.filter_map(|p| p.canonicalize().ok())
}
pub fn global_tools_file(&self) -> PathBuf {
env::var_path("MISE_GLOBAL_CONFIG_FILE")
.or_else(|| env::var_path("MISE_CONFIG_FILE"))
.unwrap_or_else(|| {
if self.asdf_compat {
env::HOME.join(&*env::MISE_DEFAULT_TOOL_VERSIONS_FILENAME)
} else {
dirs::CONFIG.join("config.toml")
}
})
}
pub fn env_files(&self) -> Vec<PathBuf> {
let mut files = vec![];
if let Some(cwd) = &*dirs::CWD
&& let Some(env_file) = &self.env_file
{
let env_file = env_file.to_string_lossy().to_string();
for p in FindUp::new(cwd, &[env_file]) {
files.push(p);
}
}
files.into_iter().rev().collect()
}
pub fn as_dict(&self) -> eyre::Result<toml::Table> {
let s = toml::to_string(self)?;
let table = toml::from_str(&s)?;
Ok(table)
}
pub fn cache_prune_age_duration(&self) -> Option<Duration> {
let age = duration::parse_duration(&self.cache_prune_age).unwrap();
if age.as_secs() == 0 { None } else { Some(age) }
}
pub fn fetch_remote_versions_timeout(&self) -> Duration {
duration::parse_duration(&self.fetch_remote_versions_timeout).unwrap()
}
pub fn fetch_remote_versions_cache(&self) -> Option<Duration> {
if self.prefer_offline() {
None
} else {
Some(duration::parse_duration(&self.fetch_remote_versions_cache).unwrap())
}
}
pub fn http_timeout(&self) -> Duration {
duration::parse_duration(&self.http_timeout).unwrap()
}
pub fn offline(&self) -> bool {
self.offline || *env::OFFLINE
}
pub fn prefer_offline(&self) -> bool {
self.offline() || self.prefer_offline || env::PREFER_OFFLINE.load(Ordering::Relaxed)
}
pub fn env_cache_ttl(&self) -> Duration {
duration::parse_duration(&self.env_cache_ttl).unwrap()
}
pub fn task_timeout_duration(&self) -> Option<Duration> {
self.task
.timeout
.as_ref()
.and_then(|s| duration::parse_duration(s).ok())
}
pub fn log_level(&self) -> log::LevelFilter {
self.log_level.parse().unwrap_or(log::LevelFilter::Info)
}
pub fn disable_tools(&self) -> BTreeSet<String> {
self.disable_tools
.iter()
.map(|t| t.trim().to_string())
.collect()
}
pub fn enable_tools(&self) -> BTreeSet<String> {
self.enable_tools
.iter()
.map(|t| t.trim().to_string())
.collect()
}
pub fn partial_as_dict(partial: &SettingsPartial) -> eyre::Result<toml::Table> {
let s = toml::to_string(partial)?;
let table = toml::from_str(&s)?;
Ok(table)
}
pub fn default_inline_shell(&self) -> Result<Vec<String>> {
let sa = if cfg!(windows) {
&self.windows_default_inline_shell_args
} else {
&self.unix_default_inline_shell_args
};
Ok(shell_words::split(sa)?)
}
pub fn default_file_shell(&self) -> Result<Vec<String>> {
let sa = if cfg!(windows) {
&self.windows_default_file_shell_args
} else {
&self.unix_default_file_shell_args
};
Ok(shell_words::split(sa)?)
}
pub fn os(&self) -> &str {
match self.os.as_deref().unwrap_or(OS) {
"darwin" | "macos" => "macos",
"linux" => "linux",
"windows" => "windows",
other => other,
}
}
pub fn arch(&self) -> &str {
match self.arch.as_deref().unwrap_or(ARCH) {
"x86_64" | "amd64" => "x64",
"aarch64" | "arm64" => "arm64",
other => other,
}
}
pub fn no_config() -> bool {
*env::MISE_NO_CONFIG
|| !*crate::env::IS_RUNNING_AS_SHIM
&& env::ARGS
.read()
.unwrap()
.iter()
.take_while(|a| *a != "--")
.any(|a| a == "--no-config")
}
pub fn no_env() -> bool {
*env::MISE_NO_ENV
|| !*crate::env::IS_RUNNING_AS_SHIM
&& env::ARGS
.read()
.unwrap()
.iter()
.take_while(|a| *a != "--")
.any(|a| a == "--no-env")
}
pub fn no_hooks() -> bool {
*env::MISE_NO_HOOKS
|| !*crate::env::IS_RUNNING_AS_SHIM
&& env::ARGS
.read()
.unwrap()
.iter()
.take_while(|a| *a != "--")
.any(|a| a == "--no-hooks")
}
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match toml::to_string_pretty(self) {
Ok(s) => write!(f, "{s}"),
Err(e) => Err(std::fmt::Error::custom(e)),
}
}
}
pub const DEFAULT_NODE_MIRROR_URL: &str = "https://nodejs.org/dist/";
impl SettingsNode {
pub fn mirror_url(&self) -> Url {
let s = self
.mirror_url
.clone()
.or(env::var("NODE_BUILD_MIRROR_URL").ok())
.unwrap_or_else(|| DEFAULT_NODE_MIRROR_URL.to_string());
Url::parse(&s).unwrap()
}
pub fn ninja(&self) -> bool {
self.ninja.unwrap_or_else(|| which::which("ninja").is_ok())
}
pub fn concurrency(&self) -> Option<usize> {
self.concurrency
.map(|c| std::cmp::max(c, 1) as usize)
.or_else(|| {
if self.ninja() {
None
} else {
Some(num_cpus::get_physical())
}
})
}
pub fn default_packages_file(&self) -> PathBuf {
self.default_packages_file
.clone()
.or_else(|| {
env::var("NODE_DEFAULT_PACKAGES_FILE")
.ok()
.map(PathBuf::from)
})
.unwrap_or_else(|| {
let p = env::HOME.join(".default-nodejs-packages");
if p.exists() {
return p;
}
let p = env::HOME.join(".default-node-packages");
if p.exists() {
return p;
}
env::HOME.join(".default-npm-packages")
})
}
pub fn cflags(&self) -> Option<String> {
self.cflags.clone().or_else(|| env::var("NODE_CFLAGS").ok())
}
pub fn configure_cmd(&self, install_path: &Path) -> String {
let mut configure_cmd = format!("./configure --prefix={}", install_path.display());
if self.ninja() {
configure_cmd.push_str(" --ninja");
}
if let Some(opts) = self
.configure_opts
.clone()
.or_else(|| env::var("NODE_CONFIGURE_OPTS").ok())
{
configure_cmd.push_str(&format!(" {opts}"));
}
configure_cmd
}
pub fn make_cmd(&self) -> String {
let mut make_cmd = self.make.clone().unwrap_or_else(|| "make".into());
if let Some(concurrency) = self.concurrency() {
make_cmd.push_str(&format!(" -j{concurrency}"));
}
if let Some(opts) = self
.make_opts
.clone()
.or_else(|| env::var("NODE_MAKE_OPTS").ok())
{
make_cmd.push_str(&format!(" {opts}"));
}
make_cmd
}
pub fn make_install_cmd(&self) -> String {
let make = self.make.clone().unwrap_or_else(|| "make".into());
let mut make_install_cmd = format!("{} install", make);
if let Some(opts) = self
.make_install_opts
.clone()
.or_else(|| env::var("NODE_MAKE_INSTALL_OPTS").ok())
{
make_install_cmd.push_str(&format!(" {opts}"));
}
make_install_cmd
}
}
impl SettingsStatus {
pub fn missing_tools(&self) -> SettingsStatusMissingTools {
SettingsStatusMissingTools::from_str(&self.missing_tools).unwrap()
}
}
fn bool_string<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.to_lowercase().as_str() {
"false" | "no" | "0" => Ok(false),
_ => Ok(true),
}
}
fn set_by_comma<T, C>(input: &str) -> Result<C, <T as FromStr>::Err>
where
T: FromStr + Eq + Ord,
C: FromIterator<T>,
{
input
.split(',')
.filter_map(|s| {
let trimmed = s.trim();
if !trimmed.is_empty() {
Some(T::from_str(trimmed))
} else {
None
}
})
.collect::<Result<BTreeSet<_>, _>>()
.map(|set| set.into_iter().collect())
}
pub fn parse_url_replacements(input: &str) -> Result<IndexMap<String, String>, serde_json::Error> {
serde_json::from_str(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_set_by_comma_empty_string() {
let result: Result<BTreeSet<String>, _> = set_by_comma("");
assert!(result.is_ok());
assert_eq!(result.unwrap(), BTreeSet::new());
}
#[test]
fn test_set_by_comma_whitespace_only() {
let result: Result<BTreeSet<String>, _> = set_by_comma(" ");
assert!(result.is_ok());
assert_eq!(result.unwrap(), BTreeSet::new());
}
#[test]
fn test_set_by_comma_single_value() {
let result: Result<BTreeSet<String>, _> = set_by_comma("foo");
assert!(result.is_ok());
let expected: BTreeSet<String> = ["foo".to_string()].into_iter().collect();
assert_eq!(result.unwrap(), expected);
}
#[test]
fn test_set_by_comma_multiple_values() {
let result: Result<BTreeSet<String>, _> = set_by_comma("foo,bar,baz");
assert!(result.is_ok());
let expected: BTreeSet<String> = ["foo".to_string(), "bar".to_string(), "baz".to_string()]
.into_iter()
.collect();
assert_eq!(result.unwrap(), expected);
}
#[test]
fn test_set_by_comma_with_whitespace() {
let result: Result<BTreeSet<String>, _> = set_by_comma("foo, bar, baz");
assert!(result.is_ok());
let expected: BTreeSet<String> = ["foo".to_string(), "bar".to_string(), "baz".to_string()]
.into_iter()
.collect();
assert_eq!(result.unwrap(), expected);
}
#[test]
fn test_set_by_comma_trailing_comma() {
let result: Result<BTreeSet<String>, _> = set_by_comma("foo,bar,");
assert!(result.is_ok());
let expected: BTreeSet<String> =
["foo".to_string(), "bar".to_string()].into_iter().collect();
assert_eq!(result.unwrap(), expected);
}
#[test]
fn test_set_by_comma_duplicate_values() {
let result: Result<BTreeSet<String>, _> = set_by_comma("foo,bar,foo");
assert!(result.is_ok());
let expected: BTreeSet<String> =
["foo".to_string(), "bar".to_string()].into_iter().collect();
assert_eq!(result.unwrap(), expected);
}
#[test]
fn test_set_by_comma_empty_elements() {
let result: Result<BTreeSet<String>, _> = set_by_comma("foo,,bar");
assert!(result.is_ok());
let expected: BTreeSet<String> =
["foo".to_string(), "bar".to_string()].into_iter().collect();
assert_eq!(result.unwrap(), expected);
}
#[test]
fn test_offline_default_is_false() {
Settings::reset(None);
let settings = Settings::get();
assert!(!settings.offline);
}
#[test]
fn test_prefer_offline_default_is_false() {
Settings::reset(None);
let settings = Settings::get();
assert!(!settings.prefer_offline);
}
#[test]
fn test_offline_setting_enables_offline() {
let mut partial = SettingsPartial::empty();
partial.offline = Some(true);
Settings::reset(Some(partial));
let settings = Settings::get();
assert!(settings.offline());
Settings::reset(None);
}
#[test]
fn test_offline_implies_prefer_offline() {
let mut partial = SettingsPartial::empty();
partial.offline = Some(true);
Settings::reset(Some(partial));
let settings = Settings::get();
assert!(settings.prefer_offline());
Settings::reset(None);
}
#[test]
fn test_prefer_offline_setting() {
let mut partial = SettingsPartial::empty();
partial.prefer_offline = Some(true);
Settings::reset(Some(partial));
let settings = Settings::get();
assert!(settings.prefer_offline());
assert!(!settings.offline);
Settings::reset(None);
}
#[test]
fn test_settings_toml_is_sorted() {
let content =
std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/settings.toml"))
.expect("failed to read settings.toml");
let table: toml::Table = content.parse().expect("failed to parse settings.toml");
fn collect_keys(table: &toml::Table, prefix: &str) -> Vec<String> {
let mut keys = Vec::new();
for (key, value) in table {
let full_key = if prefix.is_empty() {
key.clone()
} else {
format!("{prefix}.{key}")
};
if let toml::Value::Table(sub) = value {
if !sub.contains_key("type") && !sub.contains_key("description") {
keys.extend(collect_keys(sub, &full_key));
continue;
}
}
keys.push(full_key);
}
keys
}
let keys = collect_keys(&table, "");
let mut sorted = keys.clone();
sorted.sort();
for (i, (got, expected)) in keys.iter().zip(sorted.iter()).enumerate() {
assert_eq!(
got, expected,
"settings.toml is not alphabetically sorted at index {i}: found \"{got}\", expected \"{expected}\". Run the sort script or reorder manually."
);
}
}
#[test]
fn test_settings_node_build_cmds() {
let node = SettingsNode::default();
let path = Path::new("/tmp/install");
assert!(
node.configure_cmd(path)
.starts_with("./configure --prefix=/tmp/install")
);
assert!(node.make_cmd().starts_with("make"));
assert_eq!(node.make_install_cmd(), "make install");
}
#[test]
fn test_settings_node_build_cmds_with_opts() {
let node = SettingsNode {
configure_opts: Some("--verbose".to_string()),
make_opts: Some("-s".to_string()),
make_install_opts: Some("--no-strip".to_string()),
make: Some("gmake".to_string()),
concurrency: Some(4),
..Default::default()
};
let path = Path::new("/tmp/install");
assert!(node.configure_cmd(path).contains("--verbose"));
assert!(node.make_cmd().starts_with("gmake -j4 -s"));
assert_eq!(node.make_install_cmd(), "gmake install --no-strip");
}
}