1use std::fmt;
4use std::path::PathBuf;
5
6use chrono::{Duration, NaiveDateTime};
7use clap::ValueEnum;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct Backup {
18 pub filepath: PathBuf,
20
21 pub creation_date: NaiveDateTime,
23}
24
25impl Backup {
26 pub fn age(&self, now: NaiveDateTime) -> String {
30 let duration = now.signed_duration_since(self.creation_date);
31 let duration_secs = duration.num_seconds();
32
33 let month = Duration::weeks(4).num_seconds();
35 if duration_secs >= 2 * month {
36 return format!("{} months", duration_secs / month);
37 }
38 if duration_secs >= month {
39 return "1 month".into();
40 }
41
42 let week = Duration::weeks(1).num_seconds();
44 if duration_secs >= 2 * week {
45 return format!("{} weeks", duration_secs / week);
46 }
47 if duration_secs >= week {
48 return "1 week".into();
49 }
50
51 let day = Duration::days(1).num_seconds();
53 if duration_secs >= 2 * day {
54 return format!("{} days", duration_secs / day);
55 }
56 if duration_secs >= day {
57 return "1 day".into();
58 }
59
60 let hour = Duration::hours(1).num_seconds();
62 if duration_secs >= 2 * hour {
63 return format!("{} hours", duration_secs / hour);
64 }
65 if duration_secs >= hour {
66 return "1 hour".into();
67 }
68
69 let minute = Duration::minutes(1).num_seconds();
71 if duration_secs >= 2 * minute {
72 return format!("{} minutes", duration_secs / minute);
73 }
74 if duration_secs >= minute {
75 return "1 minute".into();
76 }
77
78 format!("{duration_secs} seconds")
79 }
80}
81
82#[derive(Debug, Clone, ValueEnum)]
84pub enum BackupStatus {
85 Retainable,
87
88 Purgeable,
90}
91
92impl fmt::Display for BackupStatus {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 match self {
95 BackupStatus::Retainable => write!(f, "{:12}", "retainable"),
96 BackupStatus::Purgeable => write!(f, "{:12}", "purgeable"),
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use chrono::NaiveDate;
105
106 fn backup_at(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32) -> Backup {
108 Backup {
109 filepath: PathBuf::from("/tmp/backup.tar.zst"),
110 creation_date: NaiveDate::from_ymd_opt(year, month, day)
111 .unwrap()
112 .and_hms_opt(hour, min, sec)
113 .unwrap(),
114 }
115 }
116
117 fn datetime(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32) -> NaiveDateTime {
118 NaiveDate::from_ymd_opt(year, month, day)
119 .unwrap()
120 .and_hms_opt(hour, min, sec)
121 .unwrap()
122 }
123
124 mod age_formatting {
125 use super::*;
126
127 #[test]
128 fn fresh_backup_shows_seconds() {
129 let backup = backup_at(2024, 6, 15, 10, 30, 0);
130 let now = datetime(2024, 6, 15, 10, 30, 45);
131
132 assert_eq!(backup.age(now), "45 seconds");
133 }
134
135 #[test]
136 fn zero_seconds_is_still_seconds() {
137 let backup = backup_at(2024, 6, 15, 10, 30, 0);
138 let now = datetime(2024, 6, 15, 10, 30, 0);
139
140 assert_eq!(backup.age(now), "0 seconds");
141 }
142
143 #[test]
144 fn exactly_one_minute() {
145 let backup = backup_at(2024, 6, 15, 10, 30, 0);
146 let now = datetime(2024, 6, 15, 10, 31, 0);
147
148 assert_eq!(backup.age(now), "1 minute");
149 }
150
151 #[test]
152 fn just_under_two_minutes_is_still_one_minute() {
153 let backup = backup_at(2024, 6, 15, 10, 30, 0);
154 let now = datetime(2024, 6, 15, 10, 31, 59);
155
156 assert_eq!(backup.age(now), "1 minute");
157 }
158
159 #[test]
160 fn two_minutes_uses_plural() {
161 let backup = backup_at(2024, 6, 15, 10, 30, 0);
162 let now = datetime(2024, 6, 15, 10, 32, 0);
163
164 assert_eq!(backup.age(now), "2 minutes");
165 }
166
167 #[test]
168 fn fifty_nine_minutes_before_hour_threshold() {
169 let backup = backup_at(2024, 6, 15, 10, 0, 0);
170 let now = datetime(2024, 6, 15, 10, 59, 59);
171
172 assert_eq!(backup.age(now), "59 minutes");
173 }
174
175 #[test]
176 fn exactly_one_hour() {
177 let backup = backup_at(2024, 6, 15, 10, 0, 0);
178 let now = datetime(2024, 6, 15, 11, 0, 0);
179
180 assert_eq!(backup.age(now), "1 hour");
181 }
182
183 #[test]
184 fn just_under_two_hours_is_still_one_hour() {
185 let backup = backup_at(2024, 6, 15, 10, 0, 0);
186 let now = datetime(2024, 6, 15, 11, 59, 59);
187
188 assert_eq!(backup.age(now), "1 hour");
189 }
190
191 #[test]
192 fn two_hours_uses_plural() {
193 let backup = backup_at(2024, 6, 15, 10, 0, 0);
194 let now = datetime(2024, 6, 15, 12, 0, 0);
195
196 assert_eq!(backup.age(now), "2 hours");
197 }
198
199 #[test]
200 fn twenty_three_hours_before_day_threshold() {
201 let backup = backup_at(2024, 6, 15, 0, 0, 0);
202 let now = datetime(2024, 6, 15, 23, 59, 59);
203
204 assert_eq!(backup.age(now), "23 hours");
205 }
206
207 #[test]
208 fn exactly_one_day() {
209 let backup = backup_at(2024, 6, 15, 10, 0, 0);
210 let now = datetime(2024, 6, 16, 10, 0, 0);
211
212 assert_eq!(backup.age(now), "1 day");
213 }
214
215 #[test]
216 fn six_days_before_week_threshold() {
217 let backup = backup_at(2024, 6, 15, 10, 0, 0);
218 let now = datetime(2024, 6, 21, 9, 59, 59);
219
220 assert_eq!(backup.age(now), "5 days");
221 }
222
223 #[test]
224 fn exactly_one_week() {
225 let backup = backup_at(2024, 6, 15, 10, 0, 0);
226 let now = datetime(2024, 6, 22, 10, 0, 0);
227
228 assert_eq!(backup.age(now), "1 week");
229 }
230
231 #[test]
232 fn two_weeks() {
233 let backup = backup_at(2024, 6, 1, 10, 0, 0);
234 let now = datetime(2024, 6, 15, 10, 0, 0);
235
236 assert_eq!(backup.age(now), "2 weeks");
237 }
238
239 #[test]
240 fn three_weeks_exactly() {
241 let backup = backup_at(2024, 6, 1, 10, 0, 0);
242 let now = datetime(2024, 6, 22, 10, 0, 0); assert_eq!(backup.age(now), "3 weeks");
245 }
246
247 #[test]
248 fn just_under_four_weeks_still_shows_weeks() {
249 let backup = backup_at(2024, 6, 1, 10, 0, 0);
250 let now = datetime(2024, 6, 29, 9, 59, 59); assert_eq!(backup.age(now), "3 weeks");
253 }
254
255 #[test]
256 fn exactly_one_month_four_weeks() {
257 let backup = backup_at(2024, 6, 1, 10, 0, 0);
258 let now = datetime(2024, 6, 29, 10, 0, 0);
259
260 assert_eq!(backup.age(now), "1 month");
262 }
263
264 #[test]
265 fn two_months() {
266 let backup = backup_at(2024, 1, 1, 10, 0, 0);
267 let now = datetime(2024, 3, 1, 10, 0, 0);
268
269 assert_eq!(backup.age(now), "2 months");
271 }
272
273 #[test]
274 fn many_months_ago() {
275 let backup = backup_at(2024, 1, 1, 0, 0, 0);
276 let now = datetime(2024, 12, 1, 0, 0, 0);
277
278 let age = backup.age(now);
280 assert!(age.ends_with("months"), "Expected months, got: {age}");
281 }
282 }
283
284 mod backup_status_display {
285 use super::*;
286
287 #[test]
288 fn retainable_is_padded_to_12_chars() {
289 let status = BackupStatus::Retainable;
290 assert_eq!(format!("{status}"), "retainable ");
291 }
292
293 #[test]
294 fn purgeable_is_padded_to_12_chars() {
295 let status = BackupStatus::Purgeable;
296 assert_eq!(format!("{status}"), "purgeable ");
297 }
298 }
299
300 mod backup_equality {
301 use super::*;
302
303 #[test]
304 fn same_path_and_date_are_equal() {
305 let a = backup_at(2024, 6, 15, 10, 30, 0);
306 let b = backup_at(2024, 6, 15, 10, 30, 0);
307
308 assert_eq!(a, b);
309 }
310
311 #[test]
312 fn different_dates_are_not_equal() {
313 let a = backup_at(2024, 6, 15, 10, 30, 0);
314 let b = backup_at(2024, 6, 15, 10, 30, 1);
315
316 assert_ne!(a, b);
317 }
318
319 #[test]
320 fn different_paths_are_not_equal() {
321 let a = Backup {
322 filepath: PathBuf::from("/tmp/a.tar.zst"),
323 creation_date: datetime(2024, 6, 15, 10, 30, 0),
324 };
325 let b = Backup {
326 filepath: PathBuf::from("/tmp/b.tar.zst"),
327 creation_date: datetime(2024, 6, 15, 10, 30, 0),
328 };
329
330 assert_ne!(a, b);
331 }
332 }
333}