123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- //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,
- }
- }
|