xianyun_executor/logging/
config.rs1use std::io::{self, Write};
13use std::path::PathBuf;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum Format {
18 #[default]
19 Text,
20 Json,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Default)]
25pub enum Output {
26 #[default]
27 Stderr,
28 Stdout,
29 File(PathBuf),
30}
31
32#[derive(Debug, thiserror::Error)]
34pub enum ConfigError {
35 #[error("invalid format: must be text or json, got {0:?}")]
36 InvalidFormat(Format),
37 #[error("output cannot be empty")]
38 EmptyOutput,
39}
40
41#[derive(Debug, Clone)]
43pub struct Config {
44 pub format: Format,
45 pub level: tracing::Level,
46 pub color: Option<bool>,
49 pub output: Output,
50 pub add_source: bool,
51}
52
53impl Default for Config {
54 fn default() -> Self {
55 Config {
57 format: Format::Text,
58 level: tracing::Level::INFO,
59 color: None,
60 output: Output::Stderr,
61 add_source: false,
62 }
63 }
64}
65
66impl Config {
67 pub fn validate(&self) -> Result<(), ConfigError> {
72 Ok(())
73 }
74
75 pub fn get_writer(&self) -> io::Result<Box<dyn Write + Send>> {
80 match &self.output {
81 Output::Stderr => Ok(Box::new(io::stderr())),
82 Output::Stdout => Ok(Box::new(io::stdout())),
83 Output::File(path) => {
84 let file = std::fs::OpenOptions::new()
85 .create(true)
86 .append(true)
87 .open(path)?;
88 Ok(Box::new(file))
89 }
90 }
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn new_config_defaults() {
100 let cfg = Config::default();
101
102 assert_eq!(cfg.format, Format::Text, "Default format should be text");
103 assert_eq!(
104 cfg.level,
105 tracing::Level::INFO,
106 "Default level should be info"
107 );
108 assert_eq!(cfg.color, None, "Default color should be nil (auto-detect)");
109 assert_eq!(
110 cfg.output,
111 Output::Stderr,
112 "Default output should be stderr"
113 );
114 assert!(!cfg.add_source, "Default AddSource should be false");
115 }
116
117 #[test]
118 fn config_validate_cases() {
119 struct Case {
120 name: &'static str,
121 config: Config,
122 should_err: bool,
123 }
124
125 let cases = vec![
126 Case {
127 name: "valid text format",
128 config: Config {
129 format: Format::Text,
130 level: tracing::Level::INFO,
131 color: None,
132 output: Output::Stderr,
133 add_source: false,
134 },
135 should_err: false,
136 },
137 Case {
138 name: "valid json format",
139 config: Config {
140 format: Format::Json,
141 level: tracing::Level::INFO,
142 color: None,
143 output: Output::Stdout,
144 add_source: false,
145 },
146 should_err: false,
147 },
148 Case {
149 name: "valid file output",
150 config: Config {
151 format: Format::Text,
152 level: tracing::Level::INFO,
153 color: None,
154 output: Output::File("/tmp/test.log".into()),
155 add_source: false,
156 },
157 should_err: false,
158 },
159 ];
167
168 for case in cases {
169 let result = case.config.validate();
170 if case.should_err {
171 assert!(result.is_err(), "{}", case.name);
172 } else {
173 assert!(result.is_ok(), "{}", case.name);
174 }
175 }
176 }
177
178 #[test]
179 fn config_get_writer_cases() {
180 for output in [Output::Stderr, Output::Stdout] {
181 let cfg = Config {
182 format: Format::Text,
183 level: tracing::Level::INFO,
184 color: None,
185 output,
186 add_source: false,
187 };
188
189 let writer = cfg.get_writer();
190 assert!(writer.is_ok(), "Writer should not error");
191 }
192 }
193
194 #[test]
195 fn config_get_writer_file() {
196 let tmp_dir = tempfile::tempdir().unwrap();
197 let log_file = tmp_dir.path().join("test.log");
198
199 let cfg = Config {
200 format: Format::Text,
201 level: tracing::Level::INFO,
202 color: None,
203 output: Output::File(log_file.clone()),
204 add_source: false,
205 };
206
207 let mut writer = cfg.get_writer().unwrap();
208 writer.write_all(b"test log message\n").unwrap();
209 drop(writer);
210
211 let content = std::fs::read_to_string(&log_file).unwrap();
212 assert!(content.contains("test log message"));
213 }
214
215 #[test]
216 fn config_get_writer_invalid_file() {
217 let cfg = Config {
218 format: Format::Text,
219 level: tracing::Level::INFO,
220 color: None,
221 output: Output::File("/nonexistent/directory/test.log".into()),
222 add_source: false,
223 };
224
225 assert!(cfg.get_writer().is_err());
226 }
227}