nextest_runner/user_config/
imp.rs1use super::{
7 discovery::user_config_paths,
8 elements::{DefaultUiConfig, UiConfig},
9};
10use crate::errors::UserConfigError;
11use camino::Utf8Path;
12use serde::Deserialize;
13use std::collections::BTreeSet;
14use tracing::{debug, warn};
15
16trait UserConfigWarnings {
21 fn unknown_config_keys(&mut self, config_file: &Utf8Path, unknown: &BTreeSet<String>);
23}
24
25struct DefaultUserConfigWarnings;
28
29impl UserConfigWarnings for DefaultUserConfigWarnings {
30 fn unknown_config_keys(&mut self, config_file: &Utf8Path, unknown: &BTreeSet<String>) {
31 let mut unknown_str = String::new();
32 if unknown.len() == 1 {
33 unknown_str.push_str("key: ");
35 unknown_str.push_str(unknown.iter().next().unwrap());
36 } else {
37 unknown_str.push_str("keys:\n");
38 for ignored_key in unknown {
39 unknown_str.push('\n');
40 unknown_str.push_str(" - ");
41 unknown_str.push_str(ignored_key);
42 }
43 }
44
45 warn!(
46 "in user config file {}, ignoring unknown configuration {unknown_str}",
47 config_file,
48 );
49 }
50}
51
52#[derive(Clone, Debug, Default, Deserialize)]
57#[serde(rename_all = "kebab-case")]
58pub struct UserConfig {
59 #[serde(default)]
61 pub ui: UiConfig,
62}
63
64impl UserConfig {
65 pub fn from_default_location() -> Result<Option<Self>, UserConfigError> {
74 Self::from_default_location_with_warnings(&mut DefaultUserConfigWarnings)
75 }
76
77 fn from_default_location_with_warnings(
80 warnings: &mut impl UserConfigWarnings,
81 ) -> Result<Option<Self>, UserConfigError> {
82 let paths = user_config_paths()?;
83 if paths.is_empty() {
84 debug!("user config: could not determine config directory");
85 return Ok(None);
86 }
87
88 for path in &paths {
89 match Self::from_path_with_warnings(path, warnings)? {
90 Some(config) => return Ok(Some(config)),
91 None => continue,
92 }
93 }
94
95 debug!(
96 "user config: no config file found at any candidate path: {:?}",
97 paths
98 );
99 Ok(None)
100 }
101
102 pub fn from_path(path: &Utf8Path) -> Result<Option<Self>, UserConfigError> {
107 Self::from_path_with_warnings(path, &mut DefaultUserConfigWarnings)
108 }
109
110 fn from_path_with_warnings(
115 path: &Utf8Path,
116 warnings: &mut impl UserConfigWarnings,
117 ) -> Result<Option<Self>, UserConfigError> {
118 debug!("user config: attempting to load from {path}");
119 let contents = match std::fs::read_to_string(path) {
120 Ok(contents) => contents,
121 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
122 debug!("user config: file does not exist at {path}");
123 return Ok(None);
124 }
125 Err(error) => {
126 return Err(UserConfigError::Read {
127 path: path.to_owned(),
128 error,
129 });
130 }
131 };
132
133 let (config, unknown) =
134 Self::deserialize_toml(&contents).map_err(|error| UserConfigError::Parse {
135 path: path.to_owned(),
136 error,
137 })?;
138
139 if !unknown.is_empty() {
140 warnings.unknown_config_keys(path, &unknown);
141 }
142
143 debug!("user config: loaded successfully from {path}");
144 Ok(Some(config))
145 }
146
147 fn deserialize_toml(contents: &str) -> Result<(Self, BTreeSet<String>), toml::de::Error> {
149 let deserializer = toml::Deserializer::parse(contents)?;
150 let mut unknown = BTreeSet::new();
151 let config: UserConfig = serde_ignored::deserialize(deserializer, |path| {
152 unknown.insert(path.to_string());
153 })?;
154 Ok((config, unknown))
155 }
156}
157
158#[derive(Clone, Debug, Deserialize)]
162#[serde(rename_all = "kebab-case")]
163pub struct DefaultUserConfig {
164 pub ui: DefaultUiConfig,
166}
167
168impl DefaultUserConfig {
169 pub const DEFAULT_CONFIG: &'static str = include_str!("../../default-user-config.toml");
171
172 pub fn from_embedded() -> Self {
176 let deserializer = toml::Deserializer::parse(Self::DEFAULT_CONFIG)
177 .expect("embedded default user config should parse");
178 let mut unknown = BTreeSet::new();
179 let config: DefaultUserConfig =
180 serde_ignored::deserialize(deserializer, |path: serde_ignored::Path| {
181 unknown.insert(path.to_string());
182 })
183 .expect("embedded default user config should be valid");
184
185 if !unknown.is_empty() {
188 panic!(
189 "found unknown keys in default user config: {}",
190 unknown.into_iter().collect::<Vec<_>>().join(", ")
191 );
192 }
193
194 config
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use camino::Utf8PathBuf;
202 use camino_tempfile::tempdir;
203
204 #[derive(Default)]
206 struct TestUserConfigWarnings {
207 unknown_keys: Option<(Utf8PathBuf, BTreeSet<String>)>,
208 }
209
210 impl UserConfigWarnings for TestUserConfigWarnings {
211 fn unknown_config_keys(&mut self, config_file: &Utf8Path, unknown: &BTreeSet<String>) {
212 self.unknown_keys = Some((config_file.to_owned(), unknown.clone()));
213 }
214 }
215
216 #[test]
217 fn default_user_config_is_valid() {
218 let _ = DefaultUserConfig::from_embedded();
221 }
222
223 #[test]
224 fn ignored_keys() {
225 let config_contents = r#"
226 ignored1 = "test"
227
228 [ui]
229 show-progress = "bar"
230 ignored2 = "hi"
231 "#;
232
233 let temp_dir = tempdir().unwrap();
234 let config_path = temp_dir.path().join("config.toml");
235 std::fs::write(&config_path, config_contents).unwrap();
236
237 let mut warnings = TestUserConfigWarnings::default();
238 let config =
239 UserConfig::from_path_with_warnings(&config_path, &mut warnings).expect("config valid");
240
241 assert!(config.is_some(), "config should be loaded");
242 let config = config.unwrap();
243 assert!(
244 matches!(
245 config.ui.show_progress,
246 Some(crate::user_config::elements::UiShowProgress::Bar)
247 ),
248 "show-progress should be parsed correctly"
249 );
250
251 let (path, unknown) = warnings.unknown_keys.expect("should have unknown keys");
252 assert_eq!(path, config_path, "path should match");
253 assert_eq!(
254 unknown,
255 maplit::btreeset! {
256 "ignored1".to_owned(),
257 "ui.ignored2".to_owned(),
258 },
259 "unknown keys should be detected"
260 );
261 }
262
263 #[test]
264 fn no_ignored_keys() {
265 let config_contents = r#"
266 [ui]
267 show-progress = "counter"
268 max-progress-running = 10
269 input-handler = false
270 output-indent = true
271 "#;
272
273 let temp_dir = tempdir().unwrap();
274 let config_path = temp_dir.path().join("config.toml");
275 std::fs::write(&config_path, config_contents).unwrap();
276
277 let mut warnings = TestUserConfigWarnings::default();
278 let config =
279 UserConfig::from_path_with_warnings(&config_path, &mut warnings).expect("config valid");
280
281 assert!(config.is_some(), "config should be loaded");
282 assert!(
283 warnings.unknown_keys.is_none(),
284 "no unknown keys should be detected"
285 );
286 }
287}