//nolint:all package cron import ( "reflect" "strings" "testing" "time" ) var secondParser = NewParser(Second | Minute | Hour | Dom | Month | DowOptional | Descriptor) func TestRange(t *testing.T) { zero := uint64(0) ranges := []struct { expr string min, max uint expected uint64 err string }{ {"5", 0, 7, 1 << 5, ""}, {"0", 0, 7, 1 << 0, ""}, {"7", 0, 7, 1 << 7, ""}, {"5-5", 0, 7, 1 << 5, ""}, {"5-6", 0, 7, 1<<5 | 1<<6, ""}, {"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7, ""}, {"5-6/2", 0, 7, 1 << 5, ""}, {"5-7/2", 0, 7, 1<<5 | 1<<7, ""}, {"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7, ""}, {"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit, ""}, {"*/2", 1, 3, 1<<1 | 1<<3, ""}, {"5--5", 0, 0, zero, "too many hyphens"}, {"jan-x", 0, 0, zero, `failed to parse number: strconv.Atoi: parsing "jan": invalid syntax`}, {"2-x", 1, 5, zero, `failed to parse number: strconv.Atoi: parsing "x": invalid syntax`}, {"*/-12", 0, 0, zero, "number must be positive"}, {"*//2", 0, 0, zero, "too many slashes"}, {"1", 3, 5, zero, "below minimum"}, {"6", 3, 5, zero, "above maximum"}, {"5-3", 3, 5, zero, "beginning of range after end: 5-3"}, {"*/0", 0, 0, zero, "step cannot be zero: */0"}, } for _, c := range ranges { actual, err := getRange(c.expr, bounds{c.min, c.max, nil}) if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) { t.Errorf("%s => expected %v, got %v", c.expr, c.err, err) } if len(c.err) == 0 && err != nil { t.Errorf("%s => unexpected error %v", c.expr, err) } if actual != c.expected { t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual) } } } func TestField(t *testing.T) { fields := []struct { expr string min, max uint expected uint64 }{ {"5", 1, 7, 1 << 5}, {"5,6", 1, 7, 1<<5 | 1<<6}, {"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7}, {"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3}, } for _, c := range fields { actual, _ := getField(c.expr, bounds{c.min, c.max, nil}) if actual != c.expected { t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual) } } } func TestAll(t *testing.T) { allBits := []struct { r bounds expected uint64 }{ {minutes, 0xfffffffffffffff}, // 0-59: 60 ones {hours, 0xffffff}, // 0-23: 24 ones {dom, 0xfffffffe}, // 1-31: 31 ones, 1 zero {months, 0x1ffe}, // 1-12: 12 ones, 1 zero {dow, 0x7f}, // 0-6: 7 ones } for _, c := range allBits { actual := all(c.r) // all() adds the starBit, so compensate for that.. if c.expected|starBit != actual { t.Errorf("%d-%d/%d => expected %b, got %b", c.r.min, c.r.max, 1, c.expected|starBit, actual) } } } func TestBits(t *testing.T) { bits := []struct { min, max, step uint expected uint64 }{ {0, 0, 1, 0x1}, {1, 1, 1, 0x2}, {1, 5, 2, 0x2a}, // 101010 {1, 4, 2, 0xa}, // 1010 } for _, c := range bits { actual := getBits(c.min, c.max, c.step) if c.expected != actual { t.Errorf("%d-%d/%d => expected %b, got %b", c.min, c.max, c.step, c.expected, actual) } } } func TestParseScheduleErrors(t *testing.T) { var tests = []struct{ expr, err string }{ {"* 5 j * * *", `failed to parse number: strconv.Atoi: parsing "j": invalid syntax`}, {"@every Xm", "failed to parse duration"}, {"@unrecognized", "unrecognized descriptor"}, {"* * * *", "incorrect number of fields, expected 5-6"}, {"", "empty spec string"}, } for _, c := range tests { actual, err := secondParser.Parse(c.expr) if err == nil || !strings.Contains(err.Error(), c.err) { t.Errorf("%s => expected %v, got %v", c.expr, c.err, err) } if actual != nil { t.Errorf("expected nil schedule on error, got %v", actual) } } } func TestParseSchedule(t *testing.T) { tokyo, _ := time.LoadLocation("Asia/Tokyo") entries := []struct { parser Parser expr string expected Schedule }{ {secondParser, "0 5 * * * *", every5min(time.Local)}, {standardParser, "5 * * * *", every5min(time.Local)}, {secondParser, "CRON_TZ=UTC 0 5 * * * *", every5min(time.UTC)}, {standardParser, "CRON_TZ=UTC 5 * * * *", every5min(time.UTC)}, {secondParser, "CRON_TZ=Asia/Tokyo 0 5 * * * *", every5min(tokyo)}, {secondParser, "@every 5m", ConstantDelaySchedule{5 * time.Minute}}, {secondParser, "@midnight", midnight(time.Local)}, {secondParser, "TZ=UTC @midnight", midnight(time.UTC)}, {secondParser, "TZ=Asia/Tokyo @midnight", midnight(tokyo)}, {secondParser, "@yearly", annual(time.Local)}, {secondParser, "@annually", annual(time.Local)}, { parser: secondParser, expr: "* 5 * * * *", expected: &SpecSchedule{ Second: all(seconds), Minute: 1 << 5, Hour: all(hours), Dom: all(dom), Month: all(months), Dow: all(dow), Location: time.Local, }, }, } for _, c := range entries { actual, err := c.parser.Parse(c.expr) if err != nil { t.Errorf("%s => unexpected error %v", c.expr, err) } if !reflect.DeepEqual(actual, c.expected) { t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual) } } } func TestOptionalSecondSchedule(t *testing.T) { parser := NewParser(SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor) entries := []struct { expr string expected Schedule }{ {"0 5 * * * *", every5min(time.Local)}, {"5 5 * * * *", every5min5s(time.Local)}, {"5 * * * *", every5min(time.Local)}, } for _, c := range entries { actual, err := parser.Parse(c.expr) if err != nil { t.Errorf("%s => unexpected error %v", c.expr, err) } if !reflect.DeepEqual(actual, c.expected) { t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual) } } } func TestNormalizeFields(t *testing.T) { tests := []struct { name string input []string options ParseOption expected []string }{ { "AllFields_NoOptional", []string{"0", "5", "*", "*", "*", "*"}, Second | Minute | Hour | Dom | Month | Dow | Descriptor, []string{"0", "5", "*", "*", "*", "*"}, }, { "AllFields_SecondOptional_Provided", []string{"0", "5", "*", "*", "*", "*"}, SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor, []string{"0", "5", "*", "*", "*", "*"}, }, { "AllFields_SecondOptional_NotProvided", []string{"5", "*", "*", "*", "*"}, SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor, []string{"0", "5", "*", "*", "*", "*"}, }, { "SubsetFields_NoOptional", []string{"5", "15", "*"}, Hour | Dom | Month, []string{"0", "0", "5", "15", "*", "*"}, }, { "SubsetFields_DowOptional_Provided", []string{"5", "15", "*", "4"}, Hour | Dom | Month | DowOptional, []string{"0", "0", "5", "15", "*", "4"}, }, { "SubsetFields_DowOptional_NotProvided", []string{"5", "15", "*"}, Hour | Dom | Month | DowOptional, []string{"0", "0", "5", "15", "*", "*"}, }, { "SubsetFields_SecondOptional_NotProvided", []string{"5", "15", "*"}, SecondOptional | Hour | Dom | Month, []string{"0", "0", "5", "15", "*", "*"}, }, } for _, test := range tests { t.Run(test.name, func(*testing.T) { actual, err := normalizeFields(test.input, test.options) if err != nil { t.Errorf("unexpected error: %v", err) } if !reflect.DeepEqual(actual, test.expected) { t.Errorf("expected %v, got %v", test.expected, actual) } }) } } func TestNormalizeFields_Errors(t *testing.T) { tests := []struct { name string input []string options ParseOption err string }{ { "TwoOptionals", []string{"0", "5", "*", "*", "*", "*"}, SecondOptional | Minute | Hour | Dom | Month | DowOptional, "", }, { "TooManyFields", []string{"0", "5", "*", "*"}, SecondOptional | Minute | Hour, "", }, { "NoFields", []string{}, SecondOptional | Minute | Hour, "", }, { "TooFewFields", []string{"*"}, SecondOptional | Minute | Hour, "", }, } for _, test := range tests { t.Run(test.name, func(*testing.T) { actual, err := normalizeFields(test.input, test.options) if err == nil { t.Errorf("expected an error, got none. results: %v", actual) } if !strings.Contains(err.Error(), test.err) { t.Errorf("expected error %q, got %q", test.err, err.Error()) } }) } } func TestStandardSpecSchedule(t *testing.T) { entries := []struct { expr string expected Schedule err string }{ { expr: "5 * * * *", expected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow), time.Local}, }, { expr: "@every 5m", expected: ConstantDelaySchedule{time.Duration(5) * time.Minute}, }, { expr: "5 j * * *", err: `failed to parse number: strconv.Atoi: parsing "j": invalid syntax`, }, { expr: "* * * *", err: "incorrect number of fields", }, } for _, c := range entries { actual, err := ParseStandard(c.expr) if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) { t.Errorf("%s => expected %v, got %v", c.expr, c.err, err) } if len(c.err) == 0 && err != nil { t.Errorf("%s => unexpected error %v", c.expr, err) } if !reflect.DeepEqual(actual, c.expected) { t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual) } } } func TestNoDescriptorParser(t *testing.T) { parser := NewParser(Minute | Hour) _, err := parser.Parse("@every 1m") if err == nil { t.Error("expected an error, got none") } } func every5min(loc *time.Location) *SpecSchedule { return &SpecSchedule{1 << 0, 1 << 5, all(hours), all(dom), all(months), all(dow), loc} } func every5min5s(loc *time.Location) *SpecSchedule { return &SpecSchedule{1 << 5, 1 << 5, all(hours), all(dom), all(months), all(dow), loc} } func midnight(loc *time.Location) *SpecSchedule { return &SpecSchedule{1, 1, 1, all(dom), all(months), all(dow), loc} } func annual(loc *time.Location) *SpecSchedule { return &SpecSchedule{ Second: 1 << seconds.min, Minute: 1 << minutes.min, Hour: 1 << hours.min, Dom: 1 << dom.min, Month: 1 << months.min, Dow: all(dow), Location: loc, } }