nextest_runner/user_config/
early.rs1use super::{
14 discovery::user_config_paths,
15 elements::{
16 CompiledUiOverride, DeserializedUiOverrideData, PagerSetting, PaginateSetting,
17 StreampagerConfig, StreampagerInterface, StreampagerWrapping,
18 },
19 helpers::resolve_ui_setting,
20 imp::{DefaultUserConfig, UserConfigLocation},
21};
22use camino::{Utf8Path, Utf8PathBuf};
23use serde::Deserialize;
24use std::{fmt, io};
25use target_spec::{Platform, TargetSpec};
26use tracing::{debug, warn};
27
28#[derive(Clone, Debug)]
37pub struct EarlyUserConfig {
38 pub pager: PagerSetting,
40 pub paginate: PaginateSetting,
42 pub streampager: StreampagerConfig,
44}
45
46impl EarlyUserConfig {
47 pub fn load(location: UserConfigLocation<'_>) -> Self {
59 let build_target =
60 Platform::build_target().expect("nextest is built for a supported platform");
61 match Self::try_load(&build_target, location) {
62 Ok(config) => config,
63 Err(error) => {
64 warn!(
65 "failed to load user config for pager settings, using defaults: {}",
66 error
67 );
68 Self::defaults(&build_target)
69 }
70 }
71 }
72
73 fn defaults(build_target: &Platform) -> Self {
75 let default_config = DefaultUserConfig::from_embedded();
76 Self::resolve_from_defaults(&default_config, build_target)
77 }
78
79 fn try_load(
81 build_target: &Platform,
82 location: UserConfigLocation<'_>,
83 ) -> Result<Self, EarlyConfigError> {
84 let default_config = DefaultUserConfig::from_embedded();
85
86 match location {
87 UserConfigLocation::Isolated => {
88 debug!("early user config: skipping (isolated)");
89 Ok(Self::resolve_from_defaults(&default_config, build_target))
90 }
91 UserConfigLocation::Explicit(path) => {
92 debug!("early user config: loading from explicit path {path}");
93 match EarlyDeserializedConfig::from_path(path) {
94 Ok(Some(user_config)) => {
95 debug!("early user config: loaded from {path}");
96 Ok(Self::resolve(
97 &default_config,
98 Some(&user_config),
99 build_target,
100 ))
101 }
102 Ok(None) => Err(EarlyConfigError::FileNotFound(path.to_owned())),
103 Err(error) => Err(error),
104 }
105 }
106 UserConfigLocation::Default => {
107 Self::try_load_from_default_locations(&default_config, build_target)
108 }
109 }
110 }
111
112 fn try_load_from_default_locations(
114 default_config: &DefaultUserConfig,
115 build_target: &Platform,
116 ) -> Result<Self, EarlyConfigError> {
117 let paths = user_config_paths().map_err(EarlyConfigError::Discovery)?;
118
119 if paths.is_empty() {
120 debug!("early user config: no config directory found, using defaults");
121 return Ok(Self::resolve_from_defaults(default_config, build_target));
122 }
123
124 for path in &paths {
126 match EarlyDeserializedConfig::from_path(path) {
127 Ok(Some(user_config)) => {
128 debug!("early user config: loaded from {path}");
129 return Ok(Self::resolve(
130 default_config,
131 Some(&user_config),
132 build_target,
133 ));
134 }
135 Ok(None) => {
136 debug!("early user config: file not found at {path}");
137 continue;
138 }
139 Err(error) => {
140 warn!("early user config: error loading {path}: {error}");
142 continue;
143 }
144 }
145 }
146
147 debug!("early user config: no config file found, using defaults");
148 Ok(Self::resolve_from_defaults(default_config, build_target))
149 }
150
151 fn resolve_from_defaults(default_config: &DefaultUserConfig, build_target: &Platform) -> Self {
153 Self::resolve(default_config, None, build_target)
154 }
155
156 fn resolve(
158 default_config: &DefaultUserConfig,
159 user_config: Option<&EarlyDeserializedConfig>,
160 build_target: &Platform,
161 ) -> Self {
162 let user_overrides: Vec<CompiledUiOverride> = user_config
164 .map(|c| {
165 c.overrides
166 .iter()
167 .filter_map(|o| {
168 match TargetSpec::new(o.platform.clone()) {
169 Ok(spec) => Some(CompiledUiOverride::new(spec, o.ui.clone())),
170 Err(error) => {
171 warn!(
173 "user config: invalid platform spec '{}': {error}",
174 o.platform
175 );
176 None
177 }
178 }
179 })
180 .collect()
181 })
182 .unwrap_or_default();
183
184 let pager = resolve_ui_setting(
186 &default_config.ui.pager,
187 &default_config.ui_overrides,
188 user_config.and_then(|c| c.ui.pager.as_ref()),
189 &user_overrides,
190 build_target,
191 |data| data.pager(),
192 );
193
194 let paginate = resolve_ui_setting(
195 &default_config.ui.paginate,
196 &default_config.ui_overrides,
197 user_config.and_then(|c| c.ui.paginate.as_ref()),
198 &user_overrides,
199 build_target,
200 |data| data.paginate(),
201 );
202
203 let streampager = StreampagerConfig {
204 interface: resolve_ui_setting(
205 &default_config.ui.streampager.interface,
206 &default_config.ui_overrides,
207 user_config.and_then(|c| c.ui.streampager_interface()),
208 &user_overrides,
209 build_target,
210 |data| data.streampager_interface(),
211 ),
212 wrapping: resolve_ui_setting(
213 &default_config.ui.streampager.wrapping,
214 &default_config.ui_overrides,
215 user_config.and_then(|c| c.ui.streampager_wrapping()),
216 &user_overrides,
217 build_target,
218 |data| data.streampager_wrapping(),
219 ),
220 show_ruler: resolve_ui_setting(
221 &default_config.ui.streampager.show_ruler,
222 &default_config.ui_overrides,
223 user_config.and_then(|c| c.ui.streampager_show_ruler()),
224 &user_overrides,
225 build_target,
226 |data| data.streampager_show_ruler(),
227 ),
228 };
229
230 Self {
231 pager,
232 paginate,
233 streampager,
234 }
235 }
236}
237
238#[derive(Debug)]
242enum EarlyConfigError {
243 Discovery(crate::errors::UserConfigError),
244 FileNotFound(Utf8PathBuf),
246 Read(std::io::Error),
247 Parse(toml::de::Error),
248}
249
250impl fmt::Display for EarlyConfigError {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 match self {
253 Self::Discovery(e) => write!(f, "config discovery: {e}"),
254 Self::FileNotFound(path) => write!(f, "config file not found at {path}"),
255 Self::Read(e) => write!(f, "read: {e}"),
256 Self::Parse(e) => write!(f, "parse: {e}"),
257 }
258 }
259}
260
261#[derive(Clone, Debug, Default, Deserialize)]
266#[serde(rename_all = "kebab-case")]
267struct EarlyDeserializedConfig {
268 #[serde(default)]
269 ui: EarlyDeserializedUiConfig,
270 #[serde(default)]
271 overrides: Vec<EarlyDeserializedOverride>,
272}
273
274impl EarlyDeserializedConfig {
275 fn from_path(path: &Utf8Path) -> Result<Option<Self>, EarlyConfigError> {
279 let contents = match std::fs::read_to_string(path) {
280 Ok(c) => c,
281 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
282 Err(e) => return Err(EarlyConfigError::Read(e)),
283 };
284
285 let config: Self = toml::from_str(&contents).map_err(EarlyConfigError::Parse)?;
286 Ok(Some(config))
287 }
288}
289
290#[derive(Clone, Debug, Default, Deserialize)]
292#[serde(rename_all = "kebab-case")]
293struct EarlyDeserializedUiConfig {
294 #[serde(default)]
295 pager: Option<PagerSetting>,
296 #[serde(default)]
297 paginate: Option<PaginateSetting>,
298 #[serde(default, rename = "streampager")]
300 streampager_section: EarlyDeserializedStreampagerConfig,
301}
302
303impl EarlyDeserializedUiConfig {
304 fn streampager_interface(&self) -> Option<&StreampagerInterface> {
305 self.streampager_section.interface.as_ref()
306 }
307
308 fn streampager_wrapping(&self) -> Option<&StreampagerWrapping> {
309 self.streampager_section.wrapping.as_ref()
310 }
311
312 fn streampager_show_ruler(&self) -> Option<&bool> {
313 self.streampager_section.show_ruler.as_ref()
314 }
315}
316
317#[derive(Clone, Debug, Default, Deserialize)]
319#[serde(rename_all = "kebab-case")]
320struct EarlyDeserializedStreampagerConfig {
321 #[serde(default)]
322 interface: Option<StreampagerInterface>,
323 #[serde(default)]
324 wrapping: Option<StreampagerWrapping>,
325 #[serde(default)]
326 show_ruler: Option<bool>,
327}
328
329#[derive(Clone, Debug, Deserialize)]
331#[serde(rename_all = "kebab-case")]
332struct EarlyDeserializedOverride {
333 platform: String,
334 #[serde(default)]
335 ui: DeserializedUiOverrideData,
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_early_user_config_defaults() {
344 let build_target =
345 Platform::build_target().expect("nextest is built for a supported platform");
346 let config = EarlyUserConfig::defaults(&build_target);
347
348 match &config.pager {
350 PagerSetting::Builtin => {}
351 PagerSetting::External(cmd) => {
352 assert!(!cmd.command_name().is_empty());
353 }
354 }
355
356 assert_eq!(config.paginate, PaginateSetting::Auto);
358 }
359
360 #[test]
361 fn test_early_user_config_load() {
362 let config = EarlyUserConfig::load(UserConfigLocation::Default);
364
365 match &config.pager {
367 PagerSetting::Builtin => {}
368 PagerSetting::External(cmd) => {
369 assert!(!cmd.command_name().is_empty());
370 }
371 }
372 }
373}