Skip to main content

probador/
config.rs

1//! CLI configuration
2
3use serde::{Deserialize, Serialize};
4
5/// CLI verbosity level
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
7pub enum Verbosity {
8    /// Quiet - minimal output
9    Quiet,
10    /// Normal - default output
11    #[default]
12    Normal,
13    /// Verbose - extra output
14    Verbose,
15    /// Debug - maximum output
16    Debug,
17}
18
19impl Verbosity {
20    /// Check if quiet mode
21    #[must_use]
22    pub const fn is_quiet(self) -> bool {
23        matches!(self, Self::Quiet)
24    }
25
26    /// Check if verbose or higher
27    #[must_use]
28    pub const fn is_verbose(self) -> bool {
29        matches!(self, Self::Verbose | Self::Debug)
30    }
31
32    /// Check if debug mode
33    #[must_use]
34    pub const fn is_debug(self) -> bool {
35        matches!(self, Self::Debug)
36    }
37}
38
39/// Color output choice
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
41pub enum ColorChoice {
42    /// Always use colors
43    Always,
44    /// Use colors when output is a terminal
45    #[default]
46    Auto,
47    /// Never use colors
48    Never,
49}
50
51impl ColorChoice {
52    /// Should use colors based on output detection
53    #[must_use]
54    pub fn should_color(self) -> bool {
55        match self {
56            Self::Always => true,
57            Self::Never => false,
58            Self::Auto => atty_is_terminal(),
59        }
60    }
61}
62
63/// Check if stdout is a terminal
64fn atty_is_terminal() -> bool {
65    // Use std library detection
66    std::io::IsTerminal::is_terminal(&std::io::stdout())
67}
68
69/// CLI configuration
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct CliConfig {
72    /// Verbosity level
73    pub verbosity: Verbosity,
74    /// Color output choice
75    pub color: ColorChoice,
76    /// Number of parallel jobs (0 = auto-detect)
77    pub parallel_jobs: usize,
78    /// Fail fast on first error
79    pub fail_fast: bool,
80    /// Watch mode enabled
81    pub watch: bool,
82    /// Coverage enabled
83    pub coverage: bool,
84    /// Output directory for reports
85    pub output_dir: String,
86}
87
88impl Default for CliConfig {
89    fn default() -> Self {
90        Self {
91            verbosity: Verbosity::Normal,
92            color: ColorChoice::Auto,
93            parallel_jobs: 0, // Auto-detect
94            fail_fast: false,
95            watch: false,
96            coverage: false,
97            output_dir: "target/probar".to_string(),
98        }
99    }
100}
101
102impl CliConfig {
103    /// Create new default configuration
104    #[must_use]
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    /// Set verbosity
110    #[must_use]
111    pub const fn with_verbosity(mut self, verbosity: Verbosity) -> Self {
112        self.verbosity = verbosity;
113        self
114    }
115
116    /// Set color choice
117    #[must_use]
118    pub const fn with_color(mut self, color: ColorChoice) -> Self {
119        self.color = color;
120        self
121    }
122
123    /// Set parallel jobs
124    #[must_use]
125    pub const fn with_parallel_jobs(mut self, jobs: usize) -> Self {
126        self.parallel_jobs = jobs;
127        self
128    }
129
130    /// Set fail fast
131    #[must_use]
132    pub const fn with_fail_fast(mut self, fail_fast: bool) -> Self {
133        self.fail_fast = fail_fast;
134        self
135    }
136
137    /// Set watch mode
138    #[must_use]
139    pub const fn with_watch(mut self, watch: bool) -> Self {
140        self.watch = watch;
141        self
142    }
143
144    /// Set coverage
145    #[must_use]
146    pub const fn with_coverage(mut self, coverage: bool) -> Self {
147        self.coverage = coverage;
148        self
149    }
150
151    /// Set output directory
152    #[must_use]
153    pub fn with_output_dir(mut self, dir: impl Into<String>) -> Self {
154        self.output_dir = dir.into();
155        self
156    }
157
158    /// Get effective number of parallel jobs
159    #[must_use]
160    #[allow(clippy::redundant_closure_for_method_calls)] // Cannot use NonZero::get directly due to MSRV 1.75 (stable in 1.79)
161    pub fn effective_jobs(&self) -> usize {
162        if self.parallel_jobs == 0 {
163            std::thread::available_parallelism()
164                .map(|n| n.get())
165                .unwrap_or(1)
166        } else {
167            self.parallel_jobs
168        }
169    }
170}
171
172#[cfg(test)]
173#[allow(clippy::unwrap_used, clippy::expect_used)]
174mod tests {
175    use super::*;
176
177    mod verbosity_tests {
178        use super::*;
179
180        #[test]
181        fn test_default_verbosity() {
182            let v = Verbosity::default();
183            assert_eq!(v, Verbosity::Normal);
184        }
185
186        #[test]
187        fn test_is_quiet() {
188            assert!(Verbosity::Quiet.is_quiet());
189            assert!(!Verbosity::Normal.is_quiet());
190            assert!(!Verbosity::Verbose.is_quiet());
191            assert!(!Verbosity::Debug.is_quiet());
192        }
193
194        #[test]
195        fn test_is_verbose() {
196            assert!(!Verbosity::Quiet.is_verbose());
197            assert!(!Verbosity::Normal.is_verbose());
198            assert!(Verbosity::Verbose.is_verbose());
199            assert!(Verbosity::Debug.is_verbose());
200        }
201
202        #[test]
203        fn test_is_debug() {
204            assert!(!Verbosity::Quiet.is_debug());
205            assert!(!Verbosity::Normal.is_debug());
206            assert!(!Verbosity::Verbose.is_debug());
207            assert!(Verbosity::Debug.is_debug());
208        }
209
210        #[test]
211        fn test_clone() {
212            let v = Verbosity::Debug;
213            let cloned = v;
214            assert_eq!(v, cloned);
215        }
216
217        #[test]
218        fn test_debug_trait() {
219            let debug = format!("{:?}", Verbosity::Verbose);
220            assert!(debug.contains("Verbose"));
221        }
222
223        #[test]
224        fn test_serialize() {
225            let json = serde_json::to_string(&Verbosity::Debug).unwrap();
226            assert!(json.contains("Debug"));
227        }
228
229        #[test]
230        fn test_deserialize() {
231            let v: Verbosity = serde_json::from_str("\"Quiet\"").unwrap();
232            assert_eq!(v, Verbosity::Quiet);
233        }
234    }
235
236    mod color_choice_tests {
237        use super::*;
238
239        #[test]
240        fn test_default_color() {
241            let c = ColorChoice::default();
242            assert_eq!(c, ColorChoice::Auto);
243        }
244
245        #[test]
246        fn test_should_color_always() {
247            assert!(ColorChoice::Always.should_color());
248        }
249
250        #[test]
251        fn test_should_color_never() {
252            assert!(!ColorChoice::Never.should_color());
253        }
254
255        #[test]
256        fn test_should_color_auto() {
257            // Auto depends on terminal detection, just ensure it doesn't panic
258            let _ = ColorChoice::Auto.should_color();
259        }
260
261        #[test]
262        fn test_clone() {
263            let c = ColorChoice::Always;
264            let cloned = c;
265            assert_eq!(c, cloned);
266        }
267
268        #[test]
269        fn test_debug_trait() {
270            let debug = format!("{:?}", ColorChoice::Never);
271            assert!(debug.contains("Never"));
272        }
273
274        #[test]
275        fn test_serialize() {
276            let json = serde_json::to_string(&ColorChoice::Always).unwrap();
277            assert!(json.contains("Always"));
278        }
279
280        #[test]
281        fn test_deserialize() {
282            let c: ColorChoice = serde_json::from_str("\"Never\"").unwrap();
283            assert_eq!(c, ColorChoice::Never);
284        }
285    }
286
287    mod cli_config_tests {
288        use super::*;
289
290        #[test]
291        fn test_default_config() {
292            let config = CliConfig::default();
293            assert_eq!(config.verbosity, Verbosity::Normal);
294            assert_eq!(config.color, ColorChoice::Auto);
295            assert_eq!(config.parallel_jobs, 0);
296            assert!(!config.fail_fast);
297            assert!(!config.watch);
298            assert!(!config.coverage);
299        }
300
301        #[test]
302        fn test_with_verbosity() {
303            let config = CliConfig::new().with_verbosity(Verbosity::Debug);
304            assert_eq!(config.verbosity, Verbosity::Debug);
305        }
306
307        #[test]
308        fn test_with_color() {
309            let config = CliConfig::new().with_color(ColorChoice::Never);
310            assert_eq!(config.color, ColorChoice::Never);
311        }
312
313        #[test]
314        fn test_with_parallel_jobs() {
315            let config = CliConfig::new().with_parallel_jobs(4);
316            assert_eq!(config.parallel_jobs, 4);
317        }
318
319        #[test]
320        fn test_with_fail_fast() {
321            let config = CliConfig::new().with_fail_fast(true);
322            assert!(config.fail_fast);
323        }
324
325        #[test]
326        fn test_with_watch() {
327            let config = CliConfig::new().with_watch(true);
328            assert!(config.watch);
329        }
330
331        #[test]
332        fn test_with_coverage() {
333            let config = CliConfig::new().with_coverage(true);
334            assert!(config.coverage);
335        }
336
337        #[test]
338        fn test_with_output_dir() {
339            let config = CliConfig::new().with_output_dir("custom/output");
340            assert_eq!(config.output_dir, "custom/output");
341        }
342
343        #[test]
344        fn test_effective_jobs_specified() {
345            let config = CliConfig::new().with_parallel_jobs(8);
346            assert_eq!(config.effective_jobs(), 8);
347        }
348
349        #[test]
350        fn test_effective_jobs_auto() {
351            let config = CliConfig::new().with_parallel_jobs(0);
352            assert!(config.effective_jobs() >= 1);
353        }
354
355        #[test]
356        fn test_chained_builders() {
357            let config = CliConfig::new()
358                .with_verbosity(Verbosity::Verbose)
359                .with_color(ColorChoice::Always)
360                .with_parallel_jobs(2)
361                .with_fail_fast(true)
362                .with_coverage(true);
363
364            assert_eq!(config.verbosity, Verbosity::Verbose);
365            assert_eq!(config.color, ColorChoice::Always);
366            assert_eq!(config.parallel_jobs, 2);
367            assert!(config.fail_fast);
368            assert!(config.coverage);
369        }
370
371        #[test]
372        fn test_clone() {
373            let config = CliConfig::new()
374                .with_verbosity(Verbosity::Debug)
375                .with_fail_fast(true);
376            let cloned = config.clone();
377            assert_eq!(config.verbosity, cloned.verbosity);
378            assert_eq!(config.fail_fast, cloned.fail_fast);
379        }
380
381        #[test]
382        fn test_debug_trait() {
383            let config = CliConfig::default();
384            let debug = format!("{config:?}");
385            assert!(debug.contains("CliConfig"));
386        }
387
388        #[test]
389        fn test_serialize() {
390            let config = CliConfig::new().with_fail_fast(true);
391            let json = serde_json::to_string(&config).unwrap();
392            assert!(json.contains("fail_fast"));
393            assert!(json.contains("true"));
394        }
395
396        #[test]
397        fn test_deserialize() {
398            let json = r#"{"verbosity":"Debug","color":"Always","parallel_jobs":4,"fail_fast":true,"watch":false,"coverage":true,"output_dir":"test"}"#;
399            let config: CliConfig = serde_json::from_str(json).unwrap();
400            assert_eq!(config.verbosity, Verbosity::Debug);
401            assert_eq!(config.color, ColorChoice::Always);
402            assert_eq!(config.parallel_jobs, 4);
403            assert!(config.fail_fast);
404            assert!(config.coverage);
405        }
406
407        #[test]
408        fn test_output_dir_default() {
409            let config = CliConfig::default();
410            assert_eq!(config.output_dir, "target/probar");
411        }
412    }
413}