1use serde::{Deserialize, Serialize};
4use std::time::{Duration, SystemTime};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ScanProgress {
9 pub total: usize,
11 pub completed: usize,
13 pub skipped: usize,
15 pub findings: usize,
17 pub start_time: SystemTime,
19}
20
21impl ScanProgress {
22 #[must_use]
24 pub fn new(total: usize) -> Self {
25 Self {
26 total,
27 completed: 0,
28 skipped: 0,
29 findings: 0,
30 start_time: SystemTime::now(),
31 }
32 }
33
34 pub fn record_completed(&mut self) {
36 self.completed += 1;
37 }
38
39 pub fn record_skipped(&mut self) {
41 self.skipped += 1;
42 }
43
44 pub fn record_findings(&mut self, findings: usize) {
46 self.findings += findings;
47 }
48
49 #[must_use]
51 #[allow(clippy::cast_precision_loss)]
52 pub fn rate(&self) -> f64 {
53 let elapsed = self.start_time.elapsed().map(|d| d.as_secs_f64()).unwrap_or(0.0);
54 if elapsed <= f64::EPSILON {
55 return 0.0;
56 }
57
58 (self.completed + self.skipped) as f64 / elapsed
59 }
60
61 #[must_use]
63 #[allow(clippy::cast_precision_loss)]
64 pub fn eta(&self) -> Duration {
65 let processed = self.completed + self.skipped;
66 if self.total <= processed {
67 return Duration::ZERO;
68 }
69
70 let rate = self.rate();
71 if rate <= f64::EPSILON {
72 return Duration::ZERO;
73 }
74
75 Duration::from_secs_f64((self.total - processed) as f64 / rate)
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::ScanProgress;
82 use std::time::{Duration, SystemTime};
83
84 #[test]
85 fn new_initializes_progress() {
86 let progress = ScanProgress::new(10);
87 assert_eq!(progress.total, 10);
88 assert_eq!(progress.completed, 0);
89 assert_eq!(progress.skipped, 0);
90 assert_eq!(progress.findings, 0);
91 }
92
93 #[test]
94 fn record_completed_increments_completed_count() {
95 let mut progress = ScanProgress::new(10);
96 progress.record_completed();
97 assert_eq!(progress.completed, 1);
98 }
99
100 #[test]
101 fn record_skipped_increments_skipped_count() {
102 let mut progress = ScanProgress::new(10);
103 progress.record_skipped();
104 assert_eq!(progress.skipped, 1);
105 }
106
107 #[test]
108 fn record_findings_accumulates_findings() {
109 let mut progress = ScanProgress::new(10);
110 progress.record_findings(2);
111 progress.record_findings(3);
112 assert_eq!(progress.findings, 5);
113 }
114
115 #[test]
116 fn rate_returns_targets_per_second() {
117 let mut progress = ScanProgress::new(10);
118 progress.completed = 4;
119 progress.skipped = 2;
120 progress.start_time = SystemTime::now()
121 .checked_sub(Duration::from_secs(2))
122 .expect("subtract fixed start time");
123
124 let rate = progress.rate();
125 assert!(rate >= 2.9 && rate <= 3.1, "unexpected rate: {rate}");
126 }
127
128 #[test]
129 fn rate_returns_zero_when_elapsed_is_too_small() {
130 let progress = ScanProgress::new(10);
131 let rate = progress.rate();
132 assert!(rate >= 0.0);
133 }
134
135 #[test]
136 fn eta_returns_zero_when_scan_is_complete() {
137 let mut progress = ScanProgress::new(5);
138 progress.completed = 3;
139 progress.skipped = 2;
140 progress.start_time = SystemTime::now()
141 .checked_sub(Duration::from_secs(2))
142 .expect("subtract fixed start time");
143
144 assert_eq!(progress.eta(), Duration::ZERO);
145 }
146
147 #[test]
148 fn eta_estimates_remaining_time() {
149 let mut progress = ScanProgress::new(10);
150 progress.completed = 4;
151 progress.skipped = 2;
152 progress.start_time = SystemTime::now()
153 .checked_sub(Duration::from_secs(2))
154 .expect("subtract fixed start time");
155
156 let eta = progress.eta();
157 assert!(eta >= Duration::from_millis(1200));
158 assert!(eta <= Duration::from_millis(1500));
159 }
160
161 #[test]
162 fn progress_is_serializable() {
163 let progress = ScanProgress {
164 total: 10,
165 completed: 3,
166 skipped: 1,
167 findings: 2,
168 start_time: SystemTime::UNIX_EPOCH,
169 };
170 let payload = serde_json::to_string(&progress).unwrap();
171 let decoded: ScanProgress = serde_json::from_str(&payload).unwrap();
172 assert_eq!(decoded.total, 10);
173 assert_eq!(decoded.completed, 3);
174 assert_eq!(decoded.skipped, 1);
175 assert_eq!(decoded.findings, 2);
176 assert_eq!(decoded.start_time, SystemTime::UNIX_EPOCH);
177 }
178}