use crate::merge::{MissingCoveragePolicy, SortOrder};
use anyhow::{Context, Result};
use serde::Deserialize;
use std::fs;
use std::path::Path;
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Config {
pub threshold: Option<f64>,
pub fail_above: Option<bool>,
pub missing: Option<MissingCoveragePolicy>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(alias = "default_excludes")]
pub default_excludes: Option<Vec<String>>,
pub top: Option<usize>,
pub min: Option<f64>,
#[serde(default)]
pub allow: Vec<String>,
pub fail_regression: Option<bool>,
pub jobs: Option<usize>,
pub epsilon: Option<f64>,
pub sort: Option<SortOrder>,
#[serde(alias = "show_unchanged")]
pub show_unchanged: Option<bool>,
}
pub fn load(start: &Path) -> Result<Config> {
let mut dir = if start.is_file() {
start.parent().unwrap_or(start)
} else {
start
};
loop {
let candidate = dir.join(".cargo-crap.toml");
if candidate.exists() {
let raw = fs::read_to_string(&candidate)
.with_context(|| format!("reading {}", candidate.display()))?;
let cfg: Config =
toml::from_str(&raw).with_context(|| format!("parsing {}", candidate.display()))?;
return Ok(cfg);
}
match dir.parent() {
Some(p) => dir = p,
None => return Ok(Config::default()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn write_config(
dir: &Path,
content: &str,
) {
let mut f = fs::File::create(dir.join(".cargo-crap.toml")).unwrap();
f.write_all(content.as_bytes()).unwrap();
}
#[test]
fn missing_config_returns_defaults() {
let dir = tempfile::tempdir().unwrap();
let cfg = load(dir.path()).unwrap();
assert!(cfg.threshold.is_none());
assert!(cfg.fail_above.is_none());
assert!(cfg.missing.is_none());
assert!(cfg.exclude.is_empty());
assert!(cfg.allow.is_empty());
}
#[test]
fn config_file_is_parsed() {
let dir = tempfile::tempdir().unwrap();
write_config(
dir.path(),
r#"
threshold = 20.0
fail-above = true
missing = "optimistic"
exclude = ["tests/**"]
allow = ["Foo::*"]
"#,
);
let cfg = load(dir.path()).unwrap();
assert_eq!(cfg.threshold, Some(20.0));
assert_eq!(cfg.fail_above, Some(true));
assert_eq!(cfg.missing, Some(MissingCoveragePolicy::Optimistic));
assert_eq!(cfg.exclude, ["tests/**"]);
assert_eq!(cfg.allow, ["Foo::*"]);
}
#[test]
fn default_excludes_absent_means_none() {
let dir = tempfile::tempdir().unwrap();
write_config(dir.path(), "threshold = 20.0\n");
let cfg = load(dir.path()).unwrap();
assert!(cfg.default_excludes.is_none());
}
#[test]
fn default_excludes_kebab_case_is_parsed() {
let dir = tempfile::tempdir().unwrap();
write_config(
dir.path(),
"default-excludes = [\"benches/**\", \"examples/**\"]\n",
);
let cfg = load(dir.path()).unwrap();
assert_eq!(
cfg.default_excludes.as_deref(),
Some(&["benches/**".to_string(), "examples/**".to_string()][..])
);
}
#[test]
fn default_excludes_snake_case_alias_is_parsed() {
let dir = tempfile::tempdir().unwrap();
write_config(dir.path(), "default_excludes = [\"tests/**\"]\n");
let cfg = load(dir.path()).unwrap();
assert_eq!(
cfg.default_excludes.as_deref(),
Some(&["tests/**".to_string()][..])
);
}
#[test]
fn default_excludes_empty_list_is_some_empty() {
let dir = tempfile::tempdir().unwrap();
write_config(dir.path(), "default-excludes = []\n");
let cfg = load(dir.path()).unwrap();
assert_eq!(cfg.default_excludes.as_deref(), Some(&[][..]));
}
#[test]
fn config_is_found_by_walking_up() {
let dir = tempfile::tempdir().unwrap();
write_config(dir.path(), "threshold = 15.0\n");
let subdir = dir.path().join("src");
fs::create_dir(&subdir).unwrap();
let cfg = load(&subdir).unwrap();
assert_eq!(cfg.threshold, Some(15.0));
}
#[test]
fn sort_and_show_unchanged_are_parsed() {
let dir = tempfile::tempdir().unwrap();
write_config(dir.path(), "sort = \"file\"\nshow_unchanged = true\n");
let cfg = load(dir.path()).unwrap();
assert_eq!(cfg.sort, Some(SortOrder::File));
assert_eq!(cfg.show_unchanged, Some(true));
}
#[test]
fn sort_and_show_unchanged_absent_means_none() {
let dir = tempfile::tempdir().unwrap();
write_config(dir.path(), "threshold = 20.0\n");
let cfg = load(dir.path()).unwrap();
assert!(cfg.sort.is_none());
assert!(cfg.show_unchanged.is_none());
}
#[test]
fn unknown_key_returns_error() {
let dir = tempfile::tempdir().unwrap();
write_config(dir.path(), "unknown-key = true\n");
let err = load(dir.path()).unwrap_err();
assert!(
err.to_string().contains("parsing"),
"expected parse error, got: {err}"
);
}
}