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