Skip to main content

twyg/
opts.rs

1//! Logger configuration options.
2//!
3//! This module provides the [`Opts`] struct for configuring the twyg logger.
4
5use chrono::Local;
6use serde::{Deserialize, Serialize};
7
8use super::color::Colors;
9use super::error::{Result, TwygError};
10use super::level::LogLevel;
11use super::output::Output;
12use super::timestamp::TSFormat;
13
14const DEFAULT_TS_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
15
16/// Side to pad level strings.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
18pub enum PadSide {
19    /// Pad on the left (right-align)
20    Left,
21
22    /// Pad on the right (left-align)
23    #[default]
24    Right,
25}
26
27/// Logger configuration options.
28///
29/// Configure all aspects of the twyg logger including output destination,
30/// log level, colors, and formatting.
31///
32/// # Examples
33///
34/// ```
35/// use twyg::{LogLevel, OptsBuilder, Output};
36///
37/// let opts = OptsBuilder::new()
38///     .coloured(true)
39///     .output(Output::Stdout)
40///     .level(LogLevel::Debug)
41///     .report_caller(true)
42///     .build()
43///     .unwrap();
44/// ```
45#[derive(Clone, Debug, Serialize, Deserialize)]
46pub struct Opts {
47    /// Enable colored output using ANSI escape codes.
48    #[serde(default)]
49    coloured: bool,
50
51    /// Output destination (stdout, stderr, or file).
52    #[serde(default)]
53    output: Output,
54
55    /// Minimum log level to display.
56    #[serde(default)]
57    level: LogLevel,
58
59    /// Include file name and line number in log output.
60    #[serde(default)]
61    report_caller: bool,
62
63    /// Timestamp format (enum with presets + custom).
64    #[serde(default)]
65    timestamp_format: TSFormat,
66
67    /// Enable level padding for alignment.
68    #[serde(default)]
69    pad_level: bool,
70
71    /// Number of characters to pad level to.
72    #[serde(default = "default_pad_amount")]
73    pad_amount: usize,
74
75    /// Which side to pad the level string.
76    #[serde(default)]
77    pad_side: PadSide,
78
79    /// Separator between message and attributes (default: ": ").
80    #[serde(default = "default_msg_separator")]
81    msg_separator: String,
82
83    /// Arrow character to use (default: "▶").
84    #[serde(default = "default_arrow_char")]
85    arrow_char: String,
86
87    /// Fine-grained color configuration.
88    #[serde(default)]
89    colors: Colors,
90}
91
92// Default value functions for serde
93fn default_pad_amount() -> usize {
94    5
95}
96
97fn default_msg_separator() -> String {
98    ": ".to_string()
99}
100
101fn default_arrow_char() -> String {
102    "▶".to_string()
103}
104
105impl Default for Opts {
106    fn default() -> Self {
107        Self {
108            coloured: false,
109            output: Output::default(),
110            level: LogLevel::default(),
111            report_caller: false,
112            timestamp_format: TSFormat::default(),
113            pad_level: false,
114            pad_amount: 5,
115            pad_side: PadSide::default(),
116            msg_separator: ": ".to_string(),
117            arrow_char: "▶".to_string(),
118            colors: Colors::default(),
119        }
120    }
121}
122
123impl Opts {
124    /// Creates a new Opts with default values.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use twyg::Opts;
130    ///
131    /// let opts = Opts::new();
132    /// ```
133    pub fn new() -> Opts {
134        Opts::default()
135    }
136
137    /// Returns whether colored output is enabled.
138    pub fn coloured(&self) -> bool {
139        self.coloured
140    }
141
142    /// Returns the output destination.
143    pub fn output(&self) -> &Output {
144        &self.output
145    }
146
147    /// Returns the minimum log level.
148    pub fn level(&self) -> LogLevel {
149        self.level
150    }
151
152    /// Returns whether caller reporting is enabled.
153    pub fn report_caller(&self) -> bool {
154        self.report_caller
155    }
156
157    /// Returns the timestamp format.
158    pub fn timestamp_format(&self) -> &TSFormat {
159        &self.timestamp_format
160    }
161
162    /// Returns whether level padding is enabled.
163    pub fn pad_level(&self) -> bool {
164        self.pad_level
165    }
166
167    /// Returns the padding amount.
168    pub fn pad_amount(&self) -> usize {
169        self.pad_amount
170    }
171
172    /// Returns the padding side.
173    pub fn pad_side(&self) -> PadSide {
174        self.pad_side
175    }
176
177    /// Returns the message separator.
178    pub fn msg_separator(&self) -> &str {
179        &self.msg_separator
180    }
181
182    /// Returns the arrow character.
183    pub fn arrow_char(&self) -> &str {
184        &self.arrow_char
185    }
186
187    /// Returns the color configuration.
188    pub fn colors(&self) -> &Colors {
189        &self.colors
190    }
191
192    /// Returns the time format string (deprecated, for backward compatibility).
193    #[deprecated(since = "0.6.1", note = "Use timestamp_format() instead")]
194    pub fn time_format(&self) -> Option<&str> {
195        match &self.timestamp_format {
196            TSFormat::Custom(s) => Some(s.as_str()),
197            _ => Some(self.timestamp_format.to_format_string()),
198        }
199    }
200}
201
202/// Builder for constructing [`Opts`] with validation.
203///
204/// # Examples
205///
206/// ```
207/// use twyg::{LogLevel, OptsBuilder, Output};
208///
209/// let opts = OptsBuilder::new()
210///     .coloured(true)
211///     .level(LogLevel::Debug)
212///     .report_caller(true)
213///     .build()
214///     .unwrap();
215/// ```
216#[derive(Clone, Debug)]
217pub struct OptsBuilder {
218    coloured: bool,
219    output: Output,
220    level: LogLevel,
221    report_caller: bool,
222    timestamp_format: TSFormat,
223    pad_level: bool,
224    pad_amount: usize,
225    pad_side: PadSide,
226    msg_separator: String,
227    arrow_char: String,
228    colors: Colors,
229}
230
231impl Default for OptsBuilder {
232    fn default() -> Self {
233        Self::new()
234    }
235}
236
237impl OptsBuilder {
238    /// Creates a new OptsBuilder with default values.
239    pub fn new() -> Self {
240        Self {
241            coloured: false,
242            output: Output::default(),
243            level: LogLevel::default(),
244            report_caller: false,
245            timestamp_format: TSFormat::default(),
246            pad_level: false,
247            pad_amount: 5,
248            pad_side: PadSide::default(),
249            msg_separator: ": ".to_string(),
250            arrow_char: "▶".to_string(),
251            colors: Colors::default(),
252        }
253    }
254
255    /// Preset with level padding enabled.
256    pub fn with_level_padding() -> Self {
257        Self::new()
258            .pad_level(true)
259            .pad_amount(5)
260            .pad_side(PadSide::Right)
261    }
262
263    /// Preset without caller information.
264    pub fn no_caller() -> Self {
265        Self::new().report_caller(false)
266    }
267
268    /// Enable or disable colored output.
269    pub fn coloured(mut self, coloured: bool) -> Self {
270        self.coloured = coloured;
271        self
272    }
273
274    /// Set the output destination.
275    pub fn output(mut self, output: Output) -> Self {
276        self.output = output;
277        self
278    }
279
280    /// Set the minimum log level.
281    pub fn level(mut self, level: LogLevel) -> Self {
282        self.level = level;
283        self
284    }
285
286    /// Enable or disable caller reporting.
287    pub fn report_caller(mut self, report: bool) -> Self {
288        self.report_caller = report;
289        self
290    }
291
292    /// Set the timestamp format.
293    pub fn timestamp_format(mut self, format: TSFormat) -> Self {
294        self.timestamp_format = format;
295        self
296    }
297
298    /// Enable or disable level padding.
299    pub fn pad_level(mut self, pad: bool) -> Self {
300        self.pad_level = pad;
301        self
302    }
303
304    /// Set the padding amount.
305    pub fn pad_amount(mut self, amount: usize) -> Self {
306        self.pad_amount = amount;
307        self
308    }
309
310    /// Set the padding side.
311    pub fn pad_side(mut self, side: PadSide) -> Self {
312        self.pad_side = side;
313        self
314    }
315
316    /// Set the message separator.
317    pub fn msg_separator(mut self, sep: impl Into<String>) -> Self {
318        self.msg_separator = sep.into();
319        self
320    }
321
322    /// Set the arrow character.
323    pub fn arrow_char(mut self, arrow: impl Into<String>) -> Self {
324        self.arrow_char = arrow.into();
325        self
326    }
327
328    /// Set the color configuration.
329    pub fn colors(mut self, colors: Colors) -> Self {
330        self.colors = colors;
331        self
332    }
333
334    /// Set a custom time format string (deprecated).
335    ///
336    /// The format string uses chrono's format syntax.
337    ///
338    /// # Examples
339    ///
340    /// ```
341    /// use twyg::OptsBuilder;
342    ///
343    /// let opts = OptsBuilder::new()
344    ///     .time_format("%H:%M:%S")
345    ///     .build()
346    ///     .unwrap();
347    /// ```
348    #[deprecated(since = "0.6.1", note = "Use timestamp_format() instead")]
349    pub fn time_format(mut self, format: impl Into<String>) -> Self {
350        self.timestamp_format = TSFormat::Custom(format.into());
351        self
352    }
353
354    /// Build the Opts, validating the timestamp format if provided.
355    ///
356    /// # Errors
357    ///
358    /// Returns an error if a custom timestamp format string is invalid.
359    pub fn build(self) -> Result<Opts> {
360        // Validate custom timestamp format if provided
361        if let TSFormat::Custom(ref fmt) = self.timestamp_format {
362            validate_time_format(fmt)?;
363        }
364
365        Ok(Opts {
366            coloured: self.coloured,
367            output: self.output,
368            level: self.level,
369            report_caller: self.report_caller,
370            timestamp_format: self.timestamp_format,
371            pad_level: self.pad_level,
372            pad_amount: self.pad_amount,
373            pad_side: self.pad_side,
374            msg_separator: self.msg_separator,
375            arrow_char: self.arrow_char,
376            colors: self.colors,
377        })
378    }
379}
380
381/// Validates a time format string by attempting to format the current time.
382fn validate_time_format(format: &str) -> Result<()> {
383    match std::panic::catch_unwind(|| {
384        Local::now().format(format).to_string();
385    }) {
386        Ok(_) => Ok(()),
387        Err(_) => Err(TwygError::ConfigError(format!(
388            "invalid time format string: {}",
389            format
390        ))),
391    }
392}
393
394// Backwards compatibility helpers (deprecated)
395pub mod compat {
396    use super::*;
397    use crate::out;
398
399    /// Returns the default file output (deprecated).
400    #[deprecated(since = "0.6.0", note = "Use Output::default() instead")]
401    pub fn default_file() -> Option<String> {
402        Some(out::STDOUT.to_string())
403    }
404
405    /// Returns the default log level (deprecated).
406    #[deprecated(since = "0.6.0", note = "Use LogLevel::default() instead")]
407    pub fn default_level() -> Option<String> {
408        Some("error".to_string())
409    }
410
411    /// Returns the default timestamp format (deprecated).
412    #[deprecated(since = "0.6.0", note = "Use Opts::new() or set time_format directly")]
413    pub fn default_ts_format() -> Option<String> {
414        Some(DEFAULT_TS_FORMAT.to_string())
415    }
416}
417
418// Re-export deprecated functions at module level for backwards compatibility
419#[allow(deprecated)]
420#[deprecated(since = "0.6.0", note = "Use Output::default() instead")]
421pub use compat::default_file;
422
423#[allow(deprecated)]
424#[deprecated(since = "0.6.0", note = "Use LogLevel::default() instead")]
425pub use compat::default_level;
426
427#[allow(deprecated)]
428#[deprecated(since = "0.6.0", note = "Use Opts::new() or set time_format directly")]
429pub use compat::default_ts_format;
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn test_default_opts() {
437        let opts = Opts::default();
438        assert!(!opts.coloured());
439        assert_eq!(opts.output(), &Output::Stdout);
440        assert_eq!(opts.level(), LogLevel::Error);
441        assert!(!opts.report_caller());
442        // timestamp_format is now always set (defaults to Standard)
443        assert_eq!(opts.timestamp_format(), &TSFormat::Standard);
444    }
445
446    #[test]
447    fn test_new_opts_sets_defaults() {
448        let opts = Opts::new();
449        assert!(!opts.coloured());
450        assert_eq!(opts.output(), &Output::Stdout);
451        assert_eq!(opts.level(), LogLevel::Error);
452        assert!(!opts.report_caller());
453        assert_eq!(opts.timestamp_format(), &TSFormat::Standard);
454    }
455
456    #[test]
457    fn test_opts_clone() {
458        let opts1 = Opts::new();
459        let opts2 = opts1.clone();
460        assert_eq!(opts1.output(), opts2.output());
461        assert_eq!(opts1.level(), opts2.level());
462        assert_eq!(opts1.coloured(), opts2.coloured());
463    }
464
465    #[test]
466    fn test_opts_debug() {
467        let opts = Opts::new();
468        let debug_str = format!("{:?}", opts);
469        assert!(debug_str.contains("Opts"));
470    }
471
472    #[test]
473    fn test_opts_serialize_deserialize() {
474        let opts = OptsBuilder::new()
475            .coloured(true)
476            .output(Output::Stderr)
477            .level(LogLevel::Debug)
478            .report_caller(true)
479            .timestamp_format(TSFormat::TimeOnly)
480            .build()
481            .unwrap();
482
483        let serialized = serde_json::to_string(&opts).unwrap();
484        let deserialized: Opts = serde_json::from_str(&serialized).unwrap();
485
486        assert_eq!(opts.coloured(), deserialized.coloured());
487        assert_eq!(opts.output(), deserialized.output());
488        assert_eq!(opts.level(), deserialized.level());
489        assert_eq!(opts.report_caller(), deserialized.report_caller());
490        assert_eq!(opts.timestamp_format(), deserialized.timestamp_format());
491    }
492
493    #[test]
494    fn test_opts_builder_with_custom_values() {
495        let opts = OptsBuilder::new()
496            .coloured(true)
497            .output(Output::file("/tmp/test.log"))
498            .level(LogLevel::Trace)
499            .report_caller(true)
500            .timestamp_format(TSFormat::Custom("%Y-%m-%d".to_string()))
501            .build()
502            .unwrap();
503
504        assert!(opts.coloured());
505        assert_eq!(opts.output(), &Output::file("/tmp/test.log"));
506        assert_eq!(opts.level(), LogLevel::Trace);
507        assert!(opts.report_caller());
508        assert_eq!(
509            opts.timestamp_format(),
510            &TSFormat::Custom("%Y-%m-%d".to_string())
511        );
512    }
513
514    #[test]
515    fn test_opts_builder_with_different_outputs() {
516        let stdout_opts = OptsBuilder::new().output(Output::Stdout).build().unwrap();
517        assert_eq!(stdout_opts.output(), &Output::Stdout);
518
519        let stderr_opts = OptsBuilder::new().output(Output::Stderr).build().unwrap();
520        assert_eq!(stderr_opts.output(), &Output::Stderr);
521
522        let file_opts = OptsBuilder::new()
523            .output(Output::file("/var/log/app.log"))
524            .build()
525            .unwrap();
526        assert_eq!(file_opts.output(), &Output::file("/var/log/app.log"));
527    }
528
529    #[test]
530    fn test_opts_builder_default() {
531        let opts = OptsBuilder::default().build().unwrap();
532        assert!(!opts.coloured());
533        assert_eq!(opts.output(), &Output::Stdout);
534        assert_eq!(opts.level(), LogLevel::Error);
535        assert!(!opts.report_caller());
536        // timestamp_format is now always set (defaults to Standard)
537        assert_eq!(opts.timestamp_format(), &TSFormat::Standard);
538    }
539
540    #[test]
541    fn test_validate_time_format_valid() {
542        assert!(validate_time_format("%Y-%m-%d %H:%M:%S").is_ok());
543        assert!(validate_time_format("%H:%M:%S").is_ok());
544        assert!(validate_time_format("%Y-%m-%d").is_ok());
545    }
546
547    #[test]
548    fn test_validate_time_format_invalid() {
549        // Note: chrono's format is quite lenient, so many things work
550        // This test documents the behavior
551        let result = validate_time_format("%Z"); // %Z might not work in all contexts
552                                                 // Most format strings are accepted, so this might pass
553        let _ = result;
554    }
555
556    #[test]
557    fn test_opts_builder_chaining() {
558        let opts = OptsBuilder::new()
559            .coloured(true)
560            .level(LogLevel::Debug)
561            .report_caller(true)
562            .output(Output::Stderr)
563            .build()
564            .unwrap();
565
566        assert!(opts.coloured());
567        assert_eq!(opts.level(), LogLevel::Debug);
568        assert!(opts.report_caller());
569        assert_eq!(opts.output(), &Output::Stderr);
570    }
571
572    // Test deprecated functions still work
573    #[test]
574    #[allow(deprecated)]
575    fn test_deprecated_default_file() {
576        let file = default_file();
577        assert_eq!(file, Some("stdout".to_string()));
578    }
579
580    #[test]
581    #[allow(deprecated)]
582    fn test_deprecated_default_level() {
583        let level = default_level();
584        assert_eq!(level, Some("error".to_string()));
585    }
586
587    #[test]
588    #[allow(deprecated)]
589    fn test_deprecated_default_ts_format() {
590        let format = default_ts_format();
591        assert_eq!(format, Some("%Y-%m-%d %H:%M:%S".to_string()));
592    }
593
594    #[test]
595    fn test_pad_side_default() {
596        assert_eq!(PadSide::default(), PadSide::Right);
597    }
598
599    #[test]
600    fn test_pad_side_eq() {
601        assert_eq!(PadSide::Left, PadSide::Left);
602        assert_eq!(PadSide::Right, PadSide::Right);
603        assert_ne!(PadSide::Left, PadSide::Right);
604    }
605
606    #[test]
607    fn test_pad_side_clone() {
608        let left = PadSide::Left;
609        let cloned = left.clone();
610        assert_eq!(left, cloned);
611    }
612
613    #[test]
614    fn test_pad_side_debug() {
615        let debug_str = format!("{:?}", PadSide::Left);
616        assert!(debug_str.contains("Left"));
617    }
618
619    #[test]
620    fn test_opts_all_getters() {
621        let colors = Colors::default();
622        let opts = OptsBuilder::new()
623            .coloured(true)
624            .output(Output::Stderr)
625            .level(LogLevel::Debug)
626            .report_caller(true)
627            .timestamp_format(TSFormat::TimeOnly)
628            .pad_level(true)
629            .pad_amount(7)
630            .pad_side(PadSide::Left)
631            .msg_separator(" | ")
632            .arrow_char("→")
633            .colors(colors.clone())
634            .build()
635            .unwrap();
636
637        assert!(opts.coloured());
638        assert_eq!(opts.output(), &Output::Stderr);
639        assert_eq!(opts.level(), LogLevel::Debug);
640        assert!(opts.report_caller());
641        assert_eq!(opts.timestamp_format(), &TSFormat::TimeOnly);
642        assert!(opts.pad_level());
643        assert_eq!(opts.pad_amount(), 7);
644        assert_eq!(opts.pad_side(), PadSide::Left);
645        assert_eq!(opts.msg_separator(), " | ");
646        assert_eq!(opts.arrow_char(), "→");
647        assert_eq!(opts.colors(), &colors);
648    }
649
650    #[test]
651    fn test_opts_builder_preset_with_level_padding() {
652        let opts = OptsBuilder::with_level_padding().build().unwrap();
653        assert!(opts.pad_level());
654        assert_eq!(opts.pad_amount(), 5);
655        assert_eq!(opts.pad_side(), PadSide::Right);
656    }
657
658    #[test]
659    fn test_opts_builder_preset_no_caller() {
660        let opts = OptsBuilder::no_caller().build().unwrap();
661        assert!(!opts.report_caller());
662    }
663
664    #[test]
665    fn test_opts_builder_chaining_all_methods() {
666        let opts = OptsBuilder::new()
667            .coloured(false)
668            .output(Output::file("/tmp/test.log"))
669            .level(LogLevel::Trace)
670            .report_caller(false)
671            .timestamp_format(TSFormat::RFC3339)
672            .pad_level(true)
673            .pad_amount(10)
674            .pad_side(PadSide::Left)
675            .msg_separator(" :: ")
676            .arrow_char("»")
677            .colors(Colors::default())
678            .build()
679            .unwrap();
680
681        assert!(!opts.coloured());
682        assert_eq!(opts.level(), LogLevel::Trace);
683        assert_eq!(opts.pad_amount(), 10);
684        assert_eq!(opts.msg_separator(), " :: ");
685        assert_eq!(opts.arrow_char(), "»");
686    }
687
688    #[test]
689    #[allow(deprecated)]
690    fn test_opts_deprecated_time_format_method() {
691        let opts = OptsBuilder::new().time_format("%H:%M").build().unwrap();
692
693        // The deprecated time_format() method should now return Some
694        assert!(opts.time_format().is_some());
695        let format = opts.time_format().unwrap();
696        assert_eq!(format, "%H:%M");
697    }
698
699    #[test]
700    #[allow(deprecated)]
701    fn test_opts_builder_deprecated_time_format() {
702        let opts = OptsBuilder::new().time_format("%Y%m%d").build().unwrap();
703
704        // Should have set timestamp_format to Custom variant
705        match opts.timestamp_format() {
706            TSFormat::Custom(s) => assert_eq!(s, "%Y%m%d"),
707            _ => panic!("Expected Custom variant"),
708        }
709    }
710
711    #[test]
712    fn test_validate_time_format_various_formats() {
713        // Test various valid formats
714        assert!(validate_time_format("%Y").is_ok());
715        assert!(validate_time_format("%m").is_ok());
716        assert!(validate_time_format("%d").is_ok());
717        assert!(validate_time_format("%H").is_ok());
718        assert!(validate_time_format("%M").is_ok());
719        assert!(validate_time_format("%S").is_ok());
720        assert!(validate_time_format("%Y-%m-%d %H:%M:%S").is_ok());
721        assert!(validate_time_format("%Y%m%d.%H%M%S").is_ok());
722    }
723
724    #[test]
725    fn test_opts_serialize_with_all_fields() {
726        let opts = OptsBuilder::new()
727            .coloured(true)
728            .output(Output::Stderr)
729            .level(LogLevel::Warn)
730            .report_caller(true)
731            .timestamp_format(TSFormat::Simple)
732            .pad_level(true)
733            .pad_amount(8)
734            .pad_side(PadSide::Left)
735            .msg_separator(" => ")
736            .arrow_char("⇒")
737            .colors(Colors::default())
738            .build()
739            .unwrap();
740
741        let serialized = serde_json::to_string(&opts).unwrap();
742        let deserialized: Opts = serde_json::from_str(&serialized).unwrap();
743
744        assert_eq!(opts.coloured(), deserialized.coloured());
745        assert_eq!(opts.output(), deserialized.output());
746        assert_eq!(opts.level(), deserialized.level());
747        assert_eq!(opts.report_caller(), deserialized.report_caller());
748        assert_eq!(opts.pad_level(), deserialized.pad_level());
749        assert_eq!(opts.pad_amount(), deserialized.pad_amount());
750        assert_eq!(opts.pad_side(), deserialized.pad_side());
751        assert_eq!(opts.msg_separator(), deserialized.msg_separator());
752        assert_eq!(opts.arrow_char(), deserialized.arrow_char());
753    }
754
755    #[test]
756    fn test_pad_side_serialize_deserialize() {
757        let left = PadSide::Left;
758        let serialized = serde_json::to_string(&left).unwrap();
759        let deserialized: PadSide = serde_json::from_str(&serialized).unwrap();
760        assert_eq!(left, deserialized);
761
762        let right = PadSide::Right;
763        let serialized = serde_json::to_string(&right).unwrap();
764        let deserialized: PadSide = serde_json::from_str(&serialized).unwrap();
765        assert_eq!(right, deserialized);
766    }
767
768    #[test]
769    fn test_opts_default_values_match_new() {
770        let default_opts = Opts::default();
771        let new_opts = Opts::new();
772
773        assert_eq!(default_opts.coloured(), new_opts.coloured());
774        assert_eq!(default_opts.output(), new_opts.output());
775        assert_eq!(default_opts.level(), new_opts.level());
776        assert_eq!(default_opts.report_caller(), new_opts.report_caller());
777        assert_eq!(default_opts.pad_level(), new_opts.pad_level());
778        assert_eq!(default_opts.pad_amount(), new_opts.pad_amount());
779        assert_eq!(default_opts.pad_side(), new_opts.pad_side());
780    }
781
782    #[test]
783    fn test_opts_builder_multiple_builds() {
784        let builder = OptsBuilder::new().level(LogLevel::Debug).coloured(true);
785
786        // Build multiple times from cloned builder
787        let opts1 = builder.clone().build().unwrap();
788        let opts2 = builder.clone().build().unwrap();
789
790        assert_eq!(opts1.level(), opts2.level());
791        assert_eq!(opts1.coloured(), opts2.coloured());
792    }
793
794    #[test]
795    fn test_default_helper_functions() {
796        assert_eq!(default_pad_amount(), 5);
797        assert_eq!(default_msg_separator(), ": ");
798        assert_eq!(default_arrow_char(), "▶");
799    }
800
801    #[test]
802    fn test_opts_deserialize_partial_toml_uses_defaults() {
803        let toml_str = r#"level = "debug""#;
804        let opts: Opts = toml::from_str(toml_str).unwrap();
805
806        // The explicitly set field should have the provided value.
807        assert_eq!(opts.level(), LogLevel::Debug);
808
809        // All missing fields should have their default values.
810        assert!(!opts.coloured());
811        assert_eq!(opts.output(), &Output::Stdout);
812        assert!(!opts.report_caller());
813        assert_eq!(opts.timestamp_format(), &TSFormat::Standard);
814        assert!(!opts.pad_level());
815        assert_eq!(opts.pad_amount(), 5);
816        assert_eq!(opts.pad_side(), PadSide::Right);
817        assert_eq!(opts.msg_separator(), ": ");
818        assert_eq!(opts.arrow_char(), "▶");
819    }
820}