1use serde::{Deserialize, Serialize};
2use std::time::Duration;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6#[serde(rename_all = "kebab-case")]
7pub struct TimeoutConfig {
8 pub connect_timeout: u64,
10 pub read_timeout: u64,
12 pub total_timeout: u64,
14}
15
16impl TimeoutConfig {
17 pub fn new() -> Self {
19 Self::default()
20 }
21
22 pub fn http_simple() -> Self {
24 Self {
25 connect_timeout: 30,
26 read_timeout: 60,
27 total_timeout: 300,
28 }
29 }
30
31 pub fn http_large_file() -> Self {
33 Self {
34 connect_timeout: 60,
35 read_timeout: 300,
36 total_timeout: 3600,
37 }
38 }
39
40 pub fn git_operation() -> Self {
42 Self {
43 connect_timeout: 120,
44 read_timeout: 180,
45 total_timeout: 1800,
46 }
47 }
48
49 pub fn connect_duration(&self) -> Duration {
51 Duration::from_secs(self.connect_timeout)
52 }
53
54 pub fn read_duration(&self) -> Duration {
55 Duration::from_secs(self.read_timeout)
56 }
57
58 pub fn total_duration(&self) -> Duration {
59 Duration::from_secs(self.total_timeout)
60 }
61
62 pub fn validate(&self) -> bool {
64 self.connect_timeout > 0 && self.read_timeout > 0 && self.total_timeout > 0
65 }
66}
67
68impl Default for TimeoutConfig {
69 fn default() -> Self {
70 Self {
71 connect_timeout: 30,
72 read_timeout: 60,
73 total_timeout: 300,
74 }
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct ProgressTracker {
81 last_activity: std::time::Instant,
82 timeout_duration: Duration,
83 total_downloaded: u64,
84 total_expected: Option<u64>,
85}
86
87impl ProgressTracker {
88 pub fn new(timeout_duration: Duration) -> Self {
90 Self {
91 last_activity: std::time::Instant::now(),
92 timeout_duration,
93 total_downloaded: 0,
94 total_expected: None,
95 }
96 }
97
98 pub fn update(&mut self, bytes: u64, total: Option<u64>) {
100 self.last_activity = std::time::Instant::now();
101 self.total_downloaded = bytes;
102 self.total_expected = total;
103 }
104
105 pub fn reset(&mut self) {
106 self.last_activity = std::time::Instant::now();
107 }
108
109 pub fn has_timed_out(&self) -> bool {
111 self.last_activity.elapsed() > self.timeout_duration
112 }
113
114 pub fn progress_percent(&self) -> Option<f64> {
116 match self.total_expected {
117 Some(total) if total > 0 => Some((self.total_downloaded as f64 / total as f64) * 100.0),
118 _ => None,
119 }
120 }
121
122 pub fn downloaded(&self) -> u64 {
124 self.total_downloaded
125 }
126
127 pub fn total_expected(&self) -> Option<u64> {
129 self.total_expected
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use std::time::Duration;
137
138 mod timeout_config {
140 use super::*;
141
142 #[test]
143 fn test_default_config() {
144 let config = TimeoutConfig::default();
145 assert_eq!(config.connect_timeout, 30);
146 assert_eq!(config.read_timeout, 60);
147 assert_eq!(config.total_timeout, 300);
148 }
149
150 #[test]
151 fn test_new_config() {
152 let config = TimeoutConfig::new();
153 assert_eq!(config, TimeoutConfig::default());
154 }
155
156 #[test]
157 fn test_http_simple_config() {
158 let config = TimeoutConfig::http_simple();
159 assert_eq!(config.connect_timeout, 30);
160 assert_eq!(config.read_timeout, 60);
161 assert_eq!(config.total_timeout, 300);
162 }
163
164 #[test]
165 fn test_http_large_file_config() {
166 let config = TimeoutConfig::http_large_file();
167 assert_eq!(config.connect_timeout, 60);
168 assert_eq!(config.read_timeout, 300);
169 assert_eq!(config.total_timeout, 3600);
170 }
171
172 #[test]
173 fn test_git_operation_config() {
174 let config = TimeoutConfig::git_operation();
175 assert_eq!(config.connect_timeout, 120);
176 assert_eq!(config.read_timeout, 180);
177 assert_eq!(config.total_timeout, 1800);
178 }
179
180 #[test]
181 fn test_connect_duration() {
182 let config = TimeoutConfig {
183 connect_timeout: 45,
184 read_timeout: 90,
185 total_timeout: 180,
186 };
187 assert_eq!(config.connect_duration(), Duration::from_secs(45));
188 }
189
190 #[test]
191 fn test_read_duration() {
192 let config = TimeoutConfig {
193 connect_timeout: 30,
194 read_timeout: 120,
195 total_timeout: 300,
196 };
197 assert_eq!(config.read_duration(), Duration::from_secs(120));
198 }
199
200 #[test]
201 fn test_total_duration() {
202 let config = TimeoutConfig {
203 connect_timeout: 30,
204 read_timeout: 60,
205 total_timeout: 600,
206 };
207 assert_eq!(config.total_duration(), Duration::from_secs(600));
208 }
209
210 #[test]
211 fn test_duration_conversions_zero_values() {
212 let config = TimeoutConfig {
213 connect_timeout: 0,
214 read_timeout: 0,
215 total_timeout: 0,
216 };
217 assert_eq!(config.connect_duration(), Duration::from_secs(0));
218 assert_eq!(config.read_duration(), Duration::from_secs(0));
219 assert_eq!(config.total_duration(), Duration::from_secs(0));
220 }
221
222 #[test]
223 fn test_duration_conversions_large_values() {
224 let config = TimeoutConfig {
225 connect_timeout: u64::MAX,
226 read_timeout: u64::MAX,
227 total_timeout: u64::MAX,
228 };
229 assert_eq!(config.connect_duration(), Duration::from_secs(u64::MAX));
230 assert_eq!(config.read_duration(), Duration::from_secs(u64::MAX));
231 assert_eq!(config.total_duration(), Duration::from_secs(u64::MAX));
232 }
233
234 #[test]
235 fn test_validation_valid_config() {
236 let config = TimeoutConfig {
237 connect_timeout: 10,
238 read_timeout: 20,
239 total_timeout: 30,
240 };
241 assert!(config.validate());
242 }
243
244 #[test]
245 fn test_validation_zero_connect_timeout() {
246 let config = TimeoutConfig {
247 connect_timeout: 0,
248 read_timeout: 60,
249 total_timeout: 300,
250 };
251 assert!(!config.validate());
252 }
253
254 #[test]
255 fn test_validation_zero_read_timeout() {
256 let config = TimeoutConfig {
257 connect_timeout: 30,
258 read_timeout: 0,
259 total_timeout: 300,
260 };
261 assert!(!config.validate());
262 }
263
264 #[test]
265 fn test_validation_zero_total_timeout() {
266 let config = TimeoutConfig {
267 connect_timeout: 30,
268 read_timeout: 60,
269 total_timeout: 0,
270 };
271 assert!(!config.validate());
272 }
273
274 #[test]
275 fn test_validation_all_zero_values() {
276 let config = TimeoutConfig {
277 connect_timeout: 0,
278 read_timeout: 0,
279 total_timeout: 0,
280 };
281 assert!(!config.validate());
282 }
283
284 #[test]
285 fn test_validation_edge_case_one_second() {
286 let config = TimeoutConfig {
287 connect_timeout: 1,
288 read_timeout: 1,
289 total_timeout: 1,
290 };
291 assert!(config.validate());
292 }
293
294 #[test]
295 fn test_partial_eq() {
296 let config1 = TimeoutConfig {
297 connect_timeout: 30,
298 read_timeout: 60,
299 total_timeout: 300,
300 };
301 let config2 = TimeoutConfig {
302 connect_timeout: 30,
303 read_timeout: 60,
304 total_timeout: 300,
305 };
306 let config3 = TimeoutConfig {
307 connect_timeout: 31,
308 read_timeout: 60,
309 total_timeout: 300,
310 };
311
312 assert_eq!(config1, config2);
313 assert_ne!(config1, config3);
314 }
315
316 #[test]
317 fn test_clone() {
318 let config = TimeoutConfig {
319 connect_timeout: 45,
320 read_timeout: 90,
321 total_timeout: 180,
322 };
323 let cloned_config = config.clone();
324 assert_eq!(config, cloned_config);
325 }
326
327 #[test]
328 fn test_debug_format() {
329 let config = TimeoutConfig {
330 connect_timeout: 30,
331 read_timeout: 60,
332 total_timeout: 300,
333 };
334 let debug_str = format!("{config:?}");
335 assert!(debug_str.contains("connect_timeout: 30"));
336 assert!(debug_str.contains("read_timeout: 60"));
337 assert!(debug_str.contains("total_timeout: 300"));
338 }
339 }
340
341 mod progress_tracker {
343 use super::*;
344
345 #[test]
346 fn test_new_tracker() {
347 let duration = Duration::from_secs(10);
348 let tracker = ProgressTracker::new(duration);
349
350 assert_eq!(tracker.timeout_duration, duration);
351 assert_eq!(tracker.total_downloaded, 0);
352 assert_eq!(tracker.total_expected, None);
353 assert!(!tracker.has_timed_out());
354 }
355
356 #[test]
357 fn test_update_with_total() {
358 let mut tracker = ProgressTracker::new(Duration::from_secs(10));
359 tracker.update(50, Some(100));
360
361 assert_eq!(tracker.downloaded(), 50);
362 assert_eq!(tracker.total_expected(), Some(100));
363 assert_eq!(tracker.progress_percent(), Some(50.0));
364 assert!(!tracker.has_timed_out());
365 }
366
367 #[test]
368 fn test_update_without_total() {
369 let mut tracker = ProgressTracker::new(Duration::from_secs(10));
370 tracker.update(50, None);
371
372 assert_eq!(tracker.downloaded(), 50);
373 assert_eq!(tracker.total_expected(), None);
374 assert_eq!(tracker.progress_percent(), None);
375 assert!(!tracker.has_timed_out());
376 }
377
378 #[test]
379 fn test_progress_percent_zero_total() {
380 let mut tracker = ProgressTracker::new(Duration::from_secs(10));
381 tracker.update(50, Some(0));
382
383 assert_eq!(tracker.progress_percent(), None);
384 }
385
386 #[test]
387 fn test_progress_percent_complete() {
388 let mut tracker = ProgressTracker::new(Duration::from_secs(10));
389 tracker.update(100, Some(100));
390
391 assert_eq!(tracker.progress_percent(), Some(100.0));
392 }
393
394 #[test]
395 fn test_progress_percent_half() {
396 let mut tracker = ProgressTracker::new(Duration::from_secs(10));
397 tracker.update(50, Some(100));
398
399 assert_eq!(tracker.progress_percent(), Some(50.0));
400 }
401
402 #[test]
403 fn test_progress_percent_partial() {
404 let mut tracker = ProgressTracker::new(Duration::from_secs(10));
405 tracker.update(33, Some(100));
406
407 let percent = tracker.progress_percent().unwrap();
408 assert!((percent - 33.0).abs() < 0.01);
409 }
410
411 #[test]
412 fn test_progress_percent_large_numbers() {
413 let mut tracker = ProgressTracker::new(Duration::from_secs(10));
414 tracker.update(u64::MAX / 2, Some(u64::MAX));
415
416 let percent = tracker.progress_percent().unwrap();
417 assert!((percent - 50.0).abs() < 0.01);
418 }
419
420 #[test]
421 fn test_reset() {
422 let mut tracker = ProgressTracker::new(Duration::from_millis(100));
423
424 tracker.update(50, Some(100));
426 assert_eq!(tracker.downloaded(), 50);
427
428 std::thread::sleep(Duration::from_millis(50));
430 let old_time = tracker.last_activity;
431
432 tracker.reset();
434 assert_eq!(tracker.downloaded(), 50); assert_eq!(tracker.total_expected(), Some(100)); assert!(tracker.last_activity > old_time); assert!(!tracker.has_timed_out()); }
439
440 #[test]
441 fn test_timeout_behavior() {
442 let mut tracker = ProgressTracker::new(Duration::from_millis(50));
443
444 assert!(!tracker.has_timed_out());
446
447 tracker.update(10, Some(100));
449 assert!(!tracker.has_timed_out());
450
451 std::thread::sleep(Duration::from_millis(60));
453 assert!(tracker.has_timed_out());
454
455 tracker.update(20, Some(100));
457 assert!(!tracker.has_timed_out());
458 }
459
460 #[test]
461 fn test_zero_timeout_duration() {
462 let tracker = ProgressTracker::new(Duration::from_millis(1));
463
464 let _timed_out = tracker.has_timed_out();
466 std::thread::sleep(Duration::from_millis(2));
468 assert!(tracker.has_timed_out());
469 }
470
471 #[test]
472 fn test_large_timeout_duration() {
473 let tracker = ProgressTracker::new(Duration::from_secs(3600)); assert!(!tracker.has_timed_out());
475 }
476
477 #[test]
478 fn test_multiple_updates() {
479 let mut tracker = ProgressTracker::new(Duration::from_secs(10));
480
481 tracker.update(10, Some(100));
483 assert_eq!(tracker.progress_percent(), Some(10.0));
484
485 tracker.update(25, Some(100));
486 assert_eq!(tracker.progress_percent(), Some(25.0));
487
488 tracker.update(75, Some(100));
489 assert_eq!(tracker.progress_percent(), Some(75.0));
490
491 tracker.update(100, Some(100));
492 assert_eq!(tracker.progress_percent(), Some(100.0));
493
494 assert!(!tracker.has_timed_out());
495 }
496
497 #[test]
498 fn test_update_changes_total_expected() {
499 let mut tracker = ProgressTracker::new(Duration::from_secs(10));
500
501 tracker.update(50, Some(100));
502 assert_eq!(tracker.total_expected(), Some(100));
503 assert_eq!(tracker.progress_percent(), Some(50.0));
504
505 tracker.update(50, Some(200));
507 assert_eq!(tracker.total_expected(), Some(200));
508 assert_eq!(tracker.progress_percent(), Some(25.0));
509 }
510
511 #[test]
512 fn test_clone_tracker() {
513 let mut tracker = ProgressTracker::new(Duration::from_secs(10));
514 tracker.update(50, Some(100));
515
516 let cloned_tracker = tracker.clone();
517
518 assert_eq!(cloned_tracker.downloaded(), 50);
519 assert_eq!(cloned_tracker.total_expected(), Some(100));
520 assert_eq!(cloned_tracker.progress_percent(), Some(50.0));
521 assert!(!cloned_tracker.has_timed_out());
522 }
523
524 #[test]
525 fn test_debug_format_tracker() {
526 let mut tracker = ProgressTracker::new(Duration::from_secs(10));
527 tracker.update(50, Some(100));
528
529 let debug_str = format!("{tracker:?}");
530 assert!(debug_str.contains("last_activity"));
531 assert!(debug_str.contains("timeout_duration"));
532 assert!(debug_str.contains("total_downloaded"));
533 assert!(debug_str.contains("total_expected"));
534 }
535
536 #[test]
537 fn test_tracker_behavior_under_load() {
538 let mut tracker = ProgressTracker::new(Duration::from_millis(100));
539
540 for i in 1..=100 {
542 tracker.update(i, Some(100));
543 assert_eq!(tracker.downloaded(), i);
544
545 let percent = tracker.progress_percent().unwrap();
547 assert!(
548 (percent - i as f64).abs() < 0.0001,
549 "Expected {}, got {} for i={}",
550 i as f64,
551 percent,
552 i
553 );
554
555 assert!(!tracker.has_timed_out());
556
557 std::thread::sleep(Duration::from_millis(1));
559 }
560 }
561
562 #[test]
563 fn test_edge_case_max_values() {
564 let mut tracker = ProgressTracker::new(Duration::from_secs(1));
565
566 tracker.update(u64::MAX, Some(u64::MAX));
567 assert_eq!(tracker.downloaded(), u64::MAX);
568 assert_eq!(tracker.total_expected(), Some(u64::MAX));
569 assert_eq!(tracker.progress_percent(), Some(100.0));
570 }
571
572 #[test]
573 fn test_edge_case_zero_values() {
574 let mut tracker = ProgressTracker::new(Duration::from_secs(1));
575
576 tracker.update(0, Some(0));
577 assert_eq!(tracker.downloaded(), 0);
578 assert_eq!(tracker.total_expected(), Some(0));
579 assert_eq!(tracker.progress_percent(), None);
580
581 tracker.update(0, None);
582 assert_eq!(tracker.downloaded(), 0);
583 assert_eq!(tracker.total_expected(), None);
584 assert_eq!(tracker.progress_percent(), None);
585 }
586 }
587
588 mod serde_tests {
590 use super::*;
591
592 #[test]
593 fn test_serialize_yaml() {
594 let config = TimeoutConfig {
595 connect_timeout: 30,
596 read_timeout: 60,
597 total_timeout: 300,
598 };
599
600 let yaml = serde_yaml::to_string(&config).unwrap();
601 assert!(yaml.contains("connect-timeout: 30"));
602 assert!(yaml.contains("read-timeout: 60"));
603 assert!(yaml.contains("total-timeout: 300"));
604 }
605
606 #[test]
607 fn test_deserialize_yaml() {
608 let yaml = r#"
609connect-timeout: 45
610read-timeout: 90
611total-timeout: 180
612"#;
613
614 let config: TimeoutConfig = serde_yaml::from_str(yaml).unwrap();
615 assert_eq!(config.connect_timeout, 45);
616 assert_eq!(config.read_timeout, 90);
617 assert_eq!(config.total_timeout, 180);
618 }
619
620 #[test]
621 fn test_serialize_json() {
622 let config = TimeoutConfig {
623 connect_timeout: 30,
624 read_timeout: 60,
625 total_timeout: 300,
626 };
627
628 let json = serde_json::to_string(&config).unwrap();
629 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
630
631 assert_eq!(parsed["connect-timeout"], 30);
632 assert_eq!(parsed["read-timeout"], 60);
633 assert_eq!(parsed["total-timeout"], 300);
634 }
635
636 #[test]
637 fn test_deserialize_json() {
638 let json = r#"
639{
640 "connect-timeout": 45,
641 "read-timeout": 90,
642 "total-timeout": 180
643}
644"#;
645
646 let config: TimeoutConfig = serde_json::from_str(json).unwrap();
647 assert_eq!(config.connect_timeout, 45);
648 assert_eq!(config.read_timeout, 90);
649 assert_eq!(config.total_timeout, 180);
650 }
651
652 #[test]
653 fn test_serialize_roundtrip_yaml() {
654 let original = TimeoutConfig {
655 connect_timeout: 120,
656 read_timeout: 240,
657 total_timeout: 600,
658 };
659
660 let yaml = serde_yaml::to_string(&original).unwrap();
661 let deserialized: TimeoutConfig = serde_yaml::from_str(&yaml).unwrap();
662
663 assert_eq!(original, deserialized);
664 }
665
666 #[test]
667 fn test_serialize_roundtrip_json() {
668 let original = TimeoutConfig {
669 connect_timeout: 120,
670 read_timeout: 240,
671 total_timeout: 600,
672 };
673
674 let json = serde_json::to_string(&original).unwrap();
675 let deserialized: TimeoutConfig = serde_json::from_str(&json).unwrap();
676
677 assert_eq!(original, deserialized);
678 }
679
680 #[test]
681 fn test_deserialize_missing_field() {
682 let yaml = "connect-timeout: 30\nread-timeout: 60";
683 let result: Result<TimeoutConfig, _> = serde_yaml::from_str(yaml);
684 assert!(result.is_err());
685 }
686
687 #[test]
688 fn test_deserialize_extra_field() {
689 let yaml = r#"
690connect-timeout: 30
691read-timeout: 60
692total-timeout: 300
693extra-field: "ignored"
694"#;
695
696 let result: Result<TimeoutConfig, _> = serde_yaml::from_str(yaml);
697 assert!(result.is_ok());
699 }
700 }
701}