#![allow(clippy::unwrap_used)]
#![allow(clippy::panic)]
use chrono::{Datelike, TimeZone, Timelike, Utc};
use chrono_tz::{America::Chicago, US::Pacific};
use cron_parser::{parse, parse_field};
use std::collections::BTreeSet;
macro_rules! parse_field_tests {
($($name:ident: $value:expr,)*) => {
$(
#[test]
fn $name() {
let (input, min, max, expected) = $value;
let mut expect = BTreeSet::<u32>::new();
for i in expected {
expect.insert(i);
}
assert_eq!(parse_field(input, min, max).unwrap(), expect);
}
)*
}
}
parse_field_tests! {
parse_any:("*", 0, 0, vec![0]),
parse_minutes_0: ("0", 0, 59, vec![0]),
parse_minutes_1: ("1", 0, 59, vec![1]),
parse_hours: ("23", 0, 23, vec![23]),
parse_days: ("31", 0, 31, vec![31]),
parse_day_week: ("6", 0, 6, vec![6]),
parse_every_30: ("*/30", 0, 59, vec![0,30]),
parse_every_5_minutes: ("*/5", 0, 59, vec![0,5,10,15,20,25,30,35,40,45,50,55]),
parse_every_minute: ("*", 0, 59, vec![0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59]),
parse_every_minute_step: ("*/1", 0, 59, vec![0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59]),
parse_every_hour: ("*", 0, 23, vec![0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]),
parse_every_hour_step: ("*/1", 0, 23, vec![0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]),
parse_every_day: ("*", 1, 31, vec![1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]),
parse_every_day_step: ("*/1", 1, 31, vec![1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]),
parse_every_month: ("*", 1, 12, vec![1,2,3,4,5,6,7,8,9,10,11,12]),
parse_every_month_step: ("*/1", 1, 12, vec![1,2,3,4,5,6,7,8,9,10,11,12]),
parse_every_dweek: ("*", 0, 6, vec![0,1,2,3,4,5,6]),
parse_every_dweek_step: ("*/1", 0, 6, vec![0,1,2,3,4,5,6]),
parse_range_5_10_minutes: ("5-10", 0, 59, vec![5,6,7,8,9,10]),
parse_range_5_10_hours: ("5-10", 0, 12, vec![5,6,7,8,9,10]),
parse_list_minutes: ("15,30,45,0", 0, 59, vec![0,15,30,45]),
parse_1024: ("1024", 0, 1024, vec![1024]),
parse_repeat_values:("1,1,1,1,2", 0, 59, vec![1,2]),
parse_range_and_list1: ("1-8,11", 0, 23, vec![1,2,3,4,5,6,7,8,11]),
parse_range_and_list2: ("1-8,11,9,4,5", 0, 23, vec![1,2,3,4,5,6,7,8,9,11]),
parse_range_and_list3: ("*,1-8,11,9,4,5", 0, 23, vec![0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]),
parse_range_and_list4: ("2-3,*/15", 0, 23, vec![0,2,3,15]),
parse_range_and_list5: ("2-3,9,*/15,1-8,11,9,4,5", 0, 23, vec![0,1,2,3,4,5,6,7,8,9,11,15]),
parse_range_list_step: ("*/30,40-45,57", 0, 59, vec![0,30,40,41,42,43,44,45,57]),
parse_range_list_step_repeated_values: ("*/30,40-45,57,30,44,41-45", 0, 59, vec![0,30,40,41,42,43,44,45,57]),
parse_start_step_minute: ("1/6", 0, 59, vec![1,7,13,19,25,31,37,43,49,55]),
parse_start_step_hour: ("1/6", 0, 23, vec![1,7,13,19]),
parse_start_step_day: ("1/6", 1, 31, vec![1,7,13,19,25,31]),
parse_start_step_month: ("1/6", 1, 12, vec![1,7]),
parse_start_step_dow: ("1/6", 0, 6, vec![1]),
parse_range_with_step_minute: ("5-40/3", 0, 59, vec![5,8,11,14,17,20,23,26,29,32,35,38]),
parse_range_with_step_hour: ("12-18/2", 0, 23, vec![12,14,16,18]),
parse_range_with_step_hour_2: ("1-23/6", 0, 23, vec![1,7,13,19]),
parse_range_with_step_hour_3: ("1/6", 0, 23, vec![1,7,13,19]),
parse_range_with_step_hour_4: ("6/1", 0, 23, vec![6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]),
parse_range_with_step_day: ("1-31/5", 1, 31, vec![1,6,11,16,21,26,31]),
parse_range_with_step_month: ("1-12/3", 1, 12, vec![1,4,7,10]),
}
macro_rules! parse_tests {
($($name:ident: $value:expr,)*) => {
$(
#[test]
fn $name() {
let (input, ts, expected) = $value;
let dt = Utc.timestamp_opt(ts, 0).unwrap();
assert_eq!(parse(input, &dt).unwrap().timestamp(), expected);
let dt = Pacific.from_local_datetime(&dt.naive_utc()).unwrap();
let expected = Pacific
.from_local_datetime(&Utc.timestamp_opt(expected, 0).unwrap().naive_utc())
.unwrap()
.timestamp();
assert_eq!(parse(input, &dt).unwrap().timestamp(), expected);
}
)*
}
}
parse_tests! {
any_minute: ("* * * * *", 1_572_969_395, 1_572_969_420),
any_minute2: ("*/5,* * * * *", 1_572_969_395, 1_572_969_420),
every_5_mintues: ("*/5 * * * *", 1_572_969_395, 1_572_969_600),
on_minute_5: ("5 * * * *", 1_572_969_395, 1_572_969_900),
every_minute_every_2nd_hour: ("* */2 * * *", 1_572_969_395, 1_572_969_600),
every_minute_in_october: ("* * * 10 *", 1_572_969_395, 1_601_510_400),
every_minute_on_day_4_in_november: ("* * 4 11 *", 1_572_969_395, 1_604_448_000),
daily_2am: ("0 2 * * *", 1_572_969_395, 1_573_005_600),
twice_a_day_5_17: ("0 5,17 * * *", 1_572_969_395, 1_572_973_200),
every_2nd_minute_every_hour_from_1_to_4_and_15:("*/2 1-4,15 * * *", 1_572_969_395,1_572_969_480),
febraury_30_1: ("* * 30 */2 *", 1_572_969_395, 1_575_072_000),
febraury_30_2: ("* * 30 * *", 1_548_892_800, 1_553_904_000),
febraury_29: ("* * 29 2 *", 1_583_020_800, 1_709_164_800),
day_31: ("* 5 31 * *", 1_548_936_000, 1_554_008_400),
day_31_ever2months: ("* 5 31 */3 *", 1_548_936_000, 1_564_549_200),
leap_year: ("* * 28-31 2 *", 1_583_020_800, 1_614_470_400),
every_dow_0: ("0 0 * * 0", 1_573_151_292, 1_573_344_000),
every_dow_1: ("0 0 * * 1", 1_573_151_292, 1_573_430_400),
every_dow_2: ("0 0 * * 2", 1_573_151_292, 1_573_516_800),
every_dow_3: ("0 0 * * 3", 1_573_151_292, 1_573_603_200),
every_dow_4: ("0 0 * * 4", 1_573_151_292, 1_573_689_600),
every_dow_5: ("0 0 * * 5", 1_573_151_292, 1_573_171_200),
every_dow_6: ("0 0 * * 6", 1_573_151_292, 1_573_257_600),
every_dow_sun: ("0 0 * * Sun", 1_573_151_292, 1_573_344_000),
every_dow_mon: ("0 0 * * Mon", 1_573_151_292, 1_573_430_400),
every_dow_tue: ("0 0 * * Tue", 1_573_151_292, 1_573_516_800),
every_dow_wed: ("0 0 * * Wed", 1_573_151_292, 1_573_603_200),
every_dow_thu: ("0 0 * * Thu", 1_573_151_292, 1_573_689_600),
every_dow_fri: ("0 0 * * Fri", 1_573_151_292, 1_573_171_200),
every_dow_sat: ("0 0 * * Sat", 1_573_151_292, 1_573_257_600),
every_dow_wed_and_fri: ("0 0 * * Wed,Fri", 1_573_151_292, 1_573_171_200),
dow_feb: ("0 0 29 2 6", 1_573_151_292, 1_582_934_400),
every_dow_wed_2_fri: ("0 0 * * Wed-Fri", 1_573_151_292, 1_573_171_200),
}
#[test]
fn parse_field_double_field() {
assert!(parse_field("**", 0, 0).is_err());
}
#[test]
fn parse_field_bad_range() {
assert!(parse_field("1-2-3", 0, 0).is_err(),);
assert!(parse_field("8-5", 0, 0).is_err(),);
}
#[test]
fn bad_minute_input() {
assert!(parse_field("60", 0, 59).is_err(),);
}
#[test]
fn bad_minute_input_range() {
assert!(parse_field("5-60", 0, 59).is_err());
}
#[test]
fn bad_minute_input_list() {
assert!(parse_field("40,50,60", 0, 59).is_err());
}
#[test]
fn bad_hour_input_step() {
assert!(parse_field("*/30", 0, 23).is_err());
}
#[test]
fn february_30() {
assert!(parse("* * 30 2 *", &Utc::now()).is_err());
}
#[test]
fn test_parse() {
assert!(parse("*/5 * * * *", &Utc::now()).is_ok());
assert!(parse("0 0 29 2 5", &Utc.timestamp_opt(1_573_151_292, 0).unwrap()).is_err());
assert!(parse("0 0 * * */Wed", &Utc::now()).is_err());
assert!(parse("0 0 * * */2-5", &Utc::now()).is_err());
}
#[test]
fn test_bad_input() {
assert!(parse("2-3,9,*/15,1-8,11,9,4,5, * * * *", &Utc::now()).is_ok());
assert!(parse("2-3,9,*/15,1-8,11,9,4,5,,,, * * * *", &Utc::now()).is_ok());
}
#[test]
fn test_next_100_iterations() {
let now = Utc.timestamp_opt(1_573_239_864, 0).unwrap();
let mut next = parse("0 23 */2 * *", &now).unwrap();
assert_eq!(next.timestamp(), 1_573_340_400);
for _ in 0..100 {
next = parse("0 23 */2 * *", &next).unwrap();
}
assert_eq!(next.timestamp(), 1_590_274_800);
}
#[test]
fn test_timezone() {
let utc = Utc.timestamp_opt(1_573_405_861, 0).unwrap();
let pacific_time = utc.with_timezone(&Pacific);
let next_pt = parse("*/5 * * * *", &pacific_time).unwrap();
assert_eq!(next_pt.timestamp(), 1_573_406_100);
let next_utc = parse("*/5 * * * *", &utc).unwrap();
assert_eq!(next_utc.timestamp(), 1_573_406_100);
assert_ne!(next_pt.to_string(), next_utc.to_string());
}
#[test]
fn test_timezone_dst() {
let utc = Utc.timestamp_opt(1_541_309_400, 0).unwrap();
let cst = utc.with_timezone(&Chicago);
let mut next = parse("*/15 * * * *", &cst).unwrap();
for _ in 0..10 {
next = parse("*/15 * * * *", &next).unwrap();
}
assert_eq!(next.timestamp(), 1_541_322_900);
}
#[test]
fn parse_needs_5_fields() {
assert!(parse("*/5 * * * *", &Utc::now()).is_ok());
assert!(parse("*/5 * * *", &Utc::now()).is_err());
assert!(parse("*/5 * *", &Utc::now()).is_err());
assert!(parse("*/5 *", &Utc::now()).is_err());
assert!(parse("*/5", &Utc::now()).is_err());
assert!(parse("* * * * * *", &Utc::now()).is_err());
}
#[test]
fn parse_start_step() {
assert!(parse("* 1/6 * * *", &Utc::now()).is_ok());
assert!(parse("* 0/5 * * *", &Utc::now()).is_ok());
assert!(parse("* */5 * * *", &Utc::now()).is_ok());
assert!(parse("* */0 * * *", &Utc::now()).is_err());
}
#[test]
fn combine_ranges_with_steps() {
assert!(parse("* 12-18/2 * * *", &Utc::now()).is_ok());
assert!(parse("0 12-18/3 * * *", &Utc::now()).is_ok());
}
#[test]
fn test_range_step_patterns() {
let now = Utc.timestamp_opt(1_573_239_864, 0).unwrap(); let result = parse("0 12-18/3 * * *", &now);
assert!(result.is_ok());
let next = result.unwrap();
assert!(next.hour() == 18 || (next.hour() == 12 && next.day() == 9));
assert!(parse("0 0-23/6 * * *", &Utc::now()).is_ok()); assert!(parse("*/10 9-17 * * *", &Utc::now()).is_ok()); assert!(parse("0 1-12/2 * * *", &Utc::now()).is_ok()); assert!(parse("30 8-20/4 * * 1-5", &Utc::now()).is_ok()); }
#[test]
fn test_changelog_example_12_18_step_3() {
let before_first = Utc.timestamp_opt(1_573_210_800, 0).unwrap(); let next = parse("0 12-18/3 * * *", &before_first).unwrap();
assert_eq!(next.hour(), 12);
assert_eq!(next.minute(), 0);
let after_first = Utc.timestamp_opt(1_573_218_000, 0).unwrap(); let next = parse("0 12-18/3 * * *", &after_first).unwrap();
assert_eq!(next.hour(), 15);
assert_eq!(next.minute(), 0);
let after_second = Utc.timestamp_opt(1_573_228_800, 0).unwrap(); let next = parse("0 12-18/3 * * *", &after_second).unwrap();
assert_eq!(next.hour(), 18);
assert_eq!(next.minute(), 0);
let after_last = Utc.timestamp_opt(1_573_239_600, 0).unwrap(); let next = parse("0 12-18/3 * * *", &after_last).unwrap();
assert_eq!(next.hour(), 12);
assert_eq!(next.minute(), 0);
assert_eq!(next.day(), 9); }
#[test]
fn test_range_step_edge_cases() {
let now = Utc::now();
assert!(parse("0 12-12/1 * * *", &now).is_ok());
assert!(parse("0 12-18/10 * * *", &now).is_ok());
assert!(parse("0 12-18/1 * * *", &now).is_ok());
assert!(parse("0 1-5/2,10-14/2 * * *", &now).is_ok());
}
#[test]
fn parse_field_start_stop_0() {
assert!(parse_field("*/0", 0, 24).is_err());
}
#[test]
fn test_field_parsing() {
let result = parse_field("*/5", 0, 59).unwrap();
assert_eq!(result, (0..=59).step_by(5).collect::<BTreeSet<u32>>());
let result = parse_field("1/6", 0, 59).unwrap();
assert_eq!(result, (1..=59).step_by(6).collect::<BTreeSet<u32>>());
let result = parse_field("12-18/2", 0, 23).unwrap();
assert_eq!(result, BTreeSet::from([12, 14, 16, 18]));
assert!(parse_field("*/0", 0, 59).is_err());
assert!(parse_field("60", 0, 59).is_err());
assert!(parse_field("24", 0, 23).is_err());
assert!(parse_field("13-25", 1, 12).is_err());
assert!(parse_field("10-5", 0, 59).is_err()); }
#[test]
fn test_invalid_dow_name() {
assert!(parse("0 0 * * InvalidDay", &Utc::now()).is_err());
assert!(parse("0 0 * * Monday", &Utc::now()).is_err()); }
#[test]
fn test_step_greater_than_max() {
assert!(parse_field("*/60", 0, 59).is_err());
assert!(parse_field("*/100", 0, 23).is_err());
assert!(parse_field("1/60", 0, 59).is_err());
}
#[test]
fn test_empty_field_parts() {
assert_eq!(parse_field(",,,", 0, 59).unwrap(), BTreeSet::new());
assert_eq!(parse_field("1,,,2", 0, 59).unwrap(), BTreeSet::from([1, 2]));
}
#[test]
fn test_invalid_range_step_format() {
assert!(parse_field("1/2/3", 0, 59).is_err());
assert!(parse_field("*/5/2", 0, 59).is_err());
}
#[test]
fn test_range_with_invalid_step_zero() {
assert!(parse_field("1-10/0", 0, 59).is_err());
assert!(parse_field("0-23/0", 0, 23).is_err());
}
#[test]
fn test_dow_range() {
assert!(parse_field("Mon-Fri", 0, 6).is_ok());
assert_eq!(
parse_field("Mon-Fri", 0, 6).unwrap(),
BTreeSet::from([1, 2, 3, 4, 5])
);
assert!(parse_field("Sat-Sun", 0, 6).is_err());
}
#[test]
fn test_very_large_numbers() {
assert!(parse_field("9999", 0, 59).is_err());
assert!(parse_field("100-200", 0, 59).is_err());
}
#[test]
fn test_mixed_dow_formats() {
assert!(parse_field("0,Mon,5,Fri", 0, 6).is_ok());
assert_eq!(
parse_field("0,Mon,5,Fri", 0, 6).unwrap(),
BTreeSet::from([0, 1, 5])
);
}
#[test]
fn test_cron_edge_cases() {
assert!(parse("0 0 31 4 *", &Utc::now()).is_err());
assert!(parse("0 0 31 2 *", &Utc::now()).is_err());
assert!(parse("0 0 * * */8", &Utc::now()).is_err()); }
#[test]
fn test_whitespace_handling() {
assert!(parse("* * * * *", &Utc::now()).is_ok());
assert!(parse("*/5 * * * *", &Utc::now()).is_ok());
}
#[test]
fn test_case_insensitive_dow() {
assert_eq!(
parse_field("mon", 0, 6).unwrap(),
parse_field("MON", 0, 6).unwrap()
);
assert_eq!(
parse_field("mon", 0, 6).unwrap(),
parse_field("Mon", 0, 6).unwrap()
);
}
#[test]
fn test_invalid_range_in_step() {
assert!(parse_field("1-2-3/5", 0, 59).is_err());
assert!(parse_field("10-20-30/2", 0, 59).is_err());
}
#[test]
fn test_reverse_range_with_step() {
assert!(parse_field("20-10/2", 0, 59).is_err());
assert!(parse_field("50-40/5", 0, 59).is_err());
}
#[test]
fn test_cron_never_matches() {
let now = Utc.timestamp_opt(1_573_151_292, 0).unwrap();
assert!(parse("0 0 30 2 *", &now).is_err());
}
#[test]
fn test_dst_spring_forward_skipped_time() {
let before_dst = Pacific.with_ymd_and_hms(2024, 3, 10, 1, 30, 0).unwrap();
let result = parse("30 2 * * *", &before_dst);
assert!(result.is_ok());
let next = result.unwrap();
assert!(next.day() >= 10);
}
#[test]
fn test_dst_fall_back_ambiguous_time() {
match Pacific.with_ymd_and_hms(2024, 11, 3, 1, 30, 0) {
chrono::LocalResult::Ambiguous(earlier, _later) => {
let result = parse("*/15 * * * *", &earlier);
assert!(result.is_ok());
}
chrono::LocalResult::Single(dt) => {
let result = parse("*/15 * * * *", &dt);
assert!(result.is_ok());
}
chrono::LocalResult::None => {
panic!("1:30 AM on Nov 3 should exist");
}
}
}
#[test]
fn test_very_restrictive_cron() {
let now = Utc.timestamp_opt(1_577_836_800, 0).unwrap();
let result = parse("0 0 29 2 0", &now);
assert!(result.is_err());
}