1use crate::merge::MissingCoveragePolicy;
20use anyhow::{Context, Result};
21use serde::Deserialize;
22use std::fs;
23use std::path::Path;
24
25#[derive(Debug, Default, Deserialize)]
30#[serde(deny_unknown_fields, rename_all = "kebab-case")]
31pub struct Config {
32 pub threshold: Option<f64>,
34
35 pub fail_above: Option<bool>,
37
38 pub missing: Option<MissingCoveragePolicy>,
41
42 #[serde(default)]
44 pub exclude: Vec<String>,
45
46 pub top: Option<usize>,
48
49 pub min: Option<f64>,
51
52 #[serde(default)]
56 pub allow: Vec<String>,
57
58 pub fail_regression: Option<bool>,
60
61 pub jobs: Option<usize>,
65
66 pub epsilon: Option<f64>,
70}
71
72pub fn load(start: &Path) -> Result<Config> {
77 let mut dir = if start.is_file() {
78 start.parent().unwrap_or(start)
79 } else {
80 start
81 };
82
83 loop {
84 let candidate = dir.join(".cargo-crap.toml");
85 if candidate.exists() {
86 let raw = fs::read_to_string(&candidate)
87 .with_context(|| format!("reading {}", candidate.display()))?;
88 let cfg: Config =
89 toml::from_str(&raw).with_context(|| format!("parsing {}", candidate.display()))?;
90 return Ok(cfg);
91 }
92 match dir.parent() {
93 Some(p) => dir = p,
94 None => return Ok(Config::default()),
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use std::io::Write;
103
104 fn write_config(
105 dir: &Path,
106 content: &str,
107 ) {
108 let mut f = fs::File::create(dir.join(".cargo-crap.toml")).unwrap();
109 f.write_all(content.as_bytes()).unwrap();
110 }
111
112 #[test]
113 fn missing_config_returns_defaults() {
114 let dir = tempfile::tempdir().unwrap();
115 let cfg = load(dir.path()).unwrap();
116 assert!(cfg.threshold.is_none());
117 assert!(cfg.fail_above.is_none());
118 assert!(cfg.missing.is_none());
119 assert!(cfg.exclude.is_empty());
120 assert!(cfg.allow.is_empty());
121 }
122
123 #[test]
124 fn config_file_is_parsed() {
125 let dir = tempfile::tempdir().unwrap();
126 write_config(
127 dir.path(),
128 r#"
129threshold = 20.0
130fail-above = true
131missing = "optimistic"
132exclude = ["tests/**"]
133allow = ["Foo::*"]
134"#,
135 );
136 let cfg = load(dir.path()).unwrap();
137 assert_eq!(cfg.threshold, Some(20.0));
138 assert_eq!(cfg.fail_above, Some(true));
139 assert_eq!(cfg.missing, Some(MissingCoveragePolicy::Optimistic));
140 assert_eq!(cfg.exclude, ["tests/**"]);
141 assert_eq!(cfg.allow, ["Foo::*"]);
142 }
143
144 #[test]
145 fn config_is_found_by_walking_up() {
146 let dir = tempfile::tempdir().unwrap();
147 write_config(dir.path(), "threshold = 15.0\n");
148 let subdir = dir.path().join("src");
149 fs::create_dir(&subdir).unwrap();
150 let cfg = load(&subdir).unwrap();
152 assert_eq!(cfg.threshold, Some(15.0));
153 }
154
155 #[test]
156 fn unknown_key_returns_error() {
157 let dir = tempfile::tempdir().unwrap();
158 write_config(dir.path(), "unknown-key = true\n");
159 let err = load(dir.path()).unwrap_err();
160 assert!(
161 err.to_string().contains("parsing"),
162 "expected parse error, got: {err}"
163 );
164 }
165}