]> git.proxmox.com Git - proxmox-backup.git/blob - src/tools/systemd/time.rs
tools/sytemd/time: add tests for multivalue fields
[proxmox-backup.git] / src / tools / systemd / time.rs
1 use std::convert::TryInto;
2
3 use anyhow::Error;
4 use bitflags::bitflags;
5
6 pub use super::parse_time::*;
7 use super::tm_editor::*;
8
9 bitflags!{
10 #[derive(Default)]
11 pub struct WeekDays: u8 {
12 const MONDAY = 1;
13 const TUESDAY = 2;
14 const WEDNESDAY = 4;
15 const THURSDAY = 8;
16 const FRIDAY = 16;
17 const SATURDAY = 32;
18 const SUNDAY = 64;
19 }
20 }
21
22 #[derive(Debug, Clone)]
23 pub enum DateTimeValue {
24 Single(u32),
25 Range(u32, u32),
26 Repeated(u32, u32),
27 }
28
29 impl DateTimeValue {
30 // Test if the entry contains the value
31 pub fn contains(&self, value: u32) -> bool {
32 match self {
33 DateTimeValue::Single(v) => *v == value,
34 DateTimeValue::Range(start, end) => value >= *start && value <= *end,
35 DateTimeValue::Repeated(start, repetition) => {
36 if value >= *start {
37 if *repetition > 0 {
38 let offset = value - start;
39 offset % repetition == 0
40 } else {
41 *start == value
42 }
43 } else {
44 false
45 }
46 }
47 }
48 }
49
50 pub fn list_contains(list: &[DateTimeValue], value: u32) -> bool {
51 list.iter().find(|spec| spec.contains(value)).is_some()
52 }
53
54 // Find an return an entry greater than value
55 pub fn find_next(list: &[DateTimeValue], value: u32) -> Option<u32> {
56 let mut next: Option<u32> = None;
57 let mut set_next = |v: u32| {
58 if let Some(n) = next {
59 if v < n { next = Some(v); }
60 } else {
61 next = Some(v);
62 }
63 };
64 for spec in list {
65 match spec {
66 DateTimeValue::Single(v) => {
67 if *v > value { set_next(*v); }
68 }
69 DateTimeValue::Range(start, end) => {
70 if value < *start {
71 set_next(*start);
72 } else {
73 let n = value + 1;
74 if n >= *start && n <= *end {
75 set_next(n);
76 }
77 }
78 }
79 DateTimeValue::Repeated(start, repetition) => {
80 if value < *start {
81 set_next(*start);
82 } else if *repetition > 0 {
83 set_next(start + ((value - start + repetition) / repetition) * repetition);
84 }
85 }
86 }
87 }
88
89 next
90 }
91 }
92
93 /// Calendar events may be used to refer to one or more points in time in a
94 /// single expression. They are designed after the systemd.time Calendar Events
95 /// specification, but are not guaranteed to be 100% compatible.
96 #[derive(Default, Clone, Debug)]
97 pub struct CalendarEvent {
98 /// the days in a week this event should trigger
99 pub days: WeekDays,
100 /// the second(s) this event should trigger
101 pub second: Vec<DateTimeValue>, // todo: support float values
102 /// the minute(s) this event should trigger
103 pub minute: Vec<DateTimeValue>,
104 /// the hour(s) this event should trigger
105 pub hour: Vec<DateTimeValue>,
106 /// the day(s) in a month this event should trigger
107 pub day: Vec<DateTimeValue>,
108 /// the month(s) in a year this event should trigger
109 pub month: Vec<DateTimeValue>,
110 /// the years(s) this event should trigger
111 pub year: Vec<DateTimeValue>,
112 }
113
114 #[derive(Default, Clone, Debug)]
115 pub struct TimeSpan {
116 pub nsec: u64,
117 pub usec: u64,
118 pub msec: u64,
119 pub seconds: u64,
120 pub minutes: u64,
121 pub hours: u64,
122 pub days: u64,
123 pub weeks: u64,
124 pub months: u64,
125 pub years: u64,
126 }
127
128 impl From<TimeSpan> for f64 {
129 fn from(ts: TimeSpan) -> Self {
130 (ts.seconds as f64) +
131 ((ts.nsec as f64) / 1_000_000_000.0) +
132 ((ts.usec as f64) / 1_000_000.0) +
133 ((ts.msec as f64) / 1_000.0) +
134 ((ts.minutes as f64) * 60.0) +
135 ((ts.hours as f64) * 3600.0) +
136 ((ts.days as f64) * 3600.0 * 24.0) +
137 ((ts.weeks as f64) * 3600.0 * 24.0 * 7.0) +
138 ((ts.months as f64) * 3600.0 * 24.0 * 30.44) +
139 ((ts.years as f64) * 3600.0 * 24.0 * 365.25)
140 }
141 }
142
143
144 pub fn verify_time_span<'a>(i: &'a str) -> Result<(), Error> {
145 parse_time_span(i)?;
146 Ok(())
147 }
148
149 pub fn verify_calendar_event(i: &str) -> Result<(), Error> {
150 parse_calendar_event(i)?;
151 Ok(())
152 }
153
154 pub fn compute_next_event(
155 event: &CalendarEvent,
156 last: i64,
157 utc: bool,
158 ) -> Result<Option<i64>, Error> {
159
160 let last = last + 1; // at least one second later
161
162 let all_days = event.days.is_empty() || event.days.is_all();
163
164 let mut t = TmEditor::new(last, utc)?;
165
166 let mut count = 0;
167
168 loop {
169 // cancel after 1000 loops
170 if count > 1000 {
171 return Ok(None);
172 } else {
173 count += 1;
174 }
175
176 if !event.year.is_empty() {
177 let year: u32 = t.year().try_into()?;
178 if !DateTimeValue::list_contains(&event.year, year) {
179 if let Some(n) = DateTimeValue::find_next(&event.year, year) {
180 t.add_years((n - year).try_into()?)?;
181 continue;
182 } else {
183 // if we have no valid year, we cannot find a correct timestamp
184 return Ok(None);
185 }
186 }
187 }
188
189 if !event.month.is_empty() {
190 let month: u32 = t.month().try_into()?;
191 if !DateTimeValue::list_contains(&event.month, month) {
192 if let Some(n) = DateTimeValue::find_next(&event.month, month) {
193 t.add_months((n - month).try_into()?)?;
194 } else {
195 // if we could not find valid month, retry next year
196 t.add_years(1)?;
197 }
198 continue;
199 }
200 }
201
202 if !event.day.is_empty() {
203 let day: u32 = t.day().try_into()?;
204 if !DateTimeValue::list_contains(&event.day, day) {
205 if let Some(n) = DateTimeValue::find_next(&event.day, day) {
206 t.add_days((n - day).try_into()?)?;
207 } else {
208 // if we could not find valid mday, retry next month
209 t.add_months(1)?;
210 }
211 continue;
212 }
213 }
214
215 if !all_days { // match day first
216 let day_num: u32 = t.day_num().try_into()?;
217 let day = WeekDays::from_bits(1<<day_num).unwrap();
218 if !event.days.contains(day) {
219 if let Some(n) = ((day_num+1)..7)
220 .find(|d| event.days.contains(WeekDays::from_bits(1<<d).unwrap()))
221 {
222 // try next day
223 t.add_days((n - day_num).try_into()?)?;
224 } else {
225 // try next week
226 t.add_days((7 - day_num).try_into()?)?;
227 }
228 continue;
229 }
230 }
231
232 // this day
233 if !event.hour.is_empty() {
234 let hour = t.hour().try_into()?;
235 if !DateTimeValue::list_contains(&event.hour, hour) {
236 if let Some(n) = DateTimeValue::find_next(&event.hour, hour) {
237 // test next hour
238 t.set_time(n.try_into()?, 0, 0)?;
239 } else {
240 // test next day
241 t.add_days(1)?;
242 }
243 continue;
244 }
245 }
246
247 // this hour
248 if !event.minute.is_empty() {
249 let minute = t.min().try_into()?;
250 if !DateTimeValue::list_contains(&event.minute, minute) {
251 if let Some(n) = DateTimeValue::find_next(&event.minute, minute) {
252 // test next minute
253 t.set_min_sec(n.try_into()?, 0)?;
254 } else {
255 // test next hour
256 t.set_time(t.hour() + 1, 0, 0)?;
257 }
258 continue;
259 }
260 }
261
262 // this minute
263 if !event.second.is_empty() {
264 let second = t.sec().try_into()?;
265 if !DateTimeValue::list_contains(&event.second, second) {
266 if let Some(n) = DateTimeValue::find_next(&event.second, second) {
267 // test next second
268 t.set_sec(n.try_into()?)?;
269 } else {
270 // test next min
271 t.set_min_sec(t.min() + 1, 0)?;
272 }
273 continue;
274 }
275 }
276
277 let next = t.into_epoch()?;
278 return Ok(Some(next))
279 }
280 }
281
282 #[cfg(test)]
283 mod test {
284
285 use anyhow::bail;
286
287 use super::*;
288 use proxmox::tools::time::*;
289
290 fn test_event(v: &'static str) -> Result<(), Error> {
291 match parse_calendar_event(v) {
292 Ok(event) => println!("CalendarEvent '{}' => {:?}", v, event),
293 Err(err) => bail!("parsing '{}' failed - {}", v, err),
294 }
295
296 Ok(())
297 }
298
299 const fn make_test_time(mday: i32, hour: i32, min: i32) -> libc::time_t {
300 (mday*3600*24 + hour*3600 + min*60) as libc::time_t
301 }
302
303 #[test]
304 fn test_compute_next_event() -> Result<(), Error> {
305
306 let test_value = |v: &'static str, last: i64, expect: i64| -> Result<i64, Error> {
307 let event = match parse_calendar_event(v) {
308 Ok(event) => event,
309 Err(err) => bail!("parsing '{}' failed - {}", v, err),
310 };
311
312 match compute_next_event(&event, last, true) {
313 Ok(Some(next)) => {
314 if next == expect {
315 println!("next {:?} => {}", event, next);
316 } else {
317 bail!("next {:?} failed\nnext: {:?}\nexpect: {:?}",
318 event, gmtime(next), gmtime(expect));
319 }
320 }
321 Ok(None) => bail!("next {:?} failed to find a timestamp", event),
322 Err(err) => bail!("compute next for '{}' failed - {}", v, err),
323 }
324
325 Ok(expect)
326 };
327
328 let test_never = |v: &'static str, last: i64| -> Result<(), Error> {
329 let event = match parse_calendar_event(v) {
330 Ok(event) => event,
331 Err(err) => bail!("parsing '{}' failed - {}", v, err),
332 };
333
334 match compute_next_event(&event, last, true)? {
335 None => Ok(()),
336 Some(next) => bail!("compute next for '{}' succeeded, but expected fail - result {}", v, next),
337 }
338 };
339
340 const MIN: i64 = 60;
341 const HOUR: i64 = 3600;
342 const DAY: i64 = 3600*24;
343
344 const THURSDAY_00_00: i64 = make_test_time(0, 0, 0);
345 const THURSDAY_15_00: i64 = make_test_time(0, 15, 0);
346
347 const JUL_31_2020: i64 = 1596153600; // Friday, 2020-07-31 00:00:00
348 const DEC_31_2020: i64 = 1609372800; // Thursday, 2020-12-31 00:00:00
349
350 test_value("*:0", THURSDAY_00_00, THURSDAY_00_00 + HOUR)?;
351 test_value("*:*", THURSDAY_00_00, THURSDAY_00_00 + MIN)?;
352 test_value("*:*:*", THURSDAY_00_00, THURSDAY_00_00 + 1)?;
353 test_value("*:3:5", THURSDAY_00_00, THURSDAY_00_00 + 3*MIN + 5)?;
354
355 test_value("mon *:*", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY)?;
356 test_value("mon 2:*", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY + 2*HOUR)?;
357 test_value("mon 2:50", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY + 2*HOUR + 50*MIN)?;
358
359 test_value("tue", THURSDAY_00_00, THURSDAY_00_00 + 5*DAY)?;
360 test_value("wed", THURSDAY_00_00, THURSDAY_00_00 + 6*DAY)?;
361 test_value("thu", THURSDAY_00_00, THURSDAY_00_00 + 7*DAY)?;
362 test_value("fri", THURSDAY_00_00, THURSDAY_00_00 + 1*DAY)?;
363 test_value("sat", THURSDAY_00_00, THURSDAY_00_00 + 2*DAY)?;
364 test_value("sun", THURSDAY_00_00, THURSDAY_00_00 + 3*DAY)?;
365
366 // test multiple values for a single field
367 // and test that the order does not matter
368 test_value("5,10:4,8", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR + 4*MIN)?;
369 test_value("10,5:8,4", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR + 4*MIN)?;
370 test_value("6,4..10:23,5/5", THURSDAY_00_00, THURSDAY_00_00 + 4*HOUR + 5*MIN)?;
371 test_value("4..10,6:5/5,23", THURSDAY_00_00, THURSDAY_00_00 + 4*HOUR + 5*MIN)?;
372
373 // test month wrapping
374 test_value("sat", JUL_31_2020, JUL_31_2020 + 1*DAY)?;
375 test_value("sun", JUL_31_2020, JUL_31_2020 + 2*DAY)?;
376 test_value("mon", JUL_31_2020, JUL_31_2020 + 3*DAY)?;
377 test_value("tue", JUL_31_2020, JUL_31_2020 + 4*DAY)?;
378 test_value("wed", JUL_31_2020, JUL_31_2020 + 5*DAY)?;
379 test_value("thu", JUL_31_2020, JUL_31_2020 + 6*DAY)?;
380 test_value("fri", JUL_31_2020, JUL_31_2020 + 7*DAY)?;
381
382 // test year wrapping
383 test_value("fri", DEC_31_2020, DEC_31_2020 + 1*DAY)?;
384 test_value("sat", DEC_31_2020, DEC_31_2020 + 2*DAY)?;
385 test_value("sun", DEC_31_2020, DEC_31_2020 + 3*DAY)?;
386 test_value("mon", DEC_31_2020, DEC_31_2020 + 4*DAY)?;
387 test_value("tue", DEC_31_2020, DEC_31_2020 + 5*DAY)?;
388 test_value("wed", DEC_31_2020, DEC_31_2020 + 6*DAY)?;
389 test_value("thu", DEC_31_2020, DEC_31_2020 + 7*DAY)?;
390
391 test_value("daily", THURSDAY_00_00, THURSDAY_00_00 + DAY)?;
392 test_value("daily", THURSDAY_00_00+1, THURSDAY_00_00 + DAY)?;
393
394 let n = test_value("5/2:0", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR)?;
395 let n = test_value("5/2:0", n, THURSDAY_00_00 + 7*HOUR)?;
396 let n = test_value("5/2:0", n, THURSDAY_00_00 + 9*HOUR)?;
397 test_value("5/2:0", n, THURSDAY_00_00 + 11*HOUR)?;
398
399 let mut n = test_value("*:*", THURSDAY_00_00, THURSDAY_00_00 + MIN)?;
400 for i in 2..100 {
401 n = test_value("*:*", n, THURSDAY_00_00 + i*MIN)?;
402 }
403
404 let mut n = test_value("*:0", THURSDAY_00_00, THURSDAY_00_00 + HOUR)?;
405 for i in 2..100 {
406 n = test_value("*:0", n, THURSDAY_00_00 + i*HOUR)?;
407 }
408
409 let mut n = test_value("1:0", THURSDAY_15_00, THURSDAY_00_00 + DAY + HOUR)?;
410 for i in 2..100 {
411 n = test_value("1:0", n, THURSDAY_00_00 + i*DAY + HOUR)?;
412 }
413
414 // test date functionality
415
416 test_value("2020-07-31", 0, JUL_31_2020)?;
417 test_value("02-28", 0, (31+27)*DAY)?;
418 test_value("02-29", 0, 2*365*DAY + (31+28)*DAY)?; // 1972-02-29
419 test_value("1965/5-01-01", -1, THURSDAY_00_00)?;
420 test_value("2020-7..9-2/2", JUL_31_2020, JUL_31_2020 + 2*DAY)?;
421 test_value("2020,2021-12-31", JUL_31_2020, DEC_31_2020)?;
422
423 test_value("monthly", 0, 31*DAY)?;
424 test_value("quarterly", 0, (31+28+31)*DAY)?;
425 test_value("semiannually", 0, (31+28+31+30+31+30)*DAY)?;
426 test_value("yearly", 0, (365)*DAY)?;
427
428 test_never("2021-02-29", 0)?;
429 test_never("02-30", 0)?;
430
431 Ok(())
432 }
433
434 #[test]
435 fn test_calendar_event_weekday() -> Result<(), Error> {
436 test_event("mon,wed..fri")?;
437 test_event("fri..mon")?;
438
439 test_event("mon")?;
440 test_event("MON")?;
441 test_event("monDay")?;
442 test_event("tue")?;
443 test_event("Tuesday")?;
444 test_event("wed")?;
445 test_event("wednesday")?;
446 test_event("thu")?;
447 test_event("thursday")?;
448 test_event("fri")?;
449 test_event("friday")?;
450 test_event("sat")?;
451 test_event("saturday")?;
452 test_event("sun")?;
453 test_event("sunday")?;
454
455 test_event("mon..fri")?;
456 test_event("mon,tue,fri")?;
457 test_event("mon,tue..wednesday,fri..sat")?;
458
459 Ok(())
460 }
461
462 #[test]
463 fn test_time_span_parser() -> Result<(), Error> {
464
465 let test_value = |ts_str: &str, expect: f64| -> Result<(), Error> {
466 let ts = parse_time_span(ts_str)?;
467 assert_eq!(f64::from(ts), expect, "{}", ts_str);
468 Ok(())
469 };
470
471 test_value("2", 2.0)?;
472 test_value("2s", 2.0)?;
473 test_value("2sec", 2.0)?;
474 test_value("2second", 2.0)?;
475 test_value("2seconds", 2.0)?;
476
477 test_value(" 2s 2 s 2", 6.0)?;
478
479 test_value("1msec 1ms", 0.002)?;
480 test_value("1usec 1us 1µs", 0.000_003)?;
481 test_value("1nsec 1ns", 0.000_000_002)?;
482 test_value("1minutes 1minute 1min 1m", 4.0*60.0)?;
483 test_value("1hours 1hour 1hr 1h", 4.0*3600.0)?;
484 test_value("1days 1day 1d", 3.0*86400.0)?;
485 test_value("1weeks 1 week 1w", 3.0*86400.0*7.0)?;
486 test_value("1months 1month 1M", 3.0*86400.0*30.44)?;
487 test_value("1years 1year 1y", 3.0*86400.0*365.25)?;
488
489 test_value("2h", 7200.0)?;
490 test_value(" 2 h", 7200.0)?;
491 test_value("2hours", 7200.0)?;
492 test_value("48hr", 48.0*3600.0)?;
493 test_value("1y 12month", 365.25*24.0*3600.0 + 12.0*30.44*24.0*3600.0)?;
494 test_value("55s500ms", 55.5)?;
495 test_value("300ms20s 5day", 5.0*24.0*3600.0 + 20.0 + 0.3)?;
496
497 Ok(())
498 }
499 }