1 package cron
2
3 import (
4 "reflect"
5 "strings"
6 "testing"
7 "time"
8 )
9
10 var secondParser = NewParser(Second | Minute | Hour | Dom | Month | DowOptional | Descriptor)
11
12 func TestRange(t *testing.T) {
13 zero := uint64(0)
14 ranges := []struct {
15 expr string
16 min, max uint
17 expected uint64
18 err string
19 }{
20 {"5", 0, 7, 1 << 5, ""},
21 {"0", 0, 7, 1 << 0, ""},
22 {"7", 0, 7, 1 << 7, ""},
23
24 {"5-5", 0, 7, 1 << 5, ""},
25 {"5-6", 0, 7, 1<<5 | 1<<6, ""},
26 {"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7, ""},
27
28 {"5-6/2", 0, 7, 1 << 5, ""},
29 {"5-7/2", 0, 7, 1<<5 | 1<<7, ""},
30 {"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7, ""},
31
32 {"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit, ""},
33 {"*/2", 1, 3, 1<<1 | 1<<3, ""},
34
35 {"5--5", 0, 0, zero, "too many hyphens"},
36 {"jan-x", 0, 0, zero, "failed to parse int from"},
37 {"2-x", 1, 5, zero, "failed to parse int from"},
38 {"*/-12", 0, 0, zero, "negative number"},
39 {"*//2", 0, 0, zero, "too many slashes"},
40 {"1", 3, 5, zero, "below minimum"},
41 {"6", 3, 5, zero, "above maximum"},
42 {"5-3", 3, 5, zero, "beyond end of range"},
43 {"*/0", 0, 0, zero, "should be a positive number"},
44 }
45
46 for _, c := range ranges {
47 actual, err := getRange(c.expr, bounds{c.min, c.max, nil})
48 if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) {
49 t.Errorf("%s => expected %v, got %v", c.expr, c.err, err)
50 }
51 if len(c.err) == 0 && err != nil {
52 t.Errorf("%s => unexpected error %v", c.expr, err)
53 }
54 if actual != c.expected {
55 t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual)
56 }
57 }
58 }
59
60 func TestField(t *testing.T) {
61 fields := []struct {
62 expr string
63 min, max uint
64 expected uint64
65 }{
66 {"5", 1, 7, 1 << 5},
67 {"5,6", 1, 7, 1<<5 | 1<<6},
68 {"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7},
69 {"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3},
70 }
71
72 for _, c := range fields {
73 actual, _ := getField(c.expr, bounds{c.min, c.max, nil})
74 if actual != c.expected {
75 t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual)
76 }
77 }
78 }
79
80 func TestAll(t *testing.T) {
81 allBits := []struct {
82 r bounds
83 expected uint64
84 }{
85 {minutes, 0xfffffffffffffff},
86 {hours, 0xffffff},
87 {dom, 0xfffffffe},
88 {months, 0x1ffe},
89 {dow, 0x7f},
90 }
91
92 for _, c := range allBits {
93 actual := all(c.r)
94 if c.expected|starBit != actual {
95 t.Errorf("%d-%d/%d => expected %b, got %b",
96 c.r.min, c.r.max, 1, c.expected|starBit, actual)
97 }
98 }
99 }
100
101 func TestBits(t *testing.T) {
102 bits := []struct {
103 min, max, step uint
104 expected uint64
105 }{
106 {0, 0, 1, 0x1},
107 {1, 1, 1, 0x2},
108 {1, 5, 2, 0x2a},
109 {1, 4, 2, 0xa},
110 }
111
112 for _, c := range bits {
113 actual := getBits(c.min, c.max, c.step)
114 if c.expected != actual {
115 t.Errorf("%d-%d/%d => expected %b, got %b",
116 c.min, c.max, c.step, c.expected, actual)
117 }
118 }
119 }
120
121 func TestParseScheduleErrors(t *testing.T) {
122 var tests = []struct{ expr, err string }{
123 {"* 5 j * * *", "failed to parse int from"},
124 {"@every Xm", "failed to parse duration"},
125 {"@unrecognized", "unrecognized descriptor"},
126 {"* * * *", "expected 5 to 6 fields"},
127 {"", "empty spec string"},
128 }
129 for _, c := range tests {
130 actual, err := secondParser.Parse(c.expr)
131 if err == nil || !strings.Contains(err.Error(), c.err) {
132 t.Errorf("%s => expected %v, got %v", c.expr, c.err, err)
133 }
134 if actual != nil {
135 t.Errorf("expected nil schedule on error, got %v", actual)
136 }
137 }
138 }
139
140 func TestParseSchedule(t *testing.T) {
141 tokyo, _ := time.LoadLocation("Asia/Tokyo")
142 entries := []struct {
143 parser Parser
144 expr string
145 expected Schedule
146 }{
147 {secondParser, "0 5 * * * *", every5min(time.Local)},
148 {standardParser, "5 * * * *", every5min(time.Local)},
149 {secondParser, "CRON_TZ=UTC 0 5 * * * *", every5min(time.UTC)},
150 {standardParser, "CRON_TZ=UTC 5 * * * *", every5min(time.UTC)},
151 {secondParser, "CRON_TZ=Asia/Tokyo 0 5 * * * *", every5min(tokyo)},
152 {secondParser, "@every 5m", ConstantDelaySchedule{5 * time.Minute}},
153 {secondParser, "@midnight", midnight(time.Local)},
154 {secondParser, "TZ=UTC @midnight", midnight(time.UTC)},
155 {secondParser, "TZ=Asia/Tokyo @midnight", midnight(tokyo)},
156 {secondParser, "@yearly", annual(time.Local)},
157 {secondParser, "@annually", annual(time.Local)},
158 {
159 parser: secondParser,
160 expr: "* 5 * * * *",
161 expected: &SpecSchedule{
162 Second: all(seconds),
163 Minute: 1 << 5,
164 Hour: all(hours),
165 Dom: all(dom),
166 Month: all(months),
167 Dow: all(dow),
168 Location: time.Local,
169 },
170 },
171 }
172
173 for _, c := range entries {
174 actual, err := c.parser.Parse(c.expr)
175 if err != nil {
176 t.Errorf("%s => unexpected error %v", c.expr, err)
177 }
178 if !reflect.DeepEqual(actual, c.expected) {
179 t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
180 }
181 }
182 }
183
184 func TestOptionalSecondSchedule(t *testing.T) {
185 parser := NewParser(SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor)
186 entries := []struct {
187 expr string
188 expected Schedule
189 }{
190 {"0 5 * * * *", every5min(time.Local)},
191 {"5 5 * * * *", every5min5s(time.Local)},
192 {"5 * * * *", every5min(time.Local)},
193 }
194
195 for _, c := range entries {
196 actual, err := parser.Parse(c.expr)
197 if err != nil {
198 t.Errorf("%s => unexpected error %v", c.expr, err)
199 }
200 if !reflect.DeepEqual(actual, c.expected) {
201 t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
202 }
203 }
204 }
205
206 func TestNormalizeFields(t *testing.T) {
207 tests := []struct {
208 name string
209 input []string
210 options ParseOption
211 expected []string
212 }{
213 {
214 "AllFields_NoOptional",
215 []string{"0", "5", "*", "*", "*", "*"},
216 Second | Minute | Hour | Dom | Month | Dow | Descriptor,
217 []string{"0", "5", "*", "*", "*", "*"},
218 },
219 {
220 "AllFields_SecondOptional_Provided",
221 []string{"0", "5", "*", "*", "*", "*"},
222 SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor,
223 []string{"0", "5", "*", "*", "*", "*"},
224 },
225 {
226 "AllFields_SecondOptional_NotProvided",
227 []string{"5", "*", "*", "*", "*"},
228 SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor,
229 []string{"0", "5", "*", "*", "*", "*"},
230 },
231 {
232 "SubsetFields_NoOptional",
233 []string{"5", "15", "*"},
234 Hour | Dom | Month,
235 []string{"0", "0", "5", "15", "*", "*"},
236 },
237 {
238 "SubsetFields_DowOptional_Provided",
239 []string{"5", "15", "*", "4"},
240 Hour | Dom | Month | DowOptional,
241 []string{"0", "0", "5", "15", "*", "4"},
242 },
243 {
244 "SubsetFields_DowOptional_NotProvided",
245 []string{"5", "15", "*"},
246 Hour | Dom | Month | DowOptional,
247 []string{"0", "0", "5", "15", "*", "*"},
248 },
249 {
250 "SubsetFields_SecondOptional_NotProvided",
251 []string{"5", "15", "*"},
252 SecondOptional | Hour | Dom | Month,
253 []string{"0", "0", "5", "15", "*", "*"},
254 },
255 }
256
257 for _, test := range tests {
258 t.Run(test.name, func(t *testing.T) {
259 actual, err := normalizeFields(test.input, test.options)
260 if err != nil {
261 t.Errorf("unexpected error: %v", err)
262 }
263 if !reflect.DeepEqual(actual, test.expected) {
264 t.Errorf("expected %v, got %v", test.expected, actual)
265 }
266 })
267 }
268 }
269
270 func TestNormalizeFields_Errors(t *testing.T) {
271 tests := []struct {
272 name string
273 input []string
274 options ParseOption
275 err string
276 }{
277 {
278 "TwoOptionals",
279 []string{"0", "5", "*", "*", "*", "*"},
280 SecondOptional | Minute | Hour | Dom | Month | DowOptional,
281 "",
282 },
283 {
284 "TooManyFields",
285 []string{"0", "5", "*", "*"},
286 SecondOptional | Minute | Hour,
287 "",
288 },
289 {
290 "NoFields",
291 []string{},
292 SecondOptional | Minute | Hour,
293 "",
294 },
295 {
296 "TooFewFields",
297 []string{"*"},
298 SecondOptional | Minute | Hour,
299 "",
300 },
301 }
302 for _, test := range tests {
303 t.Run(test.name, func(t *testing.T) {
304 actual, err := normalizeFields(test.input, test.options)
305 if err == nil {
306 t.Errorf("expected an error, got none. results: %v", actual)
307 }
308 if !strings.Contains(err.Error(), test.err) {
309 t.Errorf("expected error %q, got %q", test.err, err.Error())
310 }
311 })
312 }
313 }
314
315 func TestStandardSpecSchedule(t *testing.T) {
316 entries := []struct {
317 expr string
318 expected Schedule
319 err string
320 }{
321 {
322 expr: "5 * * * *",
323 expected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow), time.Local},
324 },
325 {
326 expr: "@every 5m",
327 expected: ConstantDelaySchedule{time.Duration(5) * time.Minute},
328 },
329 {
330 expr: "5 j * * *",
331 err: "failed to parse int from",
332 },
333 {
334 expr: "* * * *",
335 err: "expected exactly 5 fields",
336 },
337 }
338
339 for _, c := range entries {
340 actual, err := ParseStandard(c.expr)
341 if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) {
342 t.Errorf("%s => expected %v, got %v", c.expr, c.err, err)
343 }
344 if len(c.err) == 0 && err != nil {
345 t.Errorf("%s => unexpected error %v", c.expr, err)
346 }
347 if !reflect.DeepEqual(actual, c.expected) {
348 t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
349 }
350 }
351 }
352
353 func TestNoDescriptorParser(t *testing.T) {
354 parser := NewParser(Minute | Hour)
355 _, err := parser.Parse("@every 1m")
356 if err == nil {
357 t.Error("expected an error, got none")
358 }
359 }
360
361 func every5min(loc *time.Location) *SpecSchedule {
362 return &SpecSchedule{1 << 0, 1 << 5, all(hours), all(dom), all(months), all(dow), loc}
363 }
364
365 func every5min5s(loc *time.Location) *SpecSchedule {
366 return &SpecSchedule{1 << 5, 1 << 5, all(hours), all(dom), all(months), all(dow), loc}
367 }
368
369 func midnight(loc *time.Location) *SpecSchedule {
370 return &SpecSchedule{1, 1, 1, all(dom), all(months), all(dow), loc}
371 }
372
373 func annual(loc *time.Location) *SpecSchedule {
374 return &SpecSchedule{
375 Second: 1 << seconds.min,
376 Minute: 1 << minutes.min,
377 Hour: 1 << hours.min,
378 Dom: 1 << dom.min,
379 Month: 1 << months.min,
380 Dow: all(dow),
381 Location: loc,
382 }
383 }
384
View as plain text