1use std::fmt;
12use std::time::Duration;
13
14use crate::capture::Capture;
15use crate::device::Device;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Outcome {
20 Exact,
22 OffSize,
24 Failed,
26}
27
28impl Outcome {
29 pub fn as_str(self) -> &'static str {
30 match self {
31 Outcome::Exact => "exact",
32 Outcome::OffSize => "off-size",
33 Outcome::Failed => "failed",
34 }
35 }
36
37 pub fn is_bad(self) -> bool {
39 !matches!(self, Outcome::Exact)
40 }
41}
42
43impl fmt::Display for Outcome {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.write_str(self.as_str())
46 }
47}
48
49#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
51pub struct Summary {
52 pub exact: usize,
53 pub off_size: usize,
54 pub failed: usize,
55}
56
57impl Summary {
58 pub fn total(&self) -> usize {
59 self.exact + self.off_size + self.failed
60 }
61
62 pub fn ok(&self) -> bool {
68 self.total() > 0 && self.off_size == 0 && self.failed == 0
69 }
70
71 fn record(&mut self, o: Outcome) {
72 match o {
73 Outcome::Exact => self.exact += 1,
74 Outcome::OffSize => self.off_size += 1,
75 Outcome::Failed => self.failed += 1,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy)]
86pub struct DeviceEvent<'a> {
87 pub index: usize,
89 pub total: usize,
90 pub device: &'a Device,
91 pub outcome: Outcome,
92 pub capture: Option<&'a Capture>,
94 pub elapsed: Duration,
95 pub error: Option<&'a str>,
97}
98
99pub trait Progress {
104 fn run_started(&mut self, _total: usize, _url: &str) {}
106
107 fn device_started(&mut self, _index: usize, _total: usize, _device: &Device) {}
109
110 fn device_finished(&mut self, _event: &DeviceEvent<'_>) {}
112
113 fn run_finished(&mut self, _summary: Summary, _elapsed: Duration) {}
115}
116
117#[derive(Debug, Default, Clone, Copy)]
119pub struct Silent;
120
121impl Progress for Silent {}
122
123#[derive(Debug, Default, Clone)]
126pub struct Collector {
127 pub summary: Summary,
128 pub events: Vec<(String, Outcome, u128)>,
129}
130
131impl Progress for Collector {
132 fn device_finished(&mut self, e: &DeviceEvent<'_>) {
133 self.summary.record(e.outcome);
134 self.events
135 .push((e.device.id.clone(), e.outcome, e.elapsed.as_millis()));
136 }
137}
138
139#[derive(Debug, Default, Clone, Copy)]
141pub struct Tally(pub Summary);
142
143impl Progress for Tally {
144 fn device_finished(&mut self, e: &DeviceEvent<'_>) {
145 self.0.record(e.outcome);
146 }
147}
148
149pub fn bar(current: usize, total: usize, width: usize) -> String {
154 if total == 0 || width == 0 {
155 return String::new();
156 }
157 let filled = (current.min(total) * width) / total;
158 let mut s = String::with_capacity(width);
159 for i in 0..width {
160 s.push(if i < filled { '#' } else { '.' });
161 }
162 s
163}
164
165pub fn human(d: Duration) -> String {
167 let ms = d.as_millis();
168 if ms < 1000 {
169 format!("{ms}ms")
170 } else if ms < 60_000 {
171 format!("{:.1}s", d.as_secs_f64())
172 } else {
173 format!("{}m{:02}s", d.as_secs() / 60, d.as_secs() % 60)
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn empty_run_is_not_a_success() {
183 assert!(!Summary::default().ok());
185 }
186
187 #[test]
188 fn clean_run_is_a_success() {
189 let s = Summary {
190 exact: 3,
191 ..Default::default()
192 };
193 assert!(s.ok());
194 assert_eq!(s.total(), 3);
195 }
196
197 #[test]
198 fn any_problem_fails_the_run() {
199 assert!(!Summary {
200 exact: 5,
201 off_size: 1,
202 failed: 0
203 }
204 .ok());
205 assert!(!Summary {
206 exact: 5,
207 off_size: 0,
208 failed: 1
209 }
210 .ok());
211 }
212
213 #[test]
214 fn bar_endpoints_are_exact() {
215 assert_eq!(bar(0, 10, 10), "..........");
216 assert_eq!(bar(10, 10, 10), "##########");
217 assert_eq!(bar(5, 10, 10), "#####.....");
218 }
219
220 #[test]
221 fn bar_never_overflows_its_width() {
222 for cur in 0..30 {
223 assert_eq!(bar(cur, 10, 8).chars().count(), 8);
224 }
225 assert_eq!(bar(99, 10, 8), "########");
227 }
228
229 #[test]
230 fn bar_handles_degenerate_input() {
231 assert_eq!(bar(1, 0, 10), "");
232 assert_eq!(bar(1, 10, 0), "");
233 }
234
235 #[test]
236 fn durations_read_naturally() {
237 assert_eq!(human(Duration::from_millis(250)), "250ms");
238 assert_eq!(human(Duration::from_millis(1500)), "1.5s");
239 assert_eq!(human(Duration::from_secs(125)), "2m05s");
240 }
241
242 #[test]
243 fn outcome_badness() {
244 assert!(!Outcome::Exact.is_bad());
245 assert!(Outcome::OffSize.is_bad());
246 assert!(Outcome::Failed.is_bad());
247 }
248
249 #[test]
250 fn collector_records_every_device() {
251 let d = crate::device::builtin().into_iter().next().unwrap();
252 let mut c = Collector::default();
253 for outcome in [Outcome::Exact, Outcome::Failed] {
254 c.device_finished(&DeviceEvent {
255 index: 1,
256 total: 2,
257 device: &d,
258 outcome,
259 capture: None,
260 elapsed: Duration::from_millis(5),
261 error: None,
262 });
263 }
264 assert_eq!(c.summary.total(), 2);
265 assert_eq!(c.summary.failed, 1);
266 assert_eq!(c.events.len(), 2);
267 assert!(!c.summary.ok());
268 }
269}