exception_collector/
config.rs1use std::{collections::HashMap, path::Path, time::Duration};
7
8use serde::Deserialize;
9
10use crate::CollectorError;
11
12#[derive(Debug, Deserialize)]
16struct RepoMapFile {
17 components: HashMap<String, String>,
19}
20
21#[derive(Debug, Clone)]
28pub struct CollectorConfig {
29 pub log_dirs: Vec<String>,
31 pub scan_interval_secs: u64,
33 pub daily_trigger_hour: u8,
35 pub distinct_count_threshold: u32,
37 pub cooldown_duration: Duration,
39 pub max_retries: u32,
41 pub retry_base_delay: Duration,
43 pub repo_map: HashMap<String, String>,
45}
46
47impl Default for CollectorConfig {
48 fn default() -> Self {
49 Self {
50 log_dirs: Vec::new(),
51 scan_interval_secs: 300,
52 daily_trigger_hour: 2,
53 distinct_count_threshold: 20,
54 cooldown_duration: Duration::from_hours(1),
55 max_retries: 3,
56 retry_base_delay: Duration::from_mins(5),
57 repo_map: HashMap::new(),
58 }
59 }
60}
61
62impl CollectorConfig {
63 #[must_use]
65 pub fn new() -> Self {
66 Self::default()
67 }
68
69 #[allow(clippy::disallowed_methods, reason = "同步初始化函数,非 async 上下文")]
78 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, CollectorError> {
79 let path = path.as_ref();
80 let content = std::fs::read_to_string(path).map_err(|source| CollectorError::Io {
81 source,
82 path: path.to_path_buf(),
83 })?;
84 let file: RepoMapFile =
85 toml::from_str(&content).map_err(|source| CollectorError::TomlParse {
86 source,
87 path: path.to_path_buf(),
88 })?;
89 Ok(Self {
90 repo_map: file.components,
91 ..Default::default()
92 })
93 }
94
95 #[must_use]
99 pub fn repo_for(&self, component: &str) -> Option<&str> {
100 self.repo_map.get(component).map(String::as_str)
101 }
102}
103
104#[cfg(test)]
107#[allow(
108 clippy::unwrap_used,
109 clippy::unwrap_in_result,
110 clippy::expect_used,
111 clippy::panic,
112 clippy::pedantic,
113 clippy::disallowed_methods,
114 clippy::indexing_slicing,
115 reason = "test module relaxes production lint strictness"
116)]
117mod tests {
118 use std::path::PathBuf;
119
120 use super::*;
121
122 #[test]
123 fn test_collector_config_should_have_sane_defaults() {
124 let config = CollectorConfig::default();
125
126 assert!(config.log_dirs.is_empty());
127 assert_eq!(config.scan_interval_secs, 300);
128 assert_eq!(config.daily_trigger_hour, 2);
129 assert_eq!(config.distinct_count_threshold, 20);
130 assert_eq!(config.cooldown_duration, Duration::from_hours(1));
131 assert_eq!(config.max_retries, 3);
132 assert_eq!(config.retry_base_delay, Duration::from_mins(5));
133 assert!(config.repo_map.is_empty());
134 }
135
136 #[test]
137 fn test_collector_config_new_should_equal_default() {
138 let config = CollectorConfig::new();
139 let default = CollectorConfig::default();
140
141 assert_eq!(config.daily_trigger_hour, default.daily_trigger_hour);
142 assert_eq!(
143 config.distinct_count_threshold,
144 default.distinct_count_threshold
145 );
146 assert_eq!(config.max_retries, default.max_retries);
147 }
148
149 #[test]
150 fn test_collector_config_should_be_cloneable() {
151 let mut config = CollectorConfig::default();
152 config
153 .repo_map
154 .insert("comp".to_string(), "owner/repo".to_string());
155 let cloned = config.clone();
156
157 assert_eq!(cloned.daily_trigger_hour, config.daily_trigger_hour);
158 assert_eq!(cloned.repo_map, config.repo_map);
159 }
160
161 #[test]
162 fn test_collector_config_from_file_should_parse_repo_map() {
163 let dir = tempfile::tempdir().expect("create temp dir");
164 let path = dir.path().join("repo-map.toml");
165 std::fs::write(
166 &path,
167 r#"
168[components]
169"token-fleet-switch" = "TokenFleet-AI/token-fleet-switch"
170"agent-proxy-rust" = "TokenFleet-AI/agent-proxy-rust"
171"#,
172 )
173 .expect("write test file");
174
175 let config = CollectorConfig::from_file(&path).expect("parse config");
176
177 assert_eq!(config.repo_map.len(), 2);
178 assert_eq!(
179 config
180 .repo_map
181 .get("token-fleet-switch")
182 .map(String::as_str),
183 Some("TokenFleet-AI/token-fleet-switch")
184 );
185 assert_eq!(
186 config.repo_map.get("agent-proxy-rust").map(String::as_str),
187 Some("TokenFleet-AI/agent-proxy-rust")
188 );
189 assert_eq!(config.daily_trigger_hour, 2);
191 assert_eq!(config.distinct_count_threshold, 20);
192 }
193
194 #[test]
195 fn test_collector_config_from_file_should_error_on_missing_file() {
196 let result = CollectorConfig::from_file("/nonexistent/path/repo-map.toml");
197
198 assert!(result.is_err());
199 let err = result.expect_err("expected error for missing file");
200 match err {
201 CollectorError::Io { path, .. } => {
202 assert_eq!(path, PathBuf::from("/nonexistent/path/repo-map.toml"));
203 }
204 _ => panic!("expected Io error, got: {err:?}"),
205 }
206 }
207
208 #[test]
209 fn test_collector_config_from_file_should_error_on_invalid_toml() {
210 let dir = tempfile::tempdir().expect("create temp dir");
211 let path = dir.path().join("bad.toml");
212 std::fs::write(&path, "this is not valid toml {{{").expect("write bad file");
213
214 let result = CollectorConfig::from_file(&path);
215
216 assert!(result.is_err());
217 let err = result.expect_err("expected error for invalid TOML");
218 match err {
219 CollectorError::TomlParse { path: p, .. } => {
220 assert_eq!(p, path);
221 }
222 _ => panic!("expected TomlParse error, got: {err:?}"),
223 }
224 }
225
226 #[test]
227 fn test_collector_config_from_file_should_error_when_components_key_missing() {
228 let dir = tempfile::tempdir().expect("create temp dir");
229 let path = dir.path().join("no-components.toml");
230 std::fs::write(&path, "[other]\nkey = \"value\"\n").expect("write file");
231
232 let result = CollectorConfig::from_file(&path);
233
234 assert!(result.is_err());
235 }
236
237 #[test]
238 fn test_collector_config_from_file_should_handle_empty_components() {
239 let dir = tempfile::tempdir().expect("create temp dir");
240 let path = dir.path().join("empty.toml");
241 std::fs::write(&path, "[components]\n").expect("write file");
242
243 let config = CollectorConfig::from_file(&path).expect("parse empty components");
244
245 assert!(config.repo_map.is_empty());
246 assert_eq!(config.daily_trigger_hour, 2);
247 }
248
249 #[test]
252 fn test_repo_for_should_return_repo_for_known_component() {
253 let mut config = CollectorConfig::default();
254 config.repo_map.insert(
255 "token-fleet-switch".to_string(),
256 "TokenFleet-AI/token-fleet-switch".to_string(),
257 );
258
259 assert_eq!(
260 config.repo_for("token-fleet-switch"),
261 Some("TokenFleet-AI/token-fleet-switch")
262 );
263 }
264
265 #[test]
266 fn test_repo_for_should_return_none_for_unknown_component() {
267 let config = CollectorConfig::default();
268
269 assert_eq!(config.repo_for("unknown-component"), None);
270 }
271
272 #[test]
273 fn test_repo_for_should_return_none_for_empty_map() {
274 let config = CollectorConfig::new();
275
276 assert_eq!(config.repo_for("anything"), None);
277 }
278}