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,
21};
22use camino::Utf8Path;
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) -> Self {
55 match Self::try_load(host_platform) {
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(host_platform: &Platform) -> Result<Self, EarlyConfigError> {
75 let default_config = DefaultUserConfig::from_embedded();
76
77 let paths = user_config_paths().map_err(EarlyConfigError::Discovery)?;
79
80 if paths.is_empty() {
81 debug!("early user config: no config directory found, using defaults");
82 return Ok(Self::resolve_from_defaults(&default_config, host_platform));
83 }
84
85 for path in &paths {
87 match EarlyDeserializedConfig::from_path(path) {
88 Ok(Some(user_config)) => {
89 debug!("early user config: loaded from {path}");
90 return Ok(Self::resolve(
91 &default_config,
92 Some(&user_config),
93 host_platform,
94 ));
95 }
96 Ok(None) => {
97 debug!("early user config: file not found at {path}");
98 continue;
99 }
100 Err(error) => {
101 warn!("early user config: error loading {path}: {error}");
103 continue;
104 }
105 }
106 }
107
108 debug!("early user config: no config file found, using defaults");
109 Ok(Self::resolve_from_defaults(&default_config, host_platform))
110 }
111
112 fn resolve_from_defaults(default_config: &DefaultUserConfig, host_platform: &Platform) -> Self {
114 Self::resolve(default_config, None, host_platform)
115 }
116
117 fn resolve(
119 default_config: &DefaultUserConfig,
120 user_config: Option<&EarlyDeserializedConfig>,
121 host_platform: &Platform,
122 ) -> Self {
123 let user_overrides: Vec<CompiledUiOverride> = user_config
125 .map(|c| {
126 c.overrides
127 .iter()
128 .filter_map(|o| {
129 match TargetSpec::new(o.platform.clone()) {
130 Ok(spec) => Some(CompiledUiOverride::new(spec, o.ui.clone())),
131 Err(error) => {
132 warn!(
134 "user config: invalid platform spec '{}': {error}",
135 o.platform
136 );
137 None
138 }
139 }
140 })
141 .collect()
142 })
143 .unwrap_or_default();
144
145 let pager = resolve_ui_setting(
147 &default_config.ui.pager,
148 &default_config.ui_overrides,
149 user_config.and_then(|c| c.ui.pager.as_ref()),
150 &user_overrides,
151 host_platform,
152 |data| data.pager(),
153 );
154
155 let paginate = resolve_ui_setting(
156 &default_config.ui.paginate,
157 &default_config.ui_overrides,
158 user_config.and_then(|c| c.ui.paginate.as_ref()),
159 &user_overrides,
160 host_platform,
161 |data| data.paginate(),
162 );
163
164 let streampager = StreampagerConfig {
165 interface: resolve_ui_setting(
166 &default_config.ui.streampager.interface,
167 &default_config.ui_overrides,
168 user_config.and_then(|c| c.ui.streampager_interface()),
169 &user_overrides,
170 host_platform,
171 |data| data.streampager_interface(),
172 ),
173 wrapping: resolve_ui_setting(
174 &default_config.ui.streampager.wrapping,
175 &default_config.ui_overrides,
176 user_config.and_then(|c| c.ui.streampager_wrapping()),
177 &user_overrides,
178 host_platform,
179 |data| data.streampager_wrapping(),
180 ),
181 show_ruler: resolve_ui_setting(
182 &default_config.ui.streampager.show_ruler,
183 &default_config.ui_overrides,
184 user_config.and_then(|c| c.ui.streampager_show_ruler()),
185 &user_overrides,
186 host_platform,
187 |data| data.streampager_show_ruler(),
188 ),
189 };
190
191 Self {
192 pager,
193 paginate,
194 streampager,
195 }
196 }
197}
198
199#[derive(Debug)]
203enum EarlyConfigError {
204 Discovery(crate::errors::UserConfigError),
205 Read(std::io::Error),
206 Parse(toml::de::Error),
207}
208
209impl fmt::Display for EarlyConfigError {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 match self {
212 Self::Discovery(e) => write!(f, "config discovery: {e}"),
213 Self::Read(e) => write!(f, "read: {e}"),
214 Self::Parse(e) => write!(f, "parse: {e}"),
215 }
216 }
217}
218
219#[derive(Clone, Debug, Default, Deserialize)]
224#[serde(rename_all = "kebab-case")]
225struct EarlyDeserializedConfig {
226 #[serde(default)]
227 ui: EarlyDeserializedUiConfig,
228 #[serde(default)]
229 overrides: Vec<EarlyDeserializedOverride>,
230}
231
232impl EarlyDeserializedConfig {
233 fn from_path(path: &Utf8Path) -> Result<Option<Self>, EarlyConfigError> {
237 let contents = match std::fs::read_to_string(path) {
238 Ok(c) => c,
239 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
240 Err(e) => return Err(EarlyConfigError::Read(e)),
241 };
242
243 let config: Self = toml::from_str(&contents).map_err(EarlyConfigError::Parse)?;
244 Ok(Some(config))
245 }
246}
247
248#[derive(Clone, Debug, Default, Deserialize)]
250#[serde(rename_all = "kebab-case")]
251struct EarlyDeserializedUiConfig {
252 #[serde(default)]
253 pager: Option<PagerSetting>,
254 #[serde(default)]
255 paginate: Option<PaginateSetting>,
256 #[serde(default, rename = "streampager")]
258 streampager_section: EarlyDeserializedStreampagerConfig,
259}
260
261impl EarlyDeserializedUiConfig {
262 fn streampager_interface(&self) -> Option<&StreampagerInterface> {
263 self.streampager_section.interface.as_ref()
264 }
265
266 fn streampager_wrapping(&self) -> Option<&StreampagerWrapping> {
267 self.streampager_section.wrapping.as_ref()
268 }
269
270 fn streampager_show_ruler(&self) -> Option<&bool> {
271 self.streampager_section.show_ruler.as_ref()
272 }
273}
274
275#[derive(Clone, Debug, Default, Deserialize)]
277#[serde(rename_all = "kebab-case")]
278struct EarlyDeserializedStreampagerConfig {
279 #[serde(default)]
280 interface: Option<StreampagerInterface>,
281 #[serde(default)]
282 wrapping: Option<StreampagerWrapping>,
283 #[serde(default)]
284 show_ruler: Option<bool>,
285}
286
287#[derive(Clone, Debug, Deserialize)]
289#[serde(rename_all = "kebab-case")]
290struct EarlyDeserializedOverride {
291 platform: String,
292 #[serde(default)]
293 ui: DeserializedUiOverrideData,
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use crate::platform::detect_host_platform_for_tests;
300
301 #[test]
302 fn test_early_user_config_defaults() {
303 let host = detect_host_platform_for_tests();
304 let config = EarlyUserConfig::defaults(&host);
305
306 match &config.pager {
308 PagerSetting::Builtin => {}
309 PagerSetting::External(cmd) => {
310 assert!(!cmd.command_name().is_empty());
311 }
312 }
313
314 assert_eq!(config.paginate, PaginateSetting::Auto);
316 }
317
318 #[test]
319 fn test_early_user_config_from_host_platform() {
320 let host = detect_host_platform_for_tests();
321
322 let config = EarlyUserConfig::for_platform(&host);
324
325 match &config.pager {
327 PagerSetting::Builtin => {}
328 PagerSetting::External(cmd) => {
329 assert!(!cmd.command_name().is_empty());
330 }
331 }
332 }
333}