aboutsummaryrefslogtreecommitdiff
path: root/src/config
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-12 03:58:17 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-12 03:58:27 +0300
commita6905364b69ff997c40d22c25e489d2e49b9c272 (patch)
tree2c788574160087dcc1024b8617d1ddba23e72b3a /src/config
parent749e85e0fb5443488c5a96752afa7dd6cb040143 (diff)
fix: clarify config fallback diagnostics
Diffstat (limited to 'src/config')
-rw-r--r--src/config/io.rs198
1 files changed, 129 insertions, 69 deletions
diff --git a/src/config/io.rs b/src/config/io.rs
index 6a4e83ce..c81b6e08 100644
--- a/src/config/io.rs
+++ b/src/config/io.rs
@@ -92,45 +92,54 @@ fn platform_state_dir() -> PathBuf {
}
}
+fn read_optional_config(path: &Path) -> std::io::Result<Option<String>> {
+ match std::fs::read_to_string(path) {
+ Ok(content) => Ok(Some(content)),
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
+ Err(err) => Err(err),
+ }
+}
+
impl Config {
pub fn load() -> LoadedConfig {
let path = config_path();
- if path.exists() {
- match std::fs::read_to_string(&path) {
- Ok(content) => match toml::from_str::<Config>(&content) {
- Ok(config) => {
- let mut diagnostics =
- unknown_top_level_section_diagnostics_from_str(&content);
- diagnostics.extend(config.collect_diagnostics());
- return LoadedConfig {
- config,
- diagnostics,
- invalid_sections: Vec::new(),
- };
- }
- Err(err) => {
- warn!(err = %err, "config parse error, using defaults");
- return LoadedConfig {
- config: Self::default(),
- diagnostics: vec![format!("config parse error: {err}; using defaults")],
- invalid_sections: Vec::new(),
- };
- }
- },
- Err(err) => {
- warn!(err = %err, "config read error, using defaults");
- return LoadedConfig {
- config: Self::default(),
- diagnostics: vec![format!("config read error: {err}; using defaults")],
- invalid_sections: Vec::new(),
- };
+ let content = match read_optional_config(&path) {
+ Ok(Some(content)) => content,
+ Ok(None) => {
+ return LoadedConfig {
+ config: Self::default(),
+ diagnostics: Vec::new(),
+ invalid_sections: Vec::new(),
+ };
+ }
+ Err(err) => {
+ warn!(err = %err, "config read error, using defaults");
+ return LoadedConfig {
+ config: Self::default(),
+ diagnostics: vec![format!("config read error: {err}; using defaults")],
+ invalid_sections: Vec::new(),
+ };
+ }
+ };
+
+ match toml::from_str::<Config>(&content) {
+ Ok(config) => {
+ let mut diagnostics = unknown_top_level_section_diagnostics_from_str(&content);
+ diagnostics.extend(config.collect_diagnostics());
+ LoadedConfig {
+ config,
+ diagnostics,
+ invalid_sections: Vec::new(),
+ }
+ }
+ Err(err) => {
+ warn!(err = %err, "config parse error, using defaults");
+ LoadedConfig {
+ config: Self::default(),
+ diagnostics: vec![format!("config parse error: {err}; using defaults")],
+ invalid_sections: Vec::new(),
}
}
- }
- LoadedConfig {
- config: Self::default(),
- diagnostics: Vec::new(),
- invalid_sections: Vec::new(),
}
}
}
@@ -163,41 +172,51 @@ pub fn config_diagnostic_summary(diagnostics: &[String]) -> Option<String> {
.and_then(|name| name.to_str())
.unwrap_or("config.toml")
.to_string();
- let location = diagnostics
+ let read_error = diagnostics
.iter()
- .find_map(|diagnostic| toml_error_location(diagnostic))
- .map(|(line, column)| format!("{target}:{line}:{column}"))
- .unwrap_or_else(|| target.to_string());
-
- Some(format!("{location}; herdr config check"))
-}
+ .any(|diagnostic| diagnostic.starts_with("config read error:"));
+ let impact = if diagnostics
+ .iter()
+ .any(|diagnostic| diagnostic.contains("using defaults"))
+ {
+ if read_error {
+ " unreadable; using defaults"
+ } else {
+ " invalid; using defaults"
+ }
+ } else if diagnostics
+ .iter()
+ .any(|diagnostic| diagnostic.contains("keeping current config"))
+ {
+ if read_error {
+ " unreadable; keeping current config"
+ } else {
+ " invalid; keeping current config"
+ }
+ } else {
+ ""
+ };
-fn toml_error_location(diagnostic: &str) -> Option<(usize, usize)> {
- let marker = "TOML parse error at line ";
- let start = diagnostic.find(marker)? + marker.len();
- let rest = &diagnostic[start..];
- let (line, rest) = rest.split_once(", column ")?;
- let column = rest
- .split(|c: char| !c.is_ascii_digit())
- .next()
- .unwrap_or_default();
- let line = line.parse().ok()?;
- let column = column.parse().ok()?;
- Some((line, column))
+ Some(format!("{target}{impact}; herdr config check"))
}
pub fn load_live_config() -> Result<LoadedConfig, Vec<String>> {
let path = config_path();
- if !path.exists() {
- return Ok(LoadedConfig {
- config: Config::default(),
- diagnostics: Vec::new(),
- invalid_sections: Vec::new(),
- });
- }
-
- let content = std::fs::read_to_string(&path)
- .map_err(|err| vec![format!("config read error: {err}; keeping current config")])?;
+ let content = match read_optional_config(&path) {
+ Ok(Some(content)) => content,
+ Ok(None) => {
+ return Ok(LoadedConfig {
+ config: Config::default(),
+ diagnostics: Vec::new(),
+ invalid_sections: Vec::new(),
+ });
+ }
+ Err(err) => {
+ return Err(vec![format!(
+ "config read error: {err}; keeping current config"
+ )]);
+ }
+ };
load_live_config_from_str(&content)
}
@@ -606,7 +625,7 @@ mod tests {
}
#[test]
- fn config_diagnostic_summary_includes_toml_error_location() {
+ fn config_diagnostic_summary_reports_default_fallback() {
let diagnostics = vec![
"config parse error: TOML parse error at line 33, column 8\n |\n33 | type = \"popup\"\n | ^^^^^^^\nunknown variant `popup`; using defaults"
.to_string(),
@@ -614,24 +633,65 @@ mod tests {
assert_eq!(
config_diagnostic_summary(&diagnostics).as_deref(),
- Some("config.toml:33:8; herdr config check")
+ Some("config.toml invalid; using defaults; herdr config check")
+ );
+ }
+
+ #[test]
+ fn config_diagnostic_summary_reports_unreadable_config_impact() {
+ let startup = vec!["config read error: permission denied; using defaults".to_string()];
+ assert_eq!(
+ config_diagnostic_summary(&startup).as_deref(),
+ Some("config.toml unreadable; using defaults; herdr config check")
+ );
+
+ let reload =
+ vec!["config read error: permission denied; keeping current config".to_string()];
+ assert_eq!(
+ config_diagnostic_summary(&reload).as_deref(),
+ Some("config.toml unreadable; keeping current config; herdr config check")
);
}
#[test]
- fn config_diagnostic_summary_finds_location_after_warning() {
+ fn config_diagnostic_summary_reports_retained_live_config() {
let diagnostics = vec![
- "unknown config section [toast]; ignoring section".to_string(),
- "config parse error: TOML parse error at line 7, column 4".to_string(),
+ "config parse error: TOML parse error at line 7, column 4; keeping current config"
+ .to_string(),
];
assert_eq!(
config_diagnostic_summary(&diagnostics).as_deref(),
- Some("config.toml:7:4; herdr config check")
+ Some("config.toml invalid; keeping current config; herdr config check")
);
}
#[test]
+ fn config_loaders_report_unreadable_path() {
+ let _guard = crate::config::test_config_env_lock().lock().unwrap();
+ let path =
+ std::env::temp_dir().join(format!("herdr-config-unreadable-{}", std::process::id()));
+ std::fs::create_dir_all(&path).unwrap();
+ std::env::set_var(CONFIG_PATH_ENV_VAR, &path);
+
+ let startup = Config::load();
+ assert!(startup
+ .diagnostics
+ .iter()
+ .any(|diagnostic| diagnostic.contains("config read error")
+ && diagnostic.contains("using defaults")));
+
+ let reload = load_live_config().unwrap_err();
+ assert!(reload.iter().any(|diagnostic| {
+ diagnostic.contains("config read error")
+ && diagnostic.contains("keeping current config")
+ }));
+
+ std::env::remove_var(CONFIG_PATH_ENV_VAR);
+ let _ = std::fs::remove_dir_all(path);
+ }
+
+ #[test]
fn load_live_config_parses_session_section() {
let loaded = load_live_config_from_str(
r#"