1use 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#[derive(Clone, Debug, Default, Deserialize)]
22#[serde(rename_all = "kebab-case")]
23pub(in crate::user_config) struct DeserializedUiConfig {
24 pub(in crate::user_config) show_progress: Option<UiShowProgress>,
28
29 #[serde(default, deserialize_with = "deserialize_max_progress_running")]
33 max_progress_running: Option<MaxProgressRunning>,
34
35 input_handler: Option<bool>,
37
38 output_indent: Option<bool>,
40
41 #[serde(default)]
43 pager: Option<PagerSetting>,
44
45 #[serde(default)]
47 paginate: Option<PaginateSetting>,
48
49 #[serde(default)]
51 streampager: DeserializedStreampagerConfig,
52}
53
54#[derive(Clone, Debug, Deserialize)]
59#[serde(rename_all = "kebab-case")]
60pub(crate) struct DefaultUiConfig {
61 show_progress: UiShowProgress,
63
64 #[serde(deserialize_with = "deserialize_max_progress_running_required")]
66 max_progress_running: MaxProgressRunning,
67
68 input_handler: bool,
70
71 output_indent: bool,
73
74 pub(in crate::user_config) pager: PagerSetting,
76
77 pub(in crate::user_config) paginate: PaginateSetting,
79
80 pub(in crate::user_config) streampager: DefaultStreampagerConfig,
82}
83
84#[derive(Clone, Debug, Default, Deserialize)]
89#[serde(rename_all = "kebab-case")]
90pub(in crate::user_config) struct DeserializedUiOverrideData {
91 pub(in crate::user_config) show_progress: Option<UiShowProgress>,
93
94 #[serde(default, deserialize_with = "deserialize_max_progress_running")]
96 pub(in crate::user_config) max_progress_running: Option<MaxProgressRunning>,
97
98 pub(in crate::user_config) input_handler: Option<bool>,
100
101 pub(in crate::user_config) output_indent: Option<bool>,
103
104 #[serde(default)]
106 pub(in crate::user_config) pager: Option<PagerSetting>,
107
108 #[serde(default)]
110 pub(in crate::user_config) paginate: Option<PaginateSetting>,
111
112 #[serde(default)]
114 pub(in crate::user_config) streampager: DeserializedStreampagerConfig,
115}
116
117#[derive(Clone, Debug)]
122pub(in crate::user_config) struct CompiledUiOverride {
123 platform_spec: TargetSpec,
124 data: UiOverrideData,
125}
126
127impl CompiledUiOverride {
128 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 pub(in crate::user_config) fn matches(&self, build_target: &Platform) -> bool {
154 self.platform_spec
155 .eval(build_target)
156 .unwrap_or(false)
157 }
158
159 pub(in crate::user_config) fn data(&self) -> &UiOverrideData {
161 &self.data
162 }
163}
164
165#[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 pub(in crate::user_config) fn pager(&self) -> Option<&PagerSetting> {
182 self.pager.as_ref()
183 }
184
185 pub(in crate::user_config) fn paginate(&self) -> Option<&PaginateSetting> {
187 self.paginate.as_ref()
188 }
189
190 pub(in crate::user_config) fn streampager_interface(&self) -> Option<&StreampagerInterface> {
192 self.streampager_interface.as_ref()
193 }
194
195 pub(in crate::user_config) fn streampager_wrapping(&self) -> Option<&StreampagerWrapping> {
197 self.streampager_wrapping.as_ref()
198 }
199
200 pub(in crate::user_config) fn streampager_show_ruler(&self) -> Option<&bool> {
202 self.streampager_show_ruler.as_ref()
203 }
204}
205
206#[derive(Clone, Debug)]
211pub struct UiConfig {
212 pub show_progress: UiShowProgress,
214 pub max_progress_running: MaxProgressRunning,
216 pub input_handler: bool,
218 pub output_indent: bool,
220 pub pager: PagerSetting,
222 pub paginate: PaginateSetting,
224 pub streampager: StreampagerConfig,
226}
227
228impl UiConfig {
229 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#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
332#[serde(rename_all = "kebab-case")]
333pub enum UiShowProgress {
334 #[default]
336 Auto,
337 None,
339 Bar,
341 Counter,
343 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#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
369#[serde(rename_all = "kebab-case")]
370pub enum PaginateSetting {
371 #[default]
373 Auto,
374 Never,
376}
377
378pub const BUILTIN_PAGER_NAME: &str = ":builtin";
380
381#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
385#[serde(rename_all = "kebab-case")]
386pub(in crate::user_config) struct DeserializedStreampagerConfig {
387 pub(in crate::user_config) interface: Option<StreampagerInterface>,
389 pub(in crate::user_config) wrapping: Option<StreampagerWrapping>,
391 pub(in crate::user_config) show_ruler: Option<bool>,
393}
394
395#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
399#[serde(rename_all = "kebab-case")]
400pub(in crate::user_config) struct DefaultStreampagerConfig {
401 pub(in crate::user_config) interface: StreampagerInterface,
403 pub(in crate::user_config) wrapping: StreampagerWrapping,
405 pub(in crate::user_config) show_ruler: bool,
407}
408
409#[derive(Clone, Copy, Debug, PartialEq, Eq)]
413pub struct StreampagerConfig {
414 pub interface: StreampagerInterface,
416 pub wrapping: StreampagerWrapping,
418 pub show_ruler: bool,
420}
421
422impl StreampagerConfig {
423 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 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
450#[serde(rename_all = "kebab-case")]
451pub enum StreampagerInterface {
452 #[default]
455 QuitIfOnePage,
456 FullScreenClearOutput,
458 QuitQuicklyOrClearOutput,
460}
461
462#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
464#[serde(rename_all = "kebab-case")]
465pub enum StreampagerWrapping {
466 None,
468 #[default]
470 Word,
471 Anywhere,
473}
474
475#[derive(Clone, Debug, PartialEq, Eq)]
483pub struct CommandNameAndArgs {
484 command: Vec<String>,
486 env: BTreeMap<String, String>,
488}
489
490impl CommandNameAndArgs {
491 pub fn command_name(&self) -> &str {
493 &self.command[0]
495 }
496
497 pub fn args(&self) -> &[String] {
499 &self.command[1..]
500 }
501
502 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
517struct 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#[derive(Clone, Debug, PartialEq, Eq)]
580pub enum PagerSetting {
581 Builtin,
583 External(CommandNameAndArgs),
585}
586
587#[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
607struct 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 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
638struct 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 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
681struct 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 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 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 let config: DeserializedUiConfig = toml::from_str("").unwrap();
750 assert!(config.show_progress.is_none());
751
752 toml::from_str::<DeserializedUiConfig>(r#"show-progress = "invalid""#).unwrap_err();
754 }
755
756 #[test]
757 fn test_ui_show_progress_to_show_progress() {
758 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 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 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 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 toml::from_str::<DeserializedUiConfig>(r#"max-progress-running = "INFINITE""#).unwrap_err();
809
810 let config: DeserializedUiConfig = toml::from_str("").unwrap();
812 assert!(config.max_progress_running.is_none());
813
814 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 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); 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 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); assert!(!resolved.input_handler);
895 assert_eq!(resolved.output_indent, defaults.output_indent); }
897
898 #[test]
899 fn test_resolved_ui_config_default_override_applies() {
900 let defaults = DefaultUserConfig::from_embedded().ui;
901
902 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); assert!(!resolved.input_handler);
919 assert_eq!(resolved.output_indent, defaults.output_indent); }
921
922 #[test]
923 fn test_resolved_ui_config_platform_override_no_match() {
924 let defaults = DefaultUserConfig::from_embedded().ui;
925
926 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 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 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), 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 assert_eq!(resolved.show_progress, UiShowProgress::Bar);
981 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 let user_override = make_override(
991 "cfg(all())",
992 DeserializedUiOverrideData {
993 show_progress: Some(UiShowProgress::Bar),
994 ..Default::default()
995 },
996 );
997
998 let default_override = make_override(
1000 "cfg(all())",
1001 DeserializedUiOverrideData {
1002 show_progress: Some(UiShowProgress::Counter), 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 assert_eq!(resolved.show_progress, UiShowProgress::Bar);
1020 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 let user_config = DeserializedUiConfig {
1030 show_progress: Some(UiShowProgress::None),
1031 max_progress_running: Some(MaxProgressRunning::Count(2)),
1032 ..Default::default()
1033 };
1034
1035 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 assert_eq!(resolved.show_progress, UiShowProgress::Counter);
1056 assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(2));
1058 }
1059
1060 #[test]
1061 fn test_paginate_setting_parsing() {
1062 let config: DeserializedUiConfig = toml::from_str(r#"paginate = "auto""#).unwrap();
1064 assert_eq!(config.paginate, Some(PaginateSetting::Auto));
1065
1066 let config: DeserializedUiConfig = toml::from_str(r#"paginate = "never""#).unwrap();
1068 assert_eq!(config.paginate, Some(PaginateSetting::Never));
1069
1070 let config: DeserializedUiConfig = toml::from_str("").unwrap();
1072 assert!(config.paginate.is_none());
1073
1074 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(resolved.pager, defaults.pager);
1376 }
1377
1378 #[test]
1379 fn test_pager_setting_builtin() {
1380 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 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 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 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 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 assert_eq!(
1452 resolved.streampager.interface,
1453 StreampagerInterface::FullScreenClearOutput
1454 );
1455 assert_eq!(resolved.streampager.wrapping, defaults.streampager.wrapping);
1457 assert_eq!(
1458 resolved.streampager.show_ruler,
1459 defaults.streampager.show_ruler
1460 );
1461 }
1462}