1use crate::merge::{MissingCoveragePolicy, SortOrder};
27use anyhow::{Context, Result};
28use serde::Deserialize;
29use std::fs;
30use std::path::Path;
31
32#[derive(Debug, Default, Deserialize)]
37#[serde(deny_unknown_fields, rename_all = "kebab-case")]
38pub struct Config {
39 pub threshold: Option<f64>,
41
42 pub fail_above: Option<bool>,
44
45 pub missing: Option<MissingCoveragePolicy>,
48
49 #[serde(default)]
51 pub exclude: Vec<String>,
52
53 #[serde(alias = "default_excludes")]
59 pub default_excludes: Option<Vec<String>>,
60
61 pub top: Option<usize>,
63
64 pub min: Option<f64>,
66
67 #[serde(default)]
71 pub allow: Vec<String>,
72
73 pub fail_regression: Option<bool>,
75
76 pub jobs: Option<usize>,
80
81 pub epsilon: Option<f64>,
85
86 pub sort: Option<SortOrder>,
89
90 #[serde(alias = "show_unchanged")]
94 pub show_unchanged: Option<bool>,
95}
96
97pub fn load(start: &Path) -> Result<Config> {
102 let mut dir = if start.is_file() {
103 start.parent().unwrap_or(start)
104 } else {
105 start
106 };
107
108 loop {
109 let candidate = dir.join(".cargo-crap.toml");
110 if candidate.exists() {
111 let raw = fs::read_to_string(&candidate)
112 .with_context(|| format!("reading {}", candidate.display()))?;
113 let cfg: Config =
114 toml::from_str(&raw).with_context(|| format!("parsing {}", candidate.display()))?;
115 return Ok(cfg);
116 }
117 match dir.parent() {
118 Some(p) => dir = p,
119 None => return Ok(Config::default()),
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use std::io::Write;
128
129 fn write_config(
130 dir: &Path,
131 content: &str,
132 ) {
133 let mut f = fs::File::create(dir.join(".cargo-crap.toml")).unwrap();
134 f.write_all(content.as_bytes()).unwrap();
135 }
136
137 #[test]
138 fn missing_config_returns_defaults() {
139 let dir = tempfile::tempdir().unwrap();
140 let cfg = load(dir.path()).unwrap();
141 assert!(cfg.threshold.is_none());
142 assert!(cfg.fail_above.is_none());
143 assert!(cfg.missing.is_none());
144 assert!(cfg.exclude.is_empty());
145 assert!(cfg.allow.is_empty());
146 }
147
148 #[test]
149 fn config_file_is_parsed() {
150 let dir = tempfile::tempdir().unwrap();
151 write_config(
152 dir.path(),
153 r#"
154threshold = 20.0
155fail-above = true
156missing = "optimistic"
157exclude = ["tests/**"]
158allow = ["Foo::*"]
159"#,
160 );
161 let cfg = load(dir.path()).unwrap();
162 assert_eq!(cfg.threshold, Some(20.0));
163 assert_eq!(cfg.fail_above, Some(true));
164 assert_eq!(cfg.missing, Some(MissingCoveragePolicy::Optimistic));
165 assert_eq!(cfg.exclude, ["tests/**"]);
166 assert_eq!(cfg.allow, ["Foo::*"]);
167 }
168
169 #[test]
170 fn default_excludes_absent_means_none() {
171 let dir = tempfile::tempdir().unwrap();
172 write_config(dir.path(), "threshold = 20.0\n");
173 let cfg = load(dir.path()).unwrap();
174 assert!(cfg.default_excludes.is_none());
175 }
176
177 #[test]
178 fn default_excludes_kebab_case_is_parsed() {
179 let dir = tempfile::tempdir().unwrap();
180 write_config(
181 dir.path(),
182 "default-excludes = [\"benches/**\", \"examples/**\"]\n",
183 );
184 let cfg = load(dir.path()).unwrap();
185 assert_eq!(
186 cfg.default_excludes.as_deref(),
187 Some(&["benches/**".to_string(), "examples/**".to_string()][..])
188 );
189 }
190
191 #[test]
192 fn default_excludes_snake_case_alias_is_parsed() {
193 let dir = tempfile::tempdir().unwrap();
196 write_config(dir.path(), "default_excludes = [\"tests/**\"]\n");
197 let cfg = load(dir.path()).unwrap();
198 assert_eq!(
199 cfg.default_excludes.as_deref(),
200 Some(&["tests/**".to_string()][..])
201 );
202 }
203
204 #[test]
205 fn default_excludes_empty_list_is_some_empty() {
206 let dir = tempfile::tempdir().unwrap();
209 write_config(dir.path(), "default-excludes = []\n");
210 let cfg = load(dir.path()).unwrap();
211 assert_eq!(cfg.default_excludes.as_deref(), Some(&[][..]));
212 }
213
214 #[test]
215 fn config_is_found_by_walking_up() {
216 let dir = tempfile::tempdir().unwrap();
217 write_config(dir.path(), "threshold = 15.0\n");
218 let subdir = dir.path().join("src");
219 fs::create_dir(&subdir).unwrap();
220 let cfg = load(&subdir).unwrap();
222 assert_eq!(cfg.threshold, Some(15.0));
223 }
224
225 #[test]
226 fn sort_and_show_unchanged_are_parsed() {
227 let dir = tempfile::tempdir().unwrap();
228 write_config(dir.path(), "sort = \"file\"\nshow_unchanged = true\n");
229 let cfg = load(dir.path()).unwrap();
230 assert_eq!(cfg.sort, Some(SortOrder::File));
231 assert_eq!(cfg.show_unchanged, Some(true));
232 }
233
234 #[test]
235 fn sort_and_show_unchanged_absent_means_none() {
236 let dir = tempfile::tempdir().unwrap();
237 write_config(dir.path(), "threshold = 20.0\n");
238 let cfg = load(dir.path()).unwrap();
239 assert!(cfg.sort.is_none());
240 assert!(cfg.show_unchanged.is_none());
241 }
242
243 #[test]
244 fn unknown_key_returns_error() {
245 let dir = tempfile::tempdir().unwrap();
246 write_config(dir.path(), "unknown-key = true\n");
247 let err = load(dir.path()).unwrap_err();
248 assert!(
249 err.to_string().contains("parsing"),
250 "expected parse error, got: {err}"
251 );
252 }
253}