Skip to main content

nextest_runner/user_config/elements/
ui.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! UI-related user configuration.
5
6use crate::{
7    reporter::{MaxProgressRunning, ShowProgress},
8    user_config::helpers::resolve_ui_setting,
9};
10use serde::{
11    Deserialize, Deserializer,
12    de::{self, Unexpected},
13};
14use std::{collections::BTreeMap, fmt, process::Command};
15use target_spec::{Platform, TargetSpec};
16
17/// UI-related configuration (deserialized form).
18///
19/// This section controls how nextest displays progress and output during test
20/// runs. All fields are optional; unspecified fields will use defaults.
21#[derive(Clone, Debug, Default, Deserialize)]
22#[serde(rename_all = "kebab-case")]
23pub(in crate::user_config) struct DeserializedUiConfig {
24    /// How to show progress during test runs.
25    ///
26    /// Accepts: `"auto"`, `"none"`, `"bar"`, `"counter"`, `"only"`.
27    pub(in crate::user_config) show_progress: Option<UiShowProgress>,
28
29    /// Maximum running tests to display in the progress bar.
30    ///
31    /// Accepts: an integer, or `"infinite"` for unlimited.
32    #[serde(default, deserialize_with = "deserialize_max_progress_running")]
33    max_progress_running: Option<MaxProgressRunning>,
34
35    /// Whether to enable the input handler.
36    input_handler: Option<bool>,
37
38    /// Whether to indent captured test output.
39    output_indent: Option<bool>,
40
41    /// Pager command for output that benefits from scrolling.
42    #[serde(default)]
43    pager: Option<PagerSetting>,
44
45    /// When to paginate output.
46    #[serde(default)]
47    paginate: Option<PaginateSetting>,
48
49    /// Configuration for the builtin streampager.
50    #[serde(default)]
51    streampager: DeserializedStreampagerConfig,
52}
53
54/// Default UI configuration with all values required.
55///
56/// This is parsed from the embedded default user config TOML. All fields are
57/// required - if the TOML is missing any field, parsing fails.
58#[derive(Clone, Debug, Deserialize)]
59#[serde(rename_all = "kebab-case")]
60pub(crate) struct DefaultUiConfig {
61    /// How to show progress during test runs.
62    show_progress: UiShowProgress,
63
64    /// Maximum running tests to display in the progress bar.
65    #[serde(deserialize_with = "deserialize_max_progress_running_required")]
66    max_progress_running: MaxProgressRunning,
67
68    /// Whether to enable the input handler.
69    input_handler: bool,
70
71    /// Whether to indent captured test output.
72    output_indent: bool,
73
74    /// Pager command for output that benefits from scrolling.
75    pub(in crate::user_config) pager: PagerSetting,
76
77    /// When to paginate output.
78    pub(in crate::user_config) paginate: PaginateSetting,
79
80    /// Configuration for the builtin streampager.
81    pub(in crate::user_config) streampager: DefaultStreampagerConfig,
82}
83
84/// Deserialized form of UI override settings.
85///
86/// Each field is optional; only the fields that are specified will override the
87/// base configuration.
88#[derive(Clone, Debug, Default, Deserialize)]
89#[serde(rename_all = "kebab-case")]
90pub(in crate::user_config) struct DeserializedUiOverrideData {
91    /// How to show progress during test runs.
92    pub(in crate::user_config) show_progress: Option<UiShowProgress>,
93
94    /// Maximum running tests to display in the progress bar.
95    #[serde(default, deserialize_with = "deserialize_max_progress_running")]
96    pub(in crate::user_config) max_progress_running: Option<MaxProgressRunning>,
97
98    /// Whether to enable the input handler.
99    pub(in crate::user_config) input_handler: Option<bool>,
100
101    /// Whether to indent captured test output.
102    pub(in crate::user_config) output_indent: Option<bool>,
103
104    /// Pager command for output that benefits from scrolling.
105    #[serde(default)]
106    pub(in crate::user_config) pager: Option<PagerSetting>,
107
108    /// When to paginate output.
109    #[serde(default)]
110    pub(in crate::user_config) paginate: Option<PaginateSetting>,
111
112    /// Configuration for the builtin streampager.
113    #[serde(default)]
114    pub(in crate::user_config) streampager: DeserializedStreampagerConfig,
115}
116
117/// A compiled UI override with parsed platform spec.
118///
119/// This is created after parsing the platform expression from a
120/// `[[overrides]]` entry.
121#[derive(Clone, Debug)]
122pub(in crate::user_config) struct CompiledUiOverride {
123    platform_spec: TargetSpec,
124    data: UiOverrideData,
125}
126
127impl CompiledUiOverride {
128    /// Creates a new compiled override from a platform spec and UI data.
129    pub(in crate::user_config) fn new(
130        platform_spec: TargetSpec,
131        data: DeserializedUiOverrideData,
132    ) -> Self {
133        Self {
134            platform_spec,
135            data: UiOverrideData {
136                show_progress: data.show_progress,
137                max_progress_running: data.max_progress_running,
138                input_handler: data.input_handler,
139                output_indent: data.output_indent,
140                pager: data.pager,
141                paginate: data.paginate,
142                streampager_interface: data.streampager.interface,
143                streampager_wrapping: data.streampager.wrapping,
144                streampager_show_ruler: data.streampager.show_ruler,
145            },
146        }
147    }
148
149    /// Checks if this override matches the given platform.
150    ///
151    /// Unknown results (e.g., unrecognized target features) are treated as
152    /// non-matching to be conservative.
153    pub(in crate::user_config) fn matches(&self, build_target: &Platform) -> bool {
154        self.platform_spec
155            .eval(build_target)
156            .unwrap_or(/* unknown results are mapped to false */ false)
157    }
158
159    /// Returns a reference to the override data.
160    pub(in crate::user_config) fn data(&self) -> &UiOverrideData {
161        &self.data
162    }
163}
164
165/// Override data for UI settings.
166#[derive(Clone, Debug, Default)]
167pub(in crate::user_config) struct UiOverrideData {
168    show_progress: Option<UiShowProgress>,
169    max_progress_running: Option<MaxProgressRunning>,
170    input_handler: Option<bool>,
171    output_indent: Option<bool>,
172    pager: Option<PagerSetting>,
173    paginate: Option<PaginateSetting>,
174    streampager_interface: Option<StreampagerInterface>,
175    streampager_wrapping: Option<StreampagerWrapping>,
176    streampager_show_ruler: Option<bool>,
177}
178
179impl UiOverrideData {
180    /// Returns the pager setting, if specified.
181    pub(in crate::user_config) fn pager(&self) -> Option<&PagerSetting> {
182        self.pager.as_ref()
183    }
184
185    /// Returns the paginate setting, if specified.
186    pub(in crate::user_config) fn paginate(&self) -> Option<&PaginateSetting> {
187        self.paginate.as_ref()
188    }
189
190    /// Returns the streampager interface, if specified.
191    pub(in crate::user_config) fn streampager_interface(&self) -> Option<&StreampagerInterface> {
192        self.streampager_interface.as_ref()
193    }
194
195    /// Returns the streampager wrapping, if specified.
196    pub(in crate::user_config) fn streampager_wrapping(&self) -> Option<&StreampagerWrapping> {
197        self.streampager_wrapping.as_ref()
198    }
199
200    /// Returns the streampager show-ruler setting, if specified.
201    pub(in crate::user_config) fn streampager_show_ruler(&self) -> Option<&bool> {
202        self.streampager_show_ruler.as_ref()
203    }
204}
205
206/// Resolved UI configuration after applying overrides.
207///
208/// This represents the final resolved settings after evaluating the base
209/// configuration and any matching platform-specific overrides.
210#[derive(Clone, Debug)]
211pub struct UiConfig {
212    /// How to show progress during test runs.
213    pub show_progress: UiShowProgress,
214    /// Maximum running tests to display in the progress bar.
215    pub max_progress_running: MaxProgressRunning,
216    /// Whether to enable the input handler.
217    pub input_handler: bool,
218    /// Whether to indent captured test output.
219    pub output_indent: bool,
220    /// Pager command for output that benefits from scrolling.
221    pub pager: PagerSetting,
222    /// When to paginate output.
223    pub paginate: PaginateSetting,
224    /// Configuration for the builtin streampager.
225    pub streampager: StreampagerConfig,
226}
227
228impl UiConfig {
229    /// Resolves UI configuration from user configs, defaults, and the build
230    /// target of the nextest binary.
231    ///
232    /// Resolution order (highest to lowest priority):
233    ///
234    /// 1. User overrides (first matching override for each setting)
235    /// 2. Default overrides (first matching override for each setting)
236    /// 3. User base config
237    /// 4. Default base config
238    ///
239    /// This matches the resolution order used by repo config.
240    pub(in crate::user_config) fn resolve(
241        default_config: &DefaultUiConfig,
242        default_overrides: &[CompiledUiOverride],
243        user_config: Option<&DeserializedUiConfig>,
244        user_overrides: &[CompiledUiOverride],
245        build_target: &Platform,
246    ) -> Self {
247        Self {
248            show_progress: resolve_ui_setting(
249                &default_config.show_progress,
250                default_overrides,
251                user_config.and_then(|c| c.show_progress.as_ref()),
252                user_overrides,
253                build_target,
254                |data| data.show_progress.as_ref(),
255            ),
256            max_progress_running: resolve_ui_setting(
257                &default_config.max_progress_running,
258                default_overrides,
259                user_config.and_then(|c| c.max_progress_running.as_ref()),
260                user_overrides,
261                build_target,
262                |data| data.max_progress_running.as_ref(),
263            ),
264            input_handler: resolve_ui_setting(
265                &default_config.input_handler,
266                default_overrides,
267                user_config.and_then(|c| c.input_handler.as_ref()),
268                user_overrides,
269                build_target,
270                |data| data.input_handler.as_ref(),
271            ),
272            output_indent: resolve_ui_setting(
273                &default_config.output_indent,
274                default_overrides,
275                user_config.and_then(|c| c.output_indent.as_ref()),
276                user_overrides,
277                build_target,
278                |data| data.output_indent.as_ref(),
279            ),
280            pager: resolve_ui_setting(
281                &default_config.pager,
282                default_overrides,
283                user_config.and_then(|c| c.pager.as_ref()),
284                user_overrides,
285                build_target,
286                |data| data.pager.as_ref(),
287            ),
288            paginate: resolve_ui_setting(
289                &default_config.paginate,
290                default_overrides,
291                user_config.and_then(|c| c.paginate.as_ref()),
292                user_overrides,
293                build_target,
294                |data| data.paginate.as_ref(),
295            ),
296            streampager: StreampagerConfig {
297                interface: resolve_ui_setting(
298                    &default_config.streampager.interface,
299                    default_overrides,
300                    user_config.and_then(|c| c.streampager.interface.as_ref()),
301                    user_overrides,
302                    build_target,
303                    |data| data.streampager_interface.as_ref(),
304                ),
305                wrapping: resolve_ui_setting(
306                    &default_config.streampager.wrapping,
307                    default_overrides,
308                    user_config.and_then(|c| c.streampager.wrapping.as_ref()),
309                    user_overrides,
310                    build_target,
311                    |data| data.streampager_wrapping.as_ref(),
312                ),
313                show_ruler: resolve_ui_setting(
314                    &default_config.streampager.show_ruler,
315                    default_overrides,
316                    user_config.and_then(|c| c.streampager.show_ruler.as_ref()),
317                    user_overrides,
318                    build_target,
319                    |data| data.streampager_show_ruler.as_ref(),
320                ),
321            },
322        }
323    }
324}
325
326/// Show progress setting for UI configuration.
327///
328/// This is a serde-friendly config enum, separate from [`ShowProgress`] (the
329/// runtime enum). `UiShowProgress` is deserialized from nextest profiles and
330/// then converted to `ShowProgress` for use at runtime.
331#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
332#[serde(rename_all = "kebab-case")]
333pub enum UiShowProgress {
334    /// Automatically choose based on terminal capabilities.
335    #[default]
336    Auto,
337    /// No progress display.
338    None,
339    /// Show a progress bar with running tests.
340    Bar,
341    /// Show a simple counter (e.g., "(1/10)").
342    Counter,
343    /// Like `Bar` in interactive terminals, but also hides successful test
344    /// output by defaulting to `status-level=slow` and
345    /// `final-status-level=none`. In non-interactive contexts (piped output,
346    /// CI), behaves identically to `Auto`: successful test output is shown
347    /// normally.
348    Only,
349}
350
351impl From<UiShowProgress> for ShowProgress {
352    fn from(ui: UiShowProgress) -> Self {
353        match ui {
354            UiShowProgress::Auto => ShowProgress::Auto {
355                suppress_success: false,
356            },
357            UiShowProgress::None => ShowProgress::None,
358            UiShowProgress::Bar => ShowProgress::Running,
359            UiShowProgress::Counter => ShowProgress::Counter,
360            UiShowProgress::Only => ShowProgress::Auto {
361                suppress_success: true,
362            },
363        }
364    }
365}
366
367/// Controls when to paginate output.
368#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
369#[serde(rename_all = "kebab-case")]
370pub enum PaginateSetting {
371    /// Automatically page if stdout is a TTY and output would benefit from it.
372    #[default]
373    Auto,
374    /// Never use a pager.
375    Never,
376}
377
378/// The special string that indicates the builtin pager should be used.
379pub const BUILTIN_PAGER_NAME: &str = ":builtin";
380
381/// Deserialized streampager configuration (all fields optional).
382///
383/// Used in user config and overrides where any field may be unspecified.
384#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
385#[serde(rename_all = "kebab-case")]
386pub(in crate::user_config) struct DeserializedStreampagerConfig {
387    /// Interface mode controlling alternate screen behavior.
388    pub(in crate::user_config) interface: Option<StreampagerInterface>,
389    /// Text wrapping mode.
390    pub(in crate::user_config) wrapping: Option<StreampagerWrapping>,
391    /// Whether to show a ruler at the bottom.
392    pub(in crate::user_config) show_ruler: Option<bool>,
393}
394
395/// Default streampager configuration (all fields required).
396///
397/// Used in the embedded default config.
398#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
399#[serde(rename_all = "kebab-case")]
400pub(in crate::user_config) struct DefaultStreampagerConfig {
401    /// Interface mode controlling alternate screen behavior.
402    pub(in crate::user_config) interface: StreampagerInterface,
403    /// Text wrapping mode.
404    pub(in crate::user_config) wrapping: StreampagerWrapping,
405    /// Whether to show a ruler at the bottom.
406    pub(in crate::user_config) show_ruler: bool,
407}
408
409/// Resolved streampager configuration.
410///
411/// These settings control behavior when `pager = ":builtin"` is configured.
412#[derive(Clone, Copy, Debug, PartialEq, Eq)]
413pub struct StreampagerConfig {
414    /// Interface mode controlling alternate screen behavior.
415    pub interface: StreampagerInterface,
416    /// Text wrapping mode.
417    pub wrapping: StreampagerWrapping,
418    /// Whether to show a ruler at the bottom.
419    pub show_ruler: bool,
420}
421
422impl StreampagerConfig {
423    /// Converts to the streampager library's interface mode.
424    pub fn streampager_interface_mode(&self) -> streampager::config::InterfaceMode {
425        use streampager::config::InterfaceMode;
426        match self.interface {
427            StreampagerInterface::FullScreenClearOutput => InterfaceMode::FullScreen,
428            StreampagerInterface::QuitIfOnePage => InterfaceMode::Hybrid,
429            StreampagerInterface::QuitQuicklyOrClearOutput => {
430                InterfaceMode::Delayed(std::time::Duration::from_secs(2))
431            }
432        }
433    }
434
435    /// Converts to the streampager library's wrapping mode.
436    pub fn streampager_wrapping_mode(&self) -> streampager::config::WrappingMode {
437        use streampager::config::WrappingMode;
438        match self.wrapping {
439            StreampagerWrapping::None => WrappingMode::Unwrapped,
440            StreampagerWrapping::Word => WrappingMode::WordBoundary,
441            StreampagerWrapping::Anywhere => WrappingMode::GraphemeBoundary,
442        }
443    }
444}
445
446/// Interface mode for the builtin streampager.
447///
448/// Controls how the pager uses the alternate screen and when it exits.
449#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
450#[serde(rename_all = "kebab-case")]
451pub enum StreampagerInterface {
452    /// Exit immediately if content fits on one page; otherwise use full screen
453    /// and clear on exit.
454    #[default]
455    QuitIfOnePage,
456    /// Always use full screen mode and clear the screen on exit.
457    FullScreenClearOutput,
458    /// Wait briefly before entering full screen; clear on exit if entered.
459    QuitQuicklyOrClearOutput,
460}
461
462/// Text wrapping mode for the builtin streampager.
463#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
464#[serde(rename_all = "kebab-case")]
465pub enum StreampagerWrapping {
466    /// Do not wrap text; allow horizontal scrolling.
467    None,
468    /// Wrap at word boundaries.
469    #[default]
470    Word,
471    /// Wrap at any character (grapheme) boundary.
472    Anywhere,
473}
474
475/// A command with optional arguments and environment variables.
476///
477/// Supports three input formats, all normalized to the same representation:
478///
479/// - String: `"less -FRX"` (split on whitespace)
480/// - Array: `["less", "-FRX"]`
481/// - Structured: `{ command = ["less", "-FRX"], env = { LESSCHARSET = "utf-8" } }`
482#[derive(Clone, Debug, PartialEq, Eq)]
483pub struct CommandNameAndArgs {
484    /// The command and its arguments (non-empty after deserialization).
485    command: Vec<String>,
486    /// Environment variables to set when running the command.
487    env: BTreeMap<String, String>,
488}
489
490impl CommandNameAndArgs {
491    /// Returns the command name.
492    pub fn command_name(&self) -> &str {
493        // The command is validated to be non-empty during deserialization.
494        &self.command[0]
495    }
496
497    /// Returns the arguments.
498    pub fn args(&self) -> &[String] {
499        &self.command[1..]
500    }
501
502    /// Creates a [`std::process::Command`] from this configuration.
503    pub fn to_command(&self) -> Command {
504        let mut cmd = Command::new(self.command_name());
505        cmd.args(self.args());
506        cmd.envs(&self.env);
507        cmd
508    }
509}
510
511impl<'de> Deserialize<'de> for CommandNameAndArgs {
512    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
513        deserializer.deserialize_any(CommandNameAndArgsVisitor)
514    }
515}
516
517/// Visitor for deserializing CommandNameAndArgs.
518struct CommandNameAndArgsVisitor;
519
520impl<'de> de::Visitor<'de> for CommandNameAndArgsVisitor {
521    type Value = CommandNameAndArgs;
522
523    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
524        formatter.write_str(
525            "a command string (\"less -FRX\"), \
526             an array ([\"less\", \"-FRX\"]), \
527             or a table ({ command = [\"less\", \"-FRX\"], env = { ... } })",
528        )
529    }
530
531    fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
532        let command: Vec<String> = shell_words::split(v).map_err(de::Error::custom)?;
533        if command.is_empty() {
534            return Err(de::Error::custom("command string must not be empty"));
535        }
536        Ok(CommandNameAndArgs {
537            command,
538            env: BTreeMap::new(),
539        })
540    }
541
542    fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
543        let mut command = Vec::new();
544        while let Some(arg) = seq.next_element::<String>()? {
545            command.push(arg);
546        }
547        if command.is_empty() {
548            return Err(de::Error::custom("command array must not be empty"));
549        }
550        Ok(CommandNameAndArgs {
551            command,
552            env: BTreeMap::new(),
553        })
554    }
555
556    fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
557        #[derive(Deserialize)]
558        struct StructuredInner {
559            command: Vec<String>,
560            #[serde(default)]
561            env: BTreeMap<String, String>,
562        }
563
564        let inner = StructuredInner::deserialize(de::value::MapAccessDeserializer::new(map))?;
565        if inner.command.is_empty() {
566            return Err(de::Error::custom("command array must not be empty"));
567        }
568        Ok(CommandNameAndArgs {
569            command: inner.command,
570            env: inner.env,
571        })
572    }
573}
574
575/// Controls which pager to use for output that benefits from scrolling.
576///
577/// This specifies *which* pager to use; whether to actually paginate is
578/// controlled by [`PaginateSetting`].
579#[derive(Clone, Debug, PartialEq, Eq)]
580pub enum PagerSetting {
581    /// Use the builtin streampager.
582    Builtin,
583    /// Use an external command.
584    External(CommandNameAndArgs),
585}
586
587// Only used in unit tests -- in regular code, the default is looked up via
588// default-user-config.toml.
589#[cfg(test)]
590impl Default for PagerSetting {
591    fn default() -> Self {
592        Self::External(CommandNameAndArgs {
593            command: vec!["less".to_owned(), "-FRX".to_owned()],
594            env: [("LESSCHARSET".to_owned(), "utf-8".to_owned())]
595                .into_iter()
596                .collect(),
597        })
598    }
599}
600
601impl<'de> Deserialize<'de> for PagerSetting {
602    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
603        deserializer.deserialize_any(PagerSettingVisitor)
604    }
605}
606
607/// Visitor for deserializing PagerSetting.
608struct PagerSettingVisitor;
609
610impl<'de> de::Visitor<'de> for PagerSettingVisitor {
611    type Value = PagerSetting;
612
613    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
614        formatter
615            .write_str("\":builtin\", a command string, an array, or a table with command and env")
616    }
617
618    fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
619        // Check for the special ":builtin" value.
620        if v == BUILTIN_PAGER_NAME {
621            return Ok(PagerSetting::Builtin);
622        }
623        let cmd = CommandNameAndArgsVisitor.visit_str(v)?;
624        Ok(PagerSetting::External(cmd))
625    }
626
627    fn visit_seq<A: de::SeqAccess<'de>>(self, seq: A) -> Result<Self::Value, A::Error> {
628        let args = CommandNameAndArgsVisitor.visit_seq(seq)?;
629        Ok(PagerSetting::External(args))
630    }
631
632    fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
633        let args = CommandNameAndArgsVisitor.visit_map(map)?;
634        Ok(PagerSetting::External(args))
635    }
636}
637
638/// Visitor for deserializing max-progress-running (string or integer).
639struct MaxProgressRunningVisitor;
640
641impl<'de> de::Visitor<'de> for MaxProgressRunningVisitor {
642    type Value = MaxProgressRunning;
643
644    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
645        formatter.write_str("a non-negative integer or \"infinite\"")
646    }
647
648    fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
649        Ok(MaxProgressRunning::Count(v as usize))
650    }
651
652    fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
653        if v < 0 {
654            Err(E::invalid_value(Unexpected::Signed(v), &self))
655        } else {
656            Ok(MaxProgressRunning::Count(v as usize))
657        }
658    }
659
660    fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
661        if v == "infinite" {
662            Ok(MaxProgressRunning::Infinite)
663        } else {
664            // Try parsing as a number.
665            v.parse::<usize>()
666                .map(MaxProgressRunning::Count)
667                .map_err(|_| E::invalid_value(Unexpected::Str(v), &self))
668        }
669    }
670}
671
672fn deserialize_max_progress_running<'de, D>(
673    deserializer: D,
674) -> Result<Option<MaxProgressRunning>, D::Error>
675where
676    D: Deserializer<'de>,
677{
678    deserializer.deserialize_option(OptionMaxProgressRunningVisitor)
679}
680
681/// Visitor for deserializing Option<MaxProgressRunning>.
682struct OptionMaxProgressRunningVisitor;
683
684impl<'de> de::Visitor<'de> for OptionMaxProgressRunningVisitor {
685    type Value = Option<MaxProgressRunning>;
686
687    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
688        formatter.write_str("a non-negative integer, \"infinite\", or null")
689    }
690
691    fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
692        Ok(None)
693    }
694
695    fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
696        deserializer
697            .deserialize_any(MaxProgressRunningVisitor)
698            .map(Some)
699    }
700
701    fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
702        Ok(None)
703    }
704}
705
706fn deserialize_max_progress_running_required<'de, D>(
707    deserializer: D,
708) -> Result<MaxProgressRunning, D::Error>
709where
710    D: Deserializer<'de>,
711{
712    deserializer.deserialize_any(MaxProgressRunningVisitor)
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718    use crate::user_config::DefaultUserConfig;
719
720    /// Helper to create a CompiledUiOverride for tests.
721    fn make_override(platform: &str, data: DeserializedUiOverrideData) -> CompiledUiOverride {
722        let platform_spec =
723            TargetSpec::new(platform.to_string()).expect("valid platform spec in test");
724        CompiledUiOverride::new(platform_spec, data)
725    }
726
727    #[test]
728    fn test_ui_config_show_progress() {
729        // Test valid values.
730        let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "auto""#).unwrap();
731        assert!(matches!(config.show_progress, Some(UiShowProgress::Auto)));
732
733        let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "none""#).unwrap();
734        assert!(matches!(config.show_progress, Some(UiShowProgress::None)));
735
736        let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "bar""#).unwrap();
737        assert!(matches!(config.show_progress, Some(UiShowProgress::Bar)));
738
739        let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "counter""#).unwrap();
740        assert!(matches!(
741            config.show_progress,
742            Some(UiShowProgress::Counter)
743        ));
744
745        let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "only""#).unwrap();
746        assert!(matches!(config.show_progress, Some(UiShowProgress::Only)));
747
748        // Test missing value.
749        let config: DeserializedUiConfig = toml::from_str("").unwrap();
750        assert!(config.show_progress.is_none());
751
752        // Test invalid value.
753        toml::from_str::<DeserializedUiConfig>(r#"show-progress = "invalid""#).unwrap_err();
754    }
755
756    #[test]
757    fn test_ui_show_progress_to_show_progress() {
758        // Test conversion to ShowProgress.
759        assert_eq!(
760            ShowProgress::from(UiShowProgress::Auto),
761            ShowProgress::Auto {
762                suppress_success: false
763            }
764        );
765        assert_eq!(ShowProgress::from(UiShowProgress::None), ShowProgress::None);
766        assert_eq!(
767            ShowProgress::from(UiShowProgress::Bar),
768            ShowProgress::Running
769        );
770        assert_eq!(
771            ShowProgress::from(UiShowProgress::Counter),
772            ShowProgress::Counter
773        );
774        // Only maps to Auto with suppress_success: the displayer handles hiding
775        // successful output when interactive.
776        assert_eq!(
777            ShowProgress::from(UiShowProgress::Only),
778            ShowProgress::Auto {
779                suppress_success: true
780            }
781        );
782    }
783
784    #[test]
785    fn test_ui_config_max_progress_running() {
786        // Test integer values.
787        let config: DeserializedUiConfig = toml::from_str("max-progress-running = 10").unwrap();
788        assert!(matches!(
789            config.max_progress_running,
790            Some(MaxProgressRunning::Count(10))
791        ));
792
793        let config: DeserializedUiConfig = toml::from_str("max-progress-running = 0").unwrap();
794        assert!(matches!(
795            config.max_progress_running,
796            Some(MaxProgressRunning::Count(0))
797        ));
798
799        // Test string "infinite".
800        let config: DeserializedUiConfig =
801            toml::from_str(r#"max-progress-running = "infinite""#).unwrap();
802        assert!(matches!(
803            config.max_progress_running,
804            Some(MaxProgressRunning::Infinite)
805        ));
806
807        // Test that matching is case-sensitive.
808        toml::from_str::<DeserializedUiConfig>(r#"max-progress-running = "INFINITE""#).unwrap_err();
809
810        // Test missing value.
811        let config: DeserializedUiConfig = toml::from_str("").unwrap();
812        assert!(config.max_progress_running.is_none());
813
814        // Test invalid value.
815        toml::from_str::<DeserializedUiConfig>(r#"max-progress-running = "invalid""#).unwrap_err();
816    }
817
818    #[test]
819    fn test_ui_config_input_handler() {
820        let config: DeserializedUiConfig = toml::from_str("input-handler = true").unwrap();
821        assert_eq!(config.input_handler, Some(true));
822        let config: DeserializedUiConfig = toml::from_str("input-handler = false").unwrap();
823        assert_eq!(config.input_handler, Some(false));
824        let config: DeserializedUiConfig = toml::from_str("").unwrap();
825        assert!(config.input_handler.is_none());
826    }
827
828    #[test]
829    fn test_ui_config_output_indent() {
830        let config: DeserializedUiConfig = toml::from_str("output-indent = true").unwrap();
831        assert_eq!(config.output_indent, Some(true));
832        let config: DeserializedUiConfig = toml::from_str("output-indent = false").unwrap();
833        assert_eq!(config.output_indent, Some(false));
834        let config: DeserializedUiConfig = toml::from_str("").unwrap();
835        assert!(config.output_indent.is_none());
836    }
837
838    #[test]
839    fn test_resolved_ui_config_defaults_only() {
840        let defaults = DefaultUserConfig::from_embedded().ui;
841
842        let build_target =
843            Platform::build_target().expect("nextest is built for a supported platform");
844        let resolved = UiConfig::resolve(&defaults, &[], None, &[], &build_target);
845
846        // Resolved values should match the embedded defaults.
847        assert_eq!(resolved.show_progress, defaults.show_progress);
848        assert_eq!(resolved.max_progress_running, defaults.max_progress_running);
849        assert_eq!(resolved.input_handler, defaults.input_handler);
850        assert_eq!(resolved.output_indent, defaults.output_indent);
851    }
852
853    #[test]
854    fn test_resolved_ui_config_user_config_overrides_defaults() {
855        let defaults = DefaultUserConfig::from_embedded().ui;
856
857        let user_config = DeserializedUiConfig {
858            show_progress: Some(UiShowProgress::Bar),
859            max_progress_running: Some(MaxProgressRunning::Count(4)),
860            output_indent: Some(false),
861            ..Default::default()
862        };
863
864        let build_target =
865            Platform::build_target().expect("nextest is built for a supported platform");
866        let resolved = UiConfig::resolve(&defaults, &[], Some(&user_config), &[], &build_target);
867
868        assert_eq!(resolved.show_progress, UiShowProgress::Bar);
869        assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
870        assert_eq!(resolved.input_handler, defaults.input_handler); // From defaults.
871        assert!(!resolved.output_indent);
872    }
873
874    #[test]
875    fn test_resolved_ui_config_user_override_applies() {
876        let defaults = DefaultUserConfig::from_embedded().ui;
877
878        // Create a user override that matches any platform.
879        let override_ = make_override(
880            "cfg(all())",
881            DeserializedUiOverrideData {
882                show_progress: Some(UiShowProgress::Counter),
883                input_handler: Some(false),
884                ..Default::default()
885            },
886        );
887
888        let build_target =
889            Platform::build_target().expect("nextest is built for a supported platform");
890        let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &build_target);
891
892        assert_eq!(resolved.show_progress, UiShowProgress::Counter);
893        assert_eq!(resolved.max_progress_running, defaults.max_progress_running); // From defaults.
894        assert!(!resolved.input_handler);
895        assert_eq!(resolved.output_indent, defaults.output_indent); // From defaults.
896    }
897
898    #[test]
899    fn test_resolved_ui_config_default_override_applies() {
900        let defaults = DefaultUserConfig::from_embedded().ui;
901
902        // Create a default override that matches any platform.
903        let override_ = make_override(
904            "cfg(all())",
905            DeserializedUiOverrideData {
906                show_progress: Some(UiShowProgress::Counter),
907                input_handler: Some(false),
908                ..Default::default()
909            },
910        );
911
912        let build_target =
913            Platform::build_target().expect("nextest is built for a supported platform");
914        let resolved = UiConfig::resolve(&defaults, &[override_], None, &[], &build_target);
915
916        assert_eq!(resolved.show_progress, UiShowProgress::Counter);
917        assert_eq!(resolved.max_progress_running, defaults.max_progress_running); // From defaults.
918        assert!(!resolved.input_handler);
919        assert_eq!(resolved.output_indent, defaults.output_indent); // From defaults.
920    }
921
922    #[test]
923    fn test_resolved_ui_config_platform_override_no_match() {
924        let defaults = DefaultUserConfig::from_embedded().ui;
925
926        // Create an override that never matches (cfg(any()) with no arguments
927        // is false).
928        let override_ = make_override(
929            "cfg(any())",
930            DeserializedUiOverrideData {
931                show_progress: Some(UiShowProgress::Counter),
932                max_progress_running: Some(MaxProgressRunning::Count(2)),
933                input_handler: Some(false),
934                output_indent: Some(false),
935                pager: Some(PagerSetting::default()),
936                paginate: Some(PaginateSetting::Never),
937                streampager: Default::default(),
938            },
939        );
940
941        let build_target =
942            Platform::build_target().expect("nextest is built for a supported platform");
943        let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &build_target);
944
945        // Nothing should be overridden - all values should match defaults.
946        assert_eq!(resolved.show_progress, defaults.show_progress);
947        assert_eq!(resolved.max_progress_running, defaults.max_progress_running);
948        assert_eq!(resolved.input_handler, defaults.input_handler);
949        assert_eq!(resolved.output_indent, defaults.output_indent);
950    }
951
952    #[test]
953    fn test_resolved_ui_config_first_matching_user_override_wins() {
954        let defaults = DefaultUserConfig::from_embedded().ui;
955
956        // Create two user overrides that both match (cfg(all()) is always true).
957        let override1 = make_override(
958            "cfg(all())",
959            DeserializedUiOverrideData {
960                show_progress: Some(UiShowProgress::Bar),
961                ..Default::default()
962            },
963        );
964
965        let override2 = make_override(
966            "cfg(all())",
967            DeserializedUiOverrideData {
968                show_progress: Some(UiShowProgress::Counter), // Should be ignored.
969                max_progress_running: Some(MaxProgressRunning::Count(4)),
970                ..Default::default()
971            },
972        );
973
974        let build_target =
975            Platform::build_target().expect("nextest is built for a supported platform");
976        let resolved =
977            UiConfig::resolve(&defaults, &[], None, &[override1, override2], &build_target);
978
979        // First override wins for show_progress.
980        assert_eq!(resolved.show_progress, UiShowProgress::Bar);
981        // Second override's max_progress_running applies (first didn't set it).
982        assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
983    }
984
985    #[test]
986    fn test_resolved_ui_config_user_override_beats_default_override() {
987        let defaults = DefaultUserConfig::from_embedded().ui;
988
989        // User override sets show_progress.
990        let user_override = make_override(
991            "cfg(all())",
992            DeserializedUiOverrideData {
993                show_progress: Some(UiShowProgress::Bar),
994                ..Default::default()
995            },
996        );
997
998        // Default override sets show_progress and max_progress_running.
999        let default_override = make_override(
1000            "cfg(all())",
1001            DeserializedUiOverrideData {
1002                show_progress: Some(UiShowProgress::Counter), // Should be ignored.
1003                max_progress_running: Some(MaxProgressRunning::Count(4)),
1004                ..Default::default()
1005            },
1006        );
1007
1008        let build_target =
1009            Platform::build_target().expect("nextest is built for a supported platform");
1010        let resolved = UiConfig::resolve(
1011            &defaults,
1012            &[default_override],
1013            None,
1014            &[user_override],
1015            &build_target,
1016        );
1017
1018        // User override wins for show_progress.
1019        assert_eq!(resolved.show_progress, UiShowProgress::Bar);
1020        // Default override applies for max_progress_running (user didn't set it).
1021        assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
1022    }
1023
1024    #[test]
1025    fn test_resolved_ui_config_override_beats_user_base() {
1026        let defaults = DefaultUserConfig::from_embedded().ui;
1027
1028        // User base config sets show_progress.
1029        let user_config = DeserializedUiConfig {
1030            show_progress: Some(UiShowProgress::None),
1031            max_progress_running: Some(MaxProgressRunning::Count(2)),
1032            ..Default::default()
1033        };
1034
1035        // Default override sets show_progress (should beat user base).
1036        let default_override = make_override(
1037            "cfg(all())",
1038            DeserializedUiOverrideData {
1039                show_progress: Some(UiShowProgress::Counter),
1040                ..Default::default()
1041            },
1042        );
1043
1044        let build_target =
1045            Platform::build_target().expect("nextest is built for a supported platform");
1046        let resolved = UiConfig::resolve(
1047            &defaults,
1048            &[default_override],
1049            Some(&user_config),
1050            &[],
1051            &build_target,
1052        );
1053
1054        // Default override is chosen over user base for show_progress.
1055        assert_eq!(resolved.show_progress, UiShowProgress::Counter);
1056        // User base applies for max_progress_running (override didn't set it).
1057        assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(2));
1058    }
1059
1060    #[test]
1061    fn test_paginate_setting_parsing() {
1062        // Test "auto".
1063        let config: DeserializedUiConfig = toml::from_str(r#"paginate = "auto""#).unwrap();
1064        assert_eq!(config.paginate, Some(PaginateSetting::Auto));
1065
1066        // Test "never".
1067        let config: DeserializedUiConfig = toml::from_str(r#"paginate = "never""#).unwrap();
1068        assert_eq!(config.paginate, Some(PaginateSetting::Never));
1069
1070        // Test missing value.
1071        let config: DeserializedUiConfig = toml::from_str("").unwrap();
1072        assert!(config.paginate.is_none());
1073
1074        // Test invalid value.
1075        let err = toml::from_str::<DeserializedUiConfig>(r#"paginate = "invalid""#).unwrap_err();
1076        assert!(
1077            err.to_string().contains("unknown variant"),
1078            "error should mention 'unknown variant': {err}"
1079        );
1080    }
1081
1082    #[test]
1083    fn test_command_name_and_args_parsing() {
1084        #[derive(Debug, Deserialize)]
1085        struct Wrapper {
1086            cmd: CommandNameAndArgs,
1087        }
1088
1089        // String format: split using shell word parsing.
1090        let wrapper: Wrapper = toml::from_str(r#"cmd = "less -FRX""#).unwrap();
1091        assert_eq!(
1092            wrapper.cmd,
1093            CommandNameAndArgs {
1094                command: vec!["less".to_owned(), "-FRX".to_owned()],
1095                env: BTreeMap::new(),
1096            }
1097        );
1098        assert_eq!(wrapper.cmd.command_name(), "less");
1099        assert_eq!(wrapper.cmd.args(), &["-FRX".to_owned()]);
1100
1101        // Array format: each element is a separate argument.
1102        let wrapper: Wrapper = toml::from_str(r#"cmd = ["less", "-F", "-R", "-X"]"#).unwrap();
1103        assert_eq!(
1104            wrapper.cmd,
1105            CommandNameAndArgs {
1106                command: vec![
1107                    "less".to_owned(),
1108                    "-F".to_owned(),
1109                    "-R".to_owned(),
1110                    "-X".to_owned()
1111                ],
1112                env: BTreeMap::new(),
1113            }
1114        );
1115        assert_eq!(wrapper.cmd.command_name(), "less");
1116        assert_eq!(
1117            wrapper.cmd.args(),
1118            &["-F".to_owned(), "-R".to_owned(), "-X".to_owned()]
1119        );
1120
1121        // Structured format: command array with optional env.
1122        let cmd: CommandNameAndArgs = toml::from_str(
1123            r#"
1124            command = ["less", "-FRX"]
1125            env = { LESSCHARSET = "utf-8" }
1126            "#,
1127        )
1128        .unwrap();
1129        let expected_env: BTreeMap<String, String> =
1130            [("LESSCHARSET".to_owned(), "utf-8".to_owned())]
1131                .into_iter()
1132                .collect();
1133        assert_eq!(
1134            cmd,
1135            CommandNameAndArgs {
1136                command: vec!["less".to_owned(), "-FRX".to_owned()],
1137                env: expected_env,
1138            }
1139        );
1140        assert_eq!(cmd.command_name(), "less");
1141        assert_eq!(cmd.args(), &["-FRX".to_owned()]);
1142
1143        // Shell quoting: double quotes preserve spaces.
1144        let wrapper: Wrapper = toml::from_str(r#"cmd = 'my-pager "arg with spaces"'"#).unwrap();
1145        assert_eq!(
1146            wrapper.cmd,
1147            CommandNameAndArgs {
1148                command: vec!["my-pager".to_owned(), "arg with spaces".to_owned()],
1149                env: BTreeMap::new(),
1150            }
1151        );
1152
1153        // Shell quoting: single quotes preserve spaces.
1154        let wrapper: Wrapper = toml::from_str(r#"cmd = "my-pager 'arg with spaces'""#).unwrap();
1155        assert_eq!(
1156            wrapper.cmd,
1157            CommandNameAndArgs {
1158                command: vec!["my-pager".to_owned(), "arg with spaces".to_owned()],
1159                env: BTreeMap::new(),
1160            }
1161        );
1162
1163        // Shell quoting: escaped quotes within double quotes.
1164        let wrapper: Wrapper =
1165            toml::from_str(r#"cmd = 'my-pager "quoted \"nested\" arg"'"#).unwrap();
1166        assert_eq!(
1167            wrapper.cmd,
1168            CommandNameAndArgs {
1169                command: vec!["my-pager".to_owned(), "quoted \"nested\" arg".to_owned()],
1170                env: BTreeMap::new(),
1171            }
1172        );
1173
1174        // Shell quoting: path with spaces.
1175        let wrapper: Wrapper = toml::from_str(r#"cmd = '"/path/to/my pager" --flag'"#).unwrap();
1176        assert_eq!(
1177            wrapper.cmd,
1178            CommandNameAndArgs {
1179                command: vec!["/path/to/my pager".to_owned(), "--flag".to_owned()],
1180                env: BTreeMap::new(),
1181            }
1182        );
1183
1184        // Shell quoting: multiple quoted arguments.
1185        let wrapper: Wrapper =
1186            toml::from_str(r#"cmd = 'cmd "first arg" "second arg" third'"#).unwrap();
1187        assert_eq!(
1188            wrapper.cmd,
1189            CommandNameAndArgs {
1190                command: vec![
1191                    "cmd".to_owned(),
1192                    "first arg".to_owned(),
1193                    "second arg".to_owned(),
1194                    "third".to_owned(),
1195                ],
1196                env: BTreeMap::new(),
1197            }
1198        );
1199    }
1200
1201    #[test]
1202    fn test_command_and_pager_empty_errors() {
1203        #[derive(Debug, Deserialize)]
1204        struct Wrapper {
1205            #[expect(dead_code)]
1206            cmd: CommandNameAndArgs,
1207        }
1208
1209        // Test CommandNameAndArgs empty cases.
1210        let cmd_cases = [
1211            ("empty array", "cmd = []"),
1212            ("empty string", r#"cmd = """#),
1213            ("whitespace-only string", r#"cmd = "   ""#),
1214            (
1215                "structured with empty command",
1216                r#"cmd = { command = [], env = { LESSCHARSET = "utf-8" } }"#,
1217            ),
1218        ];
1219
1220        for (name, input) in cmd_cases {
1221            let err = toml::from_str::<Wrapper>(input).unwrap_err();
1222            assert!(
1223                err.to_string().contains("must not be empty"),
1224                "CommandNameAndArgs {name}: error should mention 'must not be empty': {err}"
1225            );
1226        }
1227
1228        // Test PagerSetting empty cases (via DeserializedUiConfig).
1229        let pager_cases = [
1230            ("empty array", "pager = []"),
1231            ("empty string", r#"pager = """#),
1232        ];
1233
1234        for (name, input) in pager_cases {
1235            let err = toml::from_str::<DeserializedUiConfig>(input).unwrap_err();
1236            assert!(
1237                err.to_string().contains("must not be empty"),
1238                "PagerSetting {name}: error should mention 'must not be empty': {err}"
1239            );
1240        }
1241
1242        // Test invalid shell quoting (unclosed quotes).
1243        let unclosed_quote_cases = [
1244            ("unclosed double quote", r#"cmd = 'pager "unclosed'"#),
1245            ("unclosed single quote", r#"cmd = "pager 'unclosed""#),
1246        ];
1247
1248        for (name, input) in unclosed_quote_cases {
1249            let err = toml::from_str::<Wrapper>(input).unwrap_err();
1250            assert!(
1251                err.to_string().contains("missing closing quote"),
1252                "CommandNameAndArgs {name}: error should mention 'missing closing quote': {err}"
1253            );
1254        }
1255    }
1256
1257    #[test]
1258    fn test_command_name_and_args_to_command() {
1259        // Test that to_command produces a valid Command.
1260        let cmd = CommandNameAndArgs {
1261            command: vec!["echo".to_owned(), "hello".to_owned()],
1262            env: BTreeMap::new(),
1263        };
1264        let std_cmd = cmd.to_command();
1265        assert_eq!(cmd.command_name(), "echo");
1266        drop(std_cmd);
1267    }
1268
1269    #[test]
1270    fn test_pager_setting_parsing() {
1271        // String format.
1272        let config: DeserializedUiConfig = toml::from_str(r#"pager = "less -FRX""#).unwrap();
1273        assert_eq!(
1274            config.pager,
1275            Some(PagerSetting::External(CommandNameAndArgs {
1276                command: vec!["less".to_owned(), "-FRX".to_owned()],
1277                env: BTreeMap::new(),
1278            }))
1279        );
1280
1281        // Array format.
1282        let config: DeserializedUiConfig = toml::from_str(r#"pager = ["less", "-FRX"]"#).unwrap();
1283        assert_eq!(
1284            config.pager,
1285            Some(PagerSetting::External(CommandNameAndArgs {
1286                command: vec!["less".to_owned(), "-FRX".to_owned()],
1287                env: BTreeMap::new(),
1288            }))
1289        );
1290
1291        // Structured format with env.
1292        let config: DeserializedUiConfig = toml::from_str(
1293            r#"
1294            [pager]
1295            command = ["less", "-FRX"]
1296            env = { LESSCHARSET = "utf-8" }
1297            "#,
1298        )
1299        .unwrap();
1300        let expected_env: BTreeMap<String, String> =
1301            [("LESSCHARSET".to_owned(), "utf-8".to_owned())]
1302                .into_iter()
1303                .collect();
1304        assert_eq!(
1305            config.pager,
1306            Some(PagerSetting::External(CommandNameAndArgs {
1307                command: vec!["less".to_owned(), "-FRX".to_owned()],
1308                env: expected_env,
1309            }))
1310        );
1311
1312        // Missing pager (None).
1313        let config: DeserializedUiConfig = toml::from_str("").unwrap();
1314        assert!(config.pager.is_none());
1315    }
1316
1317    #[test]
1318    fn test_resolved_ui_config_pager_defaults() {
1319        let defaults = DefaultUserConfig::from_embedded().ui;
1320
1321        let build_target =
1322            Platform::build_target().expect("nextest is built for a supported platform");
1323        let resolved = UiConfig::resolve(&defaults, &[], None, &[], &build_target);
1324
1325        // Resolved values should match the embedded defaults.
1326        assert_eq!(resolved.pager, defaults.pager);
1327        assert_eq!(resolved.paginate, defaults.paginate);
1328    }
1329
1330    #[test]
1331    fn test_resolved_ui_config_pager_override() {
1332        let defaults = DefaultUserConfig::from_embedded().ui;
1333
1334        // Create an override that sets a custom pager.
1335        let custom_pager = PagerSetting::External(CommandNameAndArgs {
1336            command: vec!["more".to_owned()],
1337            env: BTreeMap::new(),
1338        });
1339        let override_ = make_override(
1340            "cfg(all())",
1341            DeserializedUiOverrideData {
1342                pager: Some(custom_pager.clone()),
1343                ..Default::default()
1344            },
1345        );
1346
1347        let build_target =
1348            Platform::build_target().expect("nextest is built for a supported platform");
1349        let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &build_target);
1350
1351        assert_eq!(resolved.pager, custom_pager);
1352        // paginate should still be from defaults.
1353        assert_eq!(resolved.paginate, defaults.paginate);
1354    }
1355
1356    #[test]
1357    fn test_resolved_ui_config_paginate_override() {
1358        let defaults = DefaultUserConfig::from_embedded().ui;
1359
1360        // Create an override that sets paginate to "never".
1361        let override_ = make_override(
1362            "cfg(all())",
1363            DeserializedUiOverrideData {
1364                paginate: Some(PaginateSetting::Never),
1365                ..Default::default()
1366            },
1367        );
1368
1369        let build_target =
1370            Platform::build_target().expect("nextest is built for a supported platform");
1371        let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &build_target);
1372
1373        assert_eq!(resolved.paginate, PaginateSetting::Never);
1374        // pager should still be from defaults.
1375        assert_eq!(resolved.pager, defaults.pager);
1376    }
1377
1378    #[test]
1379    fn test_pager_setting_builtin() {
1380        // `:builtin` special string.
1381        let config: DeserializedUiConfig = toml::from_str(r#"pager = ":builtin""#).unwrap();
1382        assert_eq!(config.pager, Some(PagerSetting::Builtin));
1383    }
1384
1385    #[test]
1386    fn test_streampager_config_parsing() {
1387        // Full config.
1388        let config: DeserializedUiConfig = toml::from_str(
1389            r#"
1390            [streampager]
1391            interface = "full-screen-clear-output"
1392            wrapping = "anywhere"
1393            show-ruler = false
1394            "#,
1395        )
1396        .unwrap();
1397        assert_eq!(
1398            config.streampager.interface,
1399            Some(StreampagerInterface::FullScreenClearOutput)
1400        );
1401        assert_eq!(
1402            config.streampager.wrapping,
1403            Some(StreampagerWrapping::Anywhere)
1404        );
1405        assert_eq!(config.streampager.show_ruler, Some(false));
1406
1407        // Partial config - unspecified fields are None.
1408        let config: DeserializedUiConfig = toml::from_str(
1409            r#"
1410            [streampager]
1411            interface = "quit-quickly-or-clear-output"
1412            "#,
1413        )
1414        .unwrap();
1415        assert_eq!(
1416            config.streampager.interface,
1417            Some(StreampagerInterface::QuitQuicklyOrClearOutput)
1418        );
1419        assert_eq!(config.streampager.wrapping, None);
1420        assert_eq!(config.streampager.show_ruler, None);
1421
1422        // Empty config - all fields are None.
1423        let config: DeserializedUiConfig = toml::from_str("").unwrap();
1424        assert_eq!(config.streampager.interface, None);
1425        assert_eq!(config.streampager.wrapping, None);
1426        assert_eq!(config.streampager.show_ruler, None);
1427    }
1428
1429    #[test]
1430    fn test_streampager_config_resolution() {
1431        let defaults = DefaultUserConfig::from_embedded().ui;
1432
1433        // Override just the interface.
1434        let override_ = make_override(
1435            "cfg(all())",
1436            DeserializedUiOverrideData {
1437                streampager: DeserializedStreampagerConfig {
1438                    interface: Some(StreampagerInterface::FullScreenClearOutput),
1439                    wrapping: None,
1440                    show_ruler: None,
1441                },
1442                ..Default::default()
1443            },
1444        );
1445
1446        let build_target =
1447            Platform::build_target().expect("nextest is built for a supported platform");
1448        let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &build_target);
1449
1450        // Interface should be overridden.
1451        assert_eq!(
1452            resolved.streampager.interface,
1453            StreampagerInterface::FullScreenClearOutput
1454        );
1455        // wrapping and show_ruler should be from defaults.
1456        assert_eq!(resolved.streampager.wrapping, defaults.streampager.wrapping);
1457        assert_eq!(
1458            resolved.streampager.show_ruler,
1459            defaults.streampager.show_ruler
1460        );
1461    }
1462}