1use std::time::{Duration, Instant};
13
14use crate::log;
15use crate::model::IssueRun;
16use crate::spend;
17
18const BAR_WIDTH: usize = 20;
19
20pub struct Progress {
21 done: usize,
22 total: usize,
23 started: Instant,
24}
25
26impl Progress {
27 pub fn new(total: usize) -> Self {
28 Self {
29 done: 0,
30 total,
31 started: Instant::now(),
32 }
33 }
34
35 pub fn add(&mut self, items: usize) {
37 self.total += items;
38 }
39
40 pub fn finished(&mut self, run: &IssueRun) {
42 self.done += 1;
43 self.total = self.total.max(self.done);
44 log!("{}", self.line(run, self.started.elapsed()));
45 }
46
47 fn line(&self, run: &IssueRun, elapsed: Duration) -> String {
48 let left = self.total - self.done;
49 format!(
50 "{} {} of {} done, #{} {}, {left} left, {} elapsed",
51 bar(self.done, self.total),
52 self.done,
53 self.total,
54 run.issue,
55 run.status,
56 spend::clock(elapsed)
57 )
58 }
59}
60
61fn bar(done: usize, total: usize) -> String {
62 let filled = (done * BAR_WIDTH)
63 .checked_div(total)
64 .unwrap_or(0)
65 .min(BAR_WIDTH);
66 format!("[{}{}]", "#".repeat(filled), ".".repeat(BAR_WIDTH - filled))
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use crate::model::Status;
73
74 fn run(issue: i64, status: Status) -> IssueRun {
75 let mut run = IssueRun::new(issue, "title");
76 run.status = status;
77 run
78 }
79
80 #[test]
81 fn the_bar_fills_in_proportion_and_never_overflows() {
82 assert_eq!(format!("[{}]", ".".repeat(20)), bar(0, 5));
83 assert_eq!(format!("[{}{}]", "#".repeat(8), ".".repeat(12)), bar(2, 5));
84 assert_eq!(format!("[{}]", "#".repeat(20)), bar(5, 5));
85 assert_eq!(format!("[{}]", "#".repeat(20)), bar(7, 5));
86 assert_eq!(format!("[{}]", ".".repeat(20)), bar(0, 0));
87 }
88
89 #[test]
90 fn a_line_names_the_item_just_finished_and_what_is_left() {
91 let mut progress = Progress::new(5);
92 progress.done = 2;
93 let line = progress.line(&run(123, Status::Approved), Duration::from_secs(4000));
94 assert!(
95 line.ends_with("2 of 5 done, #123 approved, 3 left, 66m40s elapsed"),
96 "{line}"
97 );
98 }
99
100 #[test]
101 fn the_total_grows_as_waves_join_and_never_trails_what_is_done() {
102 let mut progress = Progress::new(1);
103 progress.finished(&run(1, Status::Merged));
104 progress.add(2);
105 assert_eq!((1, 3), (progress.done, progress.total));
106 progress.finished(&run(2, Status::Escalated));
107 progress.finished(&run(3, Status::Approved));
108 progress.finished(&run(4, Status::Error));
109 assert_eq!((4, 4), (progress.done, progress.total));
110 }
111}