1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
7pub enum Verbosity {
8 Quiet,
10 #[default]
12 Normal,
13 Verbose,
15 Debug,
17}
18
19impl Verbosity {
20 #[must_use]
22 pub const fn is_quiet(self) -> bool {
23 matches!(self, Self::Quiet)
24 }
25
26 #[must_use]
28 pub const fn is_verbose(self) -> bool {
29 matches!(self, Self::Verbose | Self::Debug)
30 }
31
32 #[must_use]
34 pub const fn is_debug(self) -> bool {
35 matches!(self, Self::Debug)
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
41pub enum ColorChoice {
42 Always,
44 #[default]
46 Auto,
47 Never,
49}
50
51impl ColorChoice {
52 #[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
63fn atty_is_terminal() -> bool {
65 std::io::IsTerminal::is_terminal(&std::io::stdout())
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct CliConfig {
72 pub verbosity: Verbosity,
74 pub color: ColorChoice,
76 pub parallel_jobs: usize,
78 pub fail_fast: bool,
80 pub watch: bool,
82 pub coverage: bool,
84 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, fail_fast: false,
95 watch: false,
96 coverage: false,
97 output_dir: "target/probar".to_string(),
98 }
99 }
100}
101
102impl CliConfig {
103 #[must_use]
105 pub fn new() -> Self {
106 Self::default()
107 }
108
109 #[must_use]
111 pub const fn with_verbosity(mut self, verbosity: Verbosity) -> Self {
112 self.verbosity = verbosity;
113 self
114 }
115
116 #[must_use]
118 pub const fn with_color(mut self, color: ColorChoice) -> Self {
119 self.color = color;
120 self
121 }
122
123 #[must_use]
125 pub const fn with_parallel_jobs(mut self, jobs: usize) -> Self {
126 self.parallel_jobs = jobs;
127 self
128 }
129
130 #[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 #[must_use]
139 pub const fn with_watch(mut self, watch: bool) -> Self {
140 self.watch = watch;
141 self
142 }
143
144 #[must_use]
146 pub const fn with_coverage(mut self, coverage: bool) -> Self {
147 self.coverage = coverage;
148 self
149 }
150
151 #[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 #[must_use]
160 #[allow(clippy::redundant_closure_for_method_calls)] 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 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}