use std::{
num::NonZero,
path::PathBuf,
sync::{Arc, LazyLock, Mutex},
thread,
};
use arc_swap::ArcSwap;
use generated::merge::{SettingValue, SourceMap};
use indexmap::IndexSet;
use once_cell::sync::Lazy;
use serde_json::json;
pub mod generated {
pub mod settings {
include!(concat!(env!("OUT_DIR"), "/generated_settings.rs"));
}
pub mod settings_meta {
include!(concat!(env!("OUT_DIR"), "/generated_settings_meta.rs"));
}
pub mod merge {
include!(concat!(env!("OUT_DIR"), "/generated_settings_merge.rs"));
}
pub use settings_meta::*;
}
pub use generated::settings::Settings;
use crate::settings::generated::merge::SettingSource;
#[derive(Debug, Clone, Default)]
pub struct CliSnapshot {
pub hkrc: Option<PathBuf>,
pub jobs: Option<usize>,
pub profiles: Vec<String>,
pub slow: bool,
pub quiet: bool,
pub silent: bool,
pub trace: bool,
}
static CLI_SNAPSHOT: Lazy<Mutex<Option<CliSnapshot>>> = Lazy::new(|| Mutex::new(None));
static PROGRAMMATIC_MAP: Lazy<Mutex<SourceMap>> = Lazy::new(|| Mutex::new(SourceMap::new()));
fn read_git_string_list(config: &git2::Config, key: &str) -> Result<IndexSet<String>, git2::Error> {
let mut result = IndexSet::new();
match config.multivar(key, None) {
Ok(mut entries) => {
while let Some(entry) = entries.next() {
if let Some(value) = entry?.value() {
for item in value.split(',').map(|s| s.trim()) {
if !item.is_empty() {
result.insert(item.to_string());
}
}
}
}
}
Err(_) => {
if let Ok(value) = config.get_string(key) {
for item in value.split(',').map(|s| s.trim()) {
if !item.is_empty() {
result.insert(item.to_string());
}
}
}
}
}
Ok(result)
}
impl Settings {
pub fn set_cli_snapshot(snapshot: CliSnapshot) {
let mut guard = CLI_SNAPSHOT.lock().unwrap();
*guard = Some(snapshot);
}
pub fn cli_user_config_path() -> Option<PathBuf> {
CLI_SNAPSHOT
.lock()
.unwrap()
.as_ref()
.and_then(|s| s.hkrc.clone())
}
pub fn cli_trace() -> bool {
CLI_SNAPSHOT
.lock()
.unwrap()
.as_ref()
.map(|s| s.trace)
.unwrap_or(false)
}
pub fn get() -> Arc<Settings> {
Self::get_snapshot().expect("Failed to load configuration")
}
pub fn try_get() -> Result<Arc<Settings>, eyre::Error> {
Self::get_snapshot()
}
fn get_snapshot() -> Result<SettingsSnapshot, eyre::Error> {
let mut initialized = INITIALIZED.lock().unwrap();
if !*initialized {
let new_settings = Arc::new(Self::build_from_all_sources()?);
GLOBAL_SETTINGS.store(new_settings.clone());
*initialized = true;
return Ok(new_settings);
}
drop(initialized);
Ok(GLOBAL_SETTINGS.load_full())
}
pub fn jobs(&self) -> NonZero<usize> {
NonZero::new(self.jobs).unwrap_or(thread::available_parallelism().unwrap())
}
pub fn enabled_profiles(&self) -> IndexSet<String> {
self.profiles
.iter()
.filter(|p| !p.starts_with('!'))
.cloned()
.collect()
}
pub fn disabled_profiles(&self) -> IndexSet<String> {
self.profiles
.iter()
.filter(|p| p.starts_with('!'))
.map(|p| p.strip_prefix('!').unwrap().to_string())
.collect()
}
fn build_from_all_sources() -> Result<Settings, eyre::Error> {
let defaults = generated::settings::Settings::default();
let env_map = Self::collect_env_map()?;
let git_map = Self::collect_git_map()?;
let pkl_map = Self::collect_pkl_map()?;
let cli_map = Self::collect_cli_map();
Ok(Self::merge_settings_generic(
&defaults, &env_map, &git_map, &pkl_map, &cli_map,
))
}
pub(crate) fn merge_settings_generic(
defaults: &generated::settings::Settings,
env: &SourceMap,
git: &SourceMap,
pkl: &SourceMap,
cli: &SourceMap,
) -> generated::settings::Settings {
let mut val = serde_json::to_value(defaults.clone()).unwrap_or_else(|_| json!({}));
fn set_value(val: &mut serde_json::Value, field: &str, v: &SettingValue) {
let new_v = match v {
SettingValue::Bool(b) => json!(b),
SettingValue::Usize(n) => json!(n),
SettingValue::U8(n) => json!(n),
SettingValue::String(s) => json!(s),
SettingValue::Path(p) => json!(p.display().to_string()),
SettingValue::StringList(list) => json!(list.iter().collect::<Vec<_>>()),
};
if let Some(obj) = val.as_object_mut() {
obj.insert(field.to_string(), new_v);
}
}
fn union_list(val: &mut serde_json::Value, field: &str, list: &indexmap::IndexSet<String>) {
let mut current: indexmap::IndexSet<String> =
if let Some(arr) = val.get(field).and_then(|v| v.as_array()) {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
} else {
indexmap::IndexSet::new()
};
current.extend(list.iter().cloned());
if let Some(obj) = val.as_object_mut() {
obj.insert(field.to_string(), json!(current.iter().collect::<Vec<_>>()));
}
}
for (name, meta) in generated::SETTINGS_META.iter() {
let field = *name;
let merge_is_union = meta.merge == Some("union");
let mut apply = |map: &SourceMap| {
if let Some(sv) = map.get(field) {
match sv {
SettingValue::StringList(set) if merge_is_union => {
union_list(&mut val, field, set)
}
_ => set_value(&mut val, field, sv),
}
}
};
apply(pkl);
apply(git);
apply(env);
apply(cli);
}
serde_json::from_value(val).unwrap_or_else(|_| defaults.clone())
}
pub(crate) fn merge_settings_with_sources_generic(
defaults: &generated::settings::Settings,
env: &SourceMap,
git: &SourceMap,
pkl: &SourceMap,
cli: &SourceMap,
) -> (
generated::settings::Settings,
generated::merge::SourceInfoMap,
) {
let mut val = serde_json::to_value(defaults.clone()).unwrap_or_else(|_| json!({}));
let mut info: generated::merge::SourceInfoMap = indexmap::IndexMap::new();
fn set_value2(
val: &mut serde_json::Value,
info: &mut generated::merge::SourceInfoMap,
field: &'static str,
v: &SettingValue,
src: SettingSource,
) {
let new_v = match v {
SettingValue::Bool(b) => json!(b),
SettingValue::Usize(n) => json!(n),
SettingValue::U8(n) => json!(n),
SettingValue::String(s) => json!(s),
SettingValue::Path(p) => json!(p.display().to_string()),
SettingValue::StringList(list) => json!(list.iter().collect::<Vec<_>>()),
};
if let Some(obj) = val.as_object_mut() {
obj.insert(field.to_string(), new_v);
}
info.entry(field).or_default().last = Some(src);
}
fn union_list2(
val: &mut serde_json::Value,
info: &mut generated::merge::SourceInfoMap,
field: &'static str,
list: &indexmap::IndexSet<String>,
src: SettingSource,
) {
let mut current: indexmap::IndexSet<String> =
if let Some(arr) = val.get(field).and_then(|v| v.as_array()) {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
} else {
indexmap::IndexSet::new()
};
for item in list.iter() {
let inserted = current.insert(item.clone());
let entry = info.entry(field).or_default();
let m = entry.list_items.get_or_insert_with(indexmap::IndexMap::new);
let srcs = m.entry(item.clone()).or_default();
srcs.push(src.clone());
if inserted {
}
}
if let Some(obj) = val.as_object_mut() {
obj.insert(field.to_string(), json!(current.iter().collect::<Vec<_>>()));
}
info.entry(field).or_default().last = Some(src);
}
for (name, meta) in generated::SETTINGS_META.iter() {
let field = *name;
if meta.typ.starts_with("list<string>")
&& let Some(arr) = val.get(field).and_then(|v| v.as_array())
&& !arr.is_empty()
{
let mut m: indexmap::IndexMap<String, Vec<SettingSource>> =
indexmap::IndexMap::new();
for it in arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())) {
m.insert(it, vec![SettingSource::Defaults]);
}
info.entry(field).or_default().list_items = Some(m);
}
info.entry(field).or_default().last = Some(SettingSource::Defaults);
}
for (name, meta) in generated::SETTINGS_META.iter() {
let field = *name;
let merge_is_union = meta.merge == Some("union");
let mut apply = |map: &SourceMap, src: SettingSource| {
if let Some(sv) = map.get(field) {
match sv {
SettingValue::StringList(set) if merge_is_union => {
union_list2(&mut val, &mut info, field, set, src)
}
_ => set_value2(&mut val, &mut info, field, sv, src),
}
}
};
apply(pkl, SettingSource::Pkl);
apply(git, SettingSource::Git);
apply(env, SettingSource::Env);
apply(cli, SettingSource::Cli);
}
(
serde_json::from_value(val).unwrap_or_else(|_| defaults.clone()),
info,
)
}
fn collect_env_map() -> Result<SourceMap, eyre::Error> {
let mut map: SourceMap = SourceMap::new();
for (setting_name, meta) in generated::SETTINGS_META.iter() {
for env_var in meta.sources.env {
if let Ok(val) = std::env::var(env_var) {
match meta.typ {
"bool" => {
let s = val.to_lowercase();
let parsed = matches!(s.as_str(), "1" | "true" | "yes" | "on");
map.insert(setting_name, SettingValue::Bool(parsed));
}
"usize" => {
let n = val.parse::<usize>().map_err(|_| {
eyre::eyre!(
"Invalid value for {}: '{}' (expected positive integer)",
env_var,
val
)
})?;
map.insert(setting_name, SettingValue::Usize(n));
}
"u8" => {
let n = val.parse::<u8>().map_err(|_| {
eyre::eyre!(
"Invalid value for {}: '{}' (expected integer 0-255)",
env_var,
val
)
})?;
map.insert(setting_name, SettingValue::U8(n));
}
t if t.starts_with("list<string>") => {
let items: IndexSet<String> = val
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
map.insert(setting_name, SettingValue::StringList(items));
}
"path" => {
map.insert(setting_name, SettingValue::Path(PathBuf::from(val)));
}
"string" | "enum" => {
map.insert(setting_name, SettingValue::String(val));
}
_ => {}
}
break; }
}
}
Ok(map)
}
fn collect_git_map() -> Result<SourceMap, eyre::Error> {
let mut map: SourceMap = SourceMap::new();
let cfg = {
use git2::{Config, Repository};
if let Ok(repo) = Repository::open_from_env() {
repo.config()
} else if let Ok(repo) = Repository::discover(".") {
repo.config()
} else {
Config::open_default()
}
}?;
for (setting_name, meta) in generated::SETTINGS_META.iter() {
let mut merged: Option<IndexSet<String>> = None;
for key in meta.sources.git {
match meta.typ {
"bool" => {
if let Ok(v) = cfg.get_bool(key) {
map.insert(setting_name, SettingValue::Bool(v));
break;
}
}
"usize" => {
if let Ok(v) = cfg.get_i32(key)
&& v > 0
{
map.insert(setting_name, SettingValue::Usize(v as usize));
break;
}
}
"u8" => {
if let Ok(v) = cfg.get_i32(key)
&& (0..=255).contains(&v)
{
map.insert(setting_name, SettingValue::U8(v as u8));
break;
}
}
t if t.starts_with("list<string>") => {
if let Ok(list) = read_git_string_list(&cfg, key)
&& !list.is_empty()
{
if let Some(acc) = &mut merged {
acc.extend(list);
} else {
merged = Some(list);
}
}
}
"string" | "enum" => {
if let Ok(v) = cfg.get_str(key) {
map.insert(setting_name, SettingValue::String(v.to_string()));
break;
}
}
"path" => {
if let Ok(v) = cfg.get_str(key) {
map.insert(setting_name, SettingValue::Path(PathBuf::from(v)));
break;
}
}
_ => {}
}
}
if let Some(set) = merged {
map.insert(setting_name, SettingValue::StringList(set));
}
}
Ok(map)
}
fn collect_pkl_map() -> Result<SourceMap, eyre::Error> {
let mut map: SourceMap = SourceMap::new();
let cfg = crate::config::Config::get()?;
let config_json = serde_json::to_value(&cfg)?;
for (setting_name, meta) in generated::SETTINGS_META.iter() {
if !meta.sources.pkl.is_empty() {
let field_name = setting_name.replace('-', "_");
if let Some(value) = config_json.get(&field_name) {
match (meta.typ, value) {
("bool", serde_json::Value::Bool(b)) => {
map.insert(setting_name, SettingValue::Bool(*b));
}
("usize", serde_json::Value::Number(n)) => {
if let Some(u) = n.as_u64().and_then(|u| usize::try_from(u).ok()) {
map.insert(setting_name, SettingValue::Usize(u));
}
}
("u8", serde_json::Value::Number(n)) => {
if let Some(u) = n.as_u64().and_then(|u| u8::try_from(u).ok()) {
map.insert(setting_name, SettingValue::U8(u));
}
}
("string" | "enum", serde_json::Value::String(s)) => {
map.insert(setting_name, SettingValue::String(s.clone()));
}
("path", serde_json::Value::String(s)) => {
map.insert(setting_name, SettingValue::Path(s.into()));
}
(typ, serde_json::Value::Array(arr)) if typ.starts_with("list<string>") => {
let strings: IndexSet<String> = arr
.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect();
map.insert(setting_name, SettingValue::StringList(strings));
}
(typ, serde_json::Value::String(s)) if typ.starts_with("list<string>") => {
let strings: IndexSet<String> = IndexSet::from([s.clone()]);
map.insert(setting_name, SettingValue::StringList(strings));
}
_ => {
}
}
}
}
}
Ok(map)
}
fn collect_cli_map() -> SourceMap {
let mut map: SourceMap = SourceMap::new();
if let Some(snapshot) = CLI_SNAPSHOT.lock().unwrap().clone() {
if let Some(p) = snapshot.hkrc {
map.insert("hkrc", SettingValue::Path(p));
}
if let Some(j) = snapshot.jobs {
map.insert("jobs", SettingValue::Usize(j));
}
let mut profiles: IndexSet<String> = snapshot.profiles.into_iter().collect();
if snapshot.slow {
profiles.insert("slow".to_string());
}
if !profiles.is_empty() {
map.insert("profiles", SettingValue::StringList(profiles));
}
if snapshot.quiet {
map.insert("quiet", SettingValue::Bool(true));
}
if snapshot.silent {
map.insert("silent", SettingValue::Bool(true));
}
}
{
let prog = PROGRAMMATIC_MAP.lock().unwrap();
for (k, v) in prog.iter() {
match (map.get(k), v) {
(
Some(SettingValue::StringList(existing)),
SettingValue::StringList(additional),
) => {
let mut merged = existing.clone();
merged.extend(additional.iter().cloned());
map.insert(*k, SettingValue::StringList(merged));
}
_ => {
map.insert(*k, v.clone());
}
}
}
}
map
}
}
pub type SettingsSnapshot = Arc<Settings>;
static GLOBAL_SETTINGS: LazyLock<ArcSwap<Settings>> = LazyLock::new(|| {
ArcSwap::from_pointee(generated::settings::Settings::default())
});
static INITIALIZED: LazyLock<Mutex<bool>> = LazyLock::new(|| Mutex::new(false));
impl Settings {
pub fn explain_value(key: &str) -> Result<String, eyre::Error> {
use std::fmt::Write;
let field_name = key.replace('-', "_");
let meta = generated::SETTINGS_META
.get(field_name.as_str())
.ok_or_else(|| eyre::eyre!("Unknown configuration key: {}", key))?;
let env_map = Self::collect_env_map()?;
let git_map = Self::collect_git_map()?;
let pkl_map = Self::collect_pkl_map()?;
let cli_map = Self::collect_cli_map();
let defaults = generated::settings::Settings::default();
let (_merged, sources) = Self::merge_settings_with_sources_generic(
&defaults, &env_map, &git_map, &pkl_map, &cli_map,
);
let env_id: Option<&'static str> = meta
.sources
.env
.iter()
.copied()
.find(|name| std::env::var(name).is_ok());
let git_id: Option<&'static str> = {
use git2::{Config, Repository};
let cfg_result = if let Ok(repo) = Repository::open_from_env() {
repo.config()
} else if let Ok(repo) = Repository::discover(".") {
repo.config()
} else {
Config::open_default()
};
match cfg_result {
Ok(cfg) => meta
.sources
.git
.iter()
.copied()
.find(|k| cfg.get_entry(k).is_ok()),
Err(_) => None,
}
};
let pkl_id: Option<&'static str> = if pkl_map.get(field_name.as_str()).is_some() {
meta.sources.pkl.first().copied()
} else {
None
};
let cli_id: Option<&'static str> = if cli_map.get(field_name.as_str()).is_some() {
meta.sources.cli.first().copied()
} else {
None
};
let source_to_string = |s: &generated::merge::SettingSource| -> String {
match s {
generated::merge::SettingSource::Defaults => "defaults".to_string(),
generated::merge::SettingSource::Env => match env_id {
Some(id) => format!("env({})", id),
None => "env".to_string(),
},
generated::merge::SettingSource::Git => match git_id {
Some(id) => format!("git({})", id),
None => "git".to_string(),
},
generated::merge::SettingSource::Pkl => match pkl_id {
Some(id) => format!("pkl({})", id),
None => "pkl".to_string(),
},
generated::merge::SettingSource::Cli => match cli_id {
Some(id) => format!("cli({})", id),
None => "cli".to_string(),
},
}
};
fn display_value(v: &SettingValue) -> String {
match v {
SettingValue::Bool(b) => b.to_string(),
SettingValue::Usize(n) => n.to_string(),
SettingValue::U8(n) => n.to_string(),
SettingValue::String(s) => s.clone(),
SettingValue::Path(p) => p.display().to_string(),
SettingValue::StringList(list) => format!("{:?}", list),
}
}
let mut output = String::new();
writeln!(
&mut output,
"Source resolution for '{}' (in precedence order):",
key
)?;
writeln!(
&mut output,
"================================================"
)?;
if !meta.sources.cli.is_empty() {
writeln!(&mut output, " CLI FLAGS: {}", meta.sources.cli.join(", "))?;
if let Some(v) = cli_map.get(field_name.as_str()) {
writeln!(&mut output, " ✓ Set to: {}", display_value(v))?;
}
if let Some(info) = sources.get(field_name.as_str())
&& let Some(src) = &info.last
{
writeln!(&mut output, " Source: {}", source_to_string(src))?;
}
}
if !meta.sources.env.is_empty() {
writeln!(
&mut output,
" ENVIRONMENT: {}",
meta.sources.env.join(", ")
)?;
if let Some(v) = env_map.get(field_name.as_str()) {
writeln!(&mut output, " ✓ Set to: {}", display_value(v))?;
}
if let Some(info) = sources.get(field_name.as_str())
&& let Some(src) = &info.last
{
writeln!(&mut output, " Source: {}", source_to_string(src))?;
}
}
if !meta.sources.git.is_empty() {
writeln!(&mut output, " GIT CONFIG: {}", meta.sources.git.join(", "))?;
if let Some(v) = git_map.get(field_name.as_str()) {
writeln!(&mut output, " ✓ Set to: {}", display_value(v))?;
}
if let Some(info) = sources.get(field_name.as_str())
&& let Some(src) = &info.last
{
writeln!(&mut output, " Source: {}", source_to_string(src))?;
}
}
if !meta.sources.pkl.is_empty() {
writeln!(&mut output, " PKL CONFIG: {}", meta.sources.pkl.join(", "))?;
if let Some(v) = pkl_map.get(field_name.as_str()) {
writeln!(&mut output, " ✓ Set to: {}", display_value(v))?;
}
if let Some(info) = sources.get(field_name.as_str())
&& let Some(src) = &info.last
{
writeln!(&mut output, " Source: {}", source_to_string(src))?;
}
}
writeln!(&mut output, " DEFAULT:")?;
if let Some(default) = &meta.default_value {
writeln!(&mut output, " Value: {}", default)?;
} else {
writeln!(&mut output, " Value: (type default)")?;
}
if meta.typ.starts_with("list<string>")
&& let Some(info) = sources.get(field_name.as_str())
&& let Some(items) = &info.list_items
{
writeln!(&mut output, "\n Items and their sources:")?;
for (item, srcs) in items.iter() {
let parts: Vec<String> = srcs.iter().map(&source_to_string).collect();
writeln!(&mut output, " - {}: {}", item, parts.join(", "))?;
}
}
writeln!(&mut output)?;
writeln!(
&mut output,
"Merge strategy: {}",
meta.merge.unwrap_or("replace")
)?;
Ok(output)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::settings::generated;
use crate::settings::generated::merge::{SettingValue, SourceMap};
#[test]
fn test_settings_builder_fluent_api() {
Settings::set_cli_snapshot(CliSnapshot {
hkrc: None,
..Default::default()
});
let settings = Settings::get();
assert!(settings.jobs().get() >= 1);
}
#[test]
fn test_settings_snapshot_caching() {
Settings::set_cli_snapshot(CliSnapshot {
hkrc: None,
..Default::default()
});
let snapshot1 = Settings::get_snapshot().unwrap();
let snapshot2 = Settings::get_snapshot().unwrap();
assert!(Arc::ptr_eq(&snapshot1, &snapshot2));
}
#[test]
fn test_settings_from_config() {
Settings::set_cli_snapshot(CliSnapshot {
hkrc: None,
..Default::default()
});
let _settings = Settings::get();
}
fn set_list(map: &mut SourceMap, key: &'static str, vals: &[&str]) {
let set: IndexSet<String> = vals.iter().map(|s| (*s).to_string()).collect();
map.insert(key, SettingValue::StringList(set));
}
#[test]
fn test_union_merge_skip_steps() {
let defaults = generated::settings::Settings {
skip_steps: IndexSet::from(["default_step".to_string()]),
..Default::default()
};
let mut env: SourceMap = SourceMap::new();
let mut git: SourceMap = SourceMap::new();
let mut pkl: SourceMap = SourceMap::new();
let mut cli: SourceMap = SourceMap::new();
set_list(&mut pkl, "skip_steps", &["pkl_step"]);
set_list(&mut git, "skip_steps", &["git_step", "pkl_step"]);
set_list(&mut env, "skip_steps", &["env_step"]);
set_list(&mut cli, "skip_steps", &["cli_step", "env_step"]);
let merged = Settings::merge_settings_generic(&defaults, &env, &git, &pkl, &cli);
assert!(merged.skip_steps.contains("default_step"));
assert!(merged.skip_steps.contains("pkl_step"));
assert!(merged.skip_steps.contains("git_step"));
assert!(merged.skip_steps.contains("env_step"));
assert!(merged.skip_steps.contains("cli_step"));
assert_eq!(merged.skip_steps.len(), 5);
}
#[test]
fn test_replace_merge_fail_fast() {
let defaults = generated::settings::Settings::default();
let mut env: SourceMap = SourceMap::new();
let mut git: SourceMap = SourceMap::new();
let mut pkl: SourceMap = SourceMap::new();
let mut cli: SourceMap = SourceMap::new();
env.insert("fail_fast", SettingValue::Bool(false));
git.insert("fail_fast", SettingValue::Bool(true));
pkl.insert("fail_fast", SettingValue::Bool(false));
cli.insert("fail_fast", SettingValue::Bool(true));
let merged = Settings::merge_settings_generic(&defaults, &env, &git, &pkl, &cli);
assert!(merged.fail_fast);
}
#[test]
fn test_precedence_ordering_jobs() {
let defaults = generated::settings::Settings::default();
let mut env: SourceMap = SourceMap::new();
let mut git: SourceMap = SourceMap::new();
let mut pkl: SourceMap = SourceMap::new();
let mut cli: SourceMap = SourceMap::new();
pkl.insert("jobs", SettingValue::Usize(2));
git.insert("jobs", SettingValue::Usize(3));
env.insert("jobs", SettingValue::Usize(4));
cli.insert("jobs", SettingValue::Usize(5));
let merged = Settings::merge_settings_generic(&defaults, &env, &git, &pkl, &cli);
assert_eq!(merged.jobs, 5);
}
#[test]
fn test_precedence_with_missing_layers() {
let defaults = generated::settings::Settings::default();
let env: SourceMap = SourceMap::new();
let mut git: SourceMap = SourceMap::new();
let pkl: SourceMap = SourceMap::new();
let cli: SourceMap = SourceMap::new();
git.insert("jobs", SettingValue::Usize(3));
let merged = Settings::merge_settings_generic(&defaults, &env, &git, &pkl, &cli);
assert_eq!(merged.jobs, 3);
}
#[test]
fn test_warnings_replace_merge() {
let defaults = generated::settings::Settings {
warnings: IndexSet::from(["warning1".to_string()]),
..Default::default()
};
let mut env: SourceMap = SourceMap::new();
let git: SourceMap = SourceMap::new();
let pkl: SourceMap = SourceMap::new();
let cli: SourceMap = SourceMap::new();
let env_set: IndexSet<String> = IndexSet::from(["warning4".to_string()]);
env.insert("warnings", SettingValue::StringList(env_set));
let merged = Settings::merge_settings_generic(&defaults, &env, &git, &pkl, &cli);
assert!(merged.warnings.contains("warning4"));
assert_eq!(merged.warnings.len(), 1);
}
}