1 package cron
2
3 import (
4 "bytes"
5 "fmt"
6 "log"
7 "strings"
8 "sync"
9 "sync/atomic"
10 "testing"
11 "time"
12 )
13
14
15
16
17 const OneSecond = 1*time.Second + 50*time.Millisecond
18
19 type syncWriter struct {
20 wr bytes.Buffer
21 m sync.Mutex
22 }
23
24 func (sw *syncWriter) Write(data []byte) (n int, err error) {
25 sw.m.Lock()
26 n, err = sw.wr.Write(data)
27 sw.m.Unlock()
28 return
29 }
30
31 func (sw *syncWriter) String() string {
32 sw.m.Lock()
33 defer sw.m.Unlock()
34 return sw.wr.String()
35 }
36
37 func newBufLogger(sw *syncWriter) Logger {
38 return PrintfLogger(log.New(sw, "", log.LstdFlags))
39 }
40
41 func TestFuncPanicRecovery(t *testing.T) {
42 var buf syncWriter
43 cron := New(WithParser(secondParser),
44 WithChain(Recover(newBufLogger(&buf))))
45 cron.Start()
46 defer cron.Stop()
47 cron.AddFunc("* * * * * ?", func() {
48 panic("YOLO")
49 })
50
51 select {
52 case <-time.After(OneSecond):
53 if !strings.Contains(buf.String(), "YOLO") {
54 t.Error("expected a panic to be logged, got none")
55 }
56 return
57 }
58 }
59
60 type DummyJob struct{}
61
62 func (d DummyJob) Run() {
63 panic("YOLO")
64 }
65
66 func TestJobPanicRecovery(t *testing.T) {
67 var job DummyJob
68
69 var buf syncWriter
70 cron := New(WithParser(secondParser),
71 WithChain(Recover(newBufLogger(&buf))))
72 cron.Start()
73 defer cron.Stop()
74 cron.AddJob("* * * * * ?", job)
75
76 select {
77 case <-time.After(OneSecond):
78 if !strings.Contains(buf.String(), "YOLO") {
79 t.Error("expected a panic to be logged, got none")
80 }
81 return
82 }
83 }
84
85
86 func TestNoEntries(t *testing.T) {
87 cron := newWithSeconds()
88 cron.Start()
89
90 select {
91 case <-time.After(OneSecond):
92 t.Fatal("expected cron will be stopped immediately")
93 case <-stop(cron):
94 }
95 }
96
97
98 func TestStopCausesJobsToNotRun(t *testing.T) {
99 wg := &sync.WaitGroup{}
100 wg.Add(1)
101
102 cron := newWithSeconds()
103 cron.Start()
104 cron.Stop()
105 cron.AddFunc("* * * * * ?", func() { wg.Done() })
106
107 select {
108 case <-time.After(OneSecond):
109
110 case <-wait(wg):
111 t.Fatal("expected stopped cron does not run any job")
112 }
113 }
114
115
116 func TestAddBeforeRunning(t *testing.T) {
117 wg := &sync.WaitGroup{}
118 wg.Add(1)
119
120 cron := newWithSeconds()
121 cron.AddFunc("* * * * * ?", func() { wg.Done() })
122 cron.Start()
123 defer cron.Stop()
124
125
126 select {
127 case <-time.After(OneSecond):
128 t.Fatal("expected job runs")
129 case <-wait(wg):
130 }
131 }
132
133
134 func TestAddWhileRunning(t *testing.T) {
135 wg := &sync.WaitGroup{}
136 wg.Add(1)
137
138 cron := newWithSeconds()
139 cron.Start()
140 defer cron.Stop()
141 cron.AddFunc("* * * * * ?", func() { wg.Done() })
142
143 select {
144 case <-time.After(OneSecond):
145 t.Fatal("expected job runs")
146 case <-wait(wg):
147 }
148 }
149
150
151 func TestAddWhileRunningWithDelay(t *testing.T) {
152 cron := newWithSeconds()
153 cron.Start()
154 defer cron.Stop()
155 time.Sleep(5 * time.Second)
156 var calls int64
157 cron.AddFunc("* * * * * *", func() { atomic.AddInt64(&calls, 1) })
158
159 <-time.After(OneSecond)
160 if atomic.LoadInt64(&calls) != 1 {
161 t.Errorf("called %d times, expected 1\n", calls)
162 }
163 }
164
165
166 func TestRemoveBeforeRunning(t *testing.T) {
167 wg := &sync.WaitGroup{}
168 wg.Add(1)
169
170 cron := newWithSeconds()
171 id, _ := cron.AddFunc("* * * * * ?", func() { wg.Done() })
172 cron.Remove(id)
173 cron.Start()
174 defer cron.Stop()
175
176 select {
177 case <-time.After(OneSecond):
178
179 case <-wait(wg):
180 t.FailNow()
181 }
182 }
183
184
185 func TestRemoveWhileRunning(t *testing.T) {
186 wg := &sync.WaitGroup{}
187 wg.Add(1)
188
189 cron := newWithSeconds()
190 cron.Start()
191 defer cron.Stop()
192 id, _ := cron.AddFunc("* * * * * ?", func() { wg.Done() })
193 cron.Remove(id)
194
195 select {
196 case <-time.After(OneSecond):
197 case <-wait(wg):
198 t.FailNow()
199 }
200 }
201
202
203 func TestSnapshotEntries(t *testing.T) {
204 wg := &sync.WaitGroup{}
205 wg.Add(1)
206
207 cron := New()
208 cron.AddFunc("@every 2s", func() { wg.Done() })
209 cron.Start()
210 defer cron.Stop()
211
212
213 select {
214 case <-time.After(OneSecond):
215 cron.Entries()
216 }
217
218
219 select {
220 case <-time.After(OneSecond):
221 t.Error("expected job runs at 2 second mark")
222 case <-wait(wg):
223 }
224 }
225
226
227
228
229
230 func TestMultipleEntries(t *testing.T) {
231 wg := &sync.WaitGroup{}
232 wg.Add(2)
233
234 cron := newWithSeconds()
235 cron.AddFunc("0 0 0 1 1 ?", func() {})
236 cron.AddFunc("* * * * * ?", func() { wg.Done() })
237 id1, _ := cron.AddFunc("* * * * * ?", func() { t.Fatal() })
238 id2, _ := cron.AddFunc("* * * * * ?", func() { t.Fatal() })
239 cron.AddFunc("0 0 0 31 12 ?", func() {})
240 cron.AddFunc("* * * * * ?", func() { wg.Done() })
241
242 cron.Remove(id1)
243 cron.Start()
244 cron.Remove(id2)
245 defer cron.Stop()
246
247 select {
248 case <-time.After(OneSecond):
249 t.Error("expected job run in proper order")
250 case <-wait(wg):
251 }
252 }
253
254
255 func TestRunningJobTwice(t *testing.T) {
256 wg := &sync.WaitGroup{}
257 wg.Add(2)
258
259 cron := newWithSeconds()
260 cron.AddFunc("0 0 0 1 1 ?", func() {})
261 cron.AddFunc("0 0 0 31 12 ?", func() {})
262 cron.AddFunc("* * * * * ?", func() { wg.Done() })
263
264 cron.Start()
265 defer cron.Stop()
266
267 select {
268 case <-time.After(2 * OneSecond):
269 t.Error("expected job fires 2 times")
270 case <-wait(wg):
271 }
272 }
273
274 func TestRunningMultipleSchedules(t *testing.T) {
275 wg := &sync.WaitGroup{}
276 wg.Add(2)
277
278 cron := newWithSeconds()
279 cron.AddFunc("0 0 0 1 1 ?", func() {})
280 cron.AddFunc("0 0 0 31 12 ?", func() {})
281 cron.AddFunc("* * * * * ?", func() { wg.Done() })
282 cron.Schedule(Every(time.Minute), FuncJob(func() {}))
283 cron.Schedule(Every(time.Second), FuncJob(func() { wg.Done() }))
284 cron.Schedule(Every(time.Hour), FuncJob(func() {}))
285
286 cron.Start()
287 defer cron.Stop()
288
289 select {
290 case <-time.After(2 * OneSecond):
291 t.Error("expected job fires 2 times")
292 case <-wait(wg):
293 }
294 }
295
296
297 func TestLocalTimezone(t *testing.T) {
298 wg := &sync.WaitGroup{}
299 wg.Add(2)
300
301 now := time.Now()
302
303
304
305 if now.Second() >= 58 {
306 time.Sleep(2 * time.Second)
307 now = time.Now()
308 }
309 spec := fmt.Sprintf("%d,%d %d %d %d %d ?",
310 now.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month())
311
312 cron := newWithSeconds()
313 cron.AddFunc(spec, func() { wg.Done() })
314 cron.Start()
315 defer cron.Stop()
316
317 select {
318 case <-time.After(OneSecond * 2):
319 t.Error("expected job fires 2 times")
320 case <-wait(wg):
321 }
322 }
323
324
325 func TestNonLocalTimezone(t *testing.T) {
326 wg := &sync.WaitGroup{}
327 wg.Add(2)
328
329 loc, err := time.LoadLocation("Atlantic/Cape_Verde")
330 if err != nil {
331 fmt.Printf("Failed to load time zone Atlantic/Cape_Verde: %+v", err)
332 t.Fail()
333 }
334
335 now := time.Now().In(loc)
336
337
338
339 if now.Second() >= 58 {
340 time.Sleep(2 * time.Second)
341 now = time.Now().In(loc)
342 }
343 spec := fmt.Sprintf("%d,%d %d %d %d %d ?",
344 now.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month())
345
346 cron := New(WithLocation(loc), WithParser(secondParser))
347 cron.AddFunc(spec, func() { wg.Done() })
348 cron.Start()
349 defer cron.Stop()
350
351 select {
352 case <-time.After(OneSecond * 2):
353 t.Error("expected job fires 2 times")
354 case <-wait(wg):
355 }
356 }
357
358
359
360 func TestStopWithoutStart(t *testing.T) {
361 cron := New()
362 cron.Stop()
363 }
364
365 type testJob struct {
366 wg *sync.WaitGroup
367 name string
368 }
369
370 func (t testJob) Run() {
371 t.wg.Done()
372 }
373
374
375 func TestInvalidJobSpec(t *testing.T) {
376 cron := New()
377 _, err := cron.AddJob("this will not parse", nil)
378 if err == nil {
379 t.Errorf("expected an error with invalid spec, got nil")
380 }
381 }
382
383
384 func TestBlockingRun(t *testing.T) {
385 wg := &sync.WaitGroup{}
386 wg.Add(1)
387
388 cron := newWithSeconds()
389 cron.AddFunc("* * * * * ?", func() { wg.Done() })
390
391 var unblockChan = make(chan struct{})
392
393 go func() {
394 cron.Run()
395 close(unblockChan)
396 }()
397 defer cron.Stop()
398
399 select {
400 case <-time.After(OneSecond):
401 t.Error("expected job fires")
402 case <-unblockChan:
403 t.Error("expected that Run() blocks")
404 case <-wait(wg):
405 }
406 }
407
408
409 func TestStartNoop(t *testing.T) {
410 var tickChan = make(chan struct{}, 2)
411
412 cron := newWithSeconds()
413 cron.AddFunc("* * * * * ?", func() {
414 tickChan <- struct{}{}
415 })
416
417 cron.Start()
418 defer cron.Stop()
419
420
421 <-tickChan
422
423 cron.Start()
424
425 <-tickChan
426
427
428 select {
429 case <-time.After(time.Millisecond):
430 case <-tickChan:
431 t.Error("expected job fires exactly twice")
432 }
433 }
434
435
436 func TestJob(t *testing.T) {
437 wg := &sync.WaitGroup{}
438 wg.Add(1)
439
440 cron := newWithSeconds()
441 cron.AddJob("0 0 0 30 Feb ?", testJob{wg, "job0"})
442 cron.AddJob("0 0 0 1 1 ?", testJob{wg, "job1"})
443 job2, _ := cron.AddJob("* * * * * ?", testJob{wg, "job2"})
444 cron.AddJob("1 0 0 1 1 ?", testJob{wg, "job3"})
445 cron.Schedule(Every(5*time.Second+5*time.Nanosecond), testJob{wg, "job4"})
446 job5 := cron.Schedule(Every(5*time.Minute), testJob{wg, "job5"})
447
448
449 if actualName := cron.Entry(job2).Job.(testJob).name; actualName != "job2" {
450 t.Error("wrong job retrieved:", actualName)
451 }
452 if actualName := cron.Entry(job5).Job.(testJob).name; actualName != "job5" {
453 t.Error("wrong job retrieved:", actualName)
454 }
455
456 cron.Start()
457 defer cron.Stop()
458
459 select {
460 case <-time.After(OneSecond):
461 t.FailNow()
462 case <-wait(wg):
463 }
464
465
466 expecteds := []string{"job2", "job4", "job5", "job1", "job3", "job0"}
467
468 var actuals []string
469 for _, entry := range cron.Entries() {
470 actuals = append(actuals, entry.Job.(testJob).name)
471 }
472
473 for i, expected := range expecteds {
474 if actuals[i] != expected {
475 t.Fatalf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals)
476 }
477 }
478
479
480 if actualName := cron.Entry(job2).Job.(testJob).name; actualName != "job2" {
481 t.Error("wrong job retrieved:", actualName)
482 }
483 if actualName := cron.Entry(job5).Job.(testJob).name; actualName != "job5" {
484 t.Error("wrong job retrieved:", actualName)
485 }
486 }
487
488
489
490 func TestScheduleAfterRemoval(t *testing.T) {
491 var wg1 sync.WaitGroup
492 var wg2 sync.WaitGroup
493 wg1.Add(1)
494 wg2.Add(1)
495
496
497
498
499
500 var calls int
501 var mu sync.Mutex
502
503 cron := newWithSeconds()
504 hourJob := cron.Schedule(Every(time.Hour), FuncJob(func() {}))
505 cron.Schedule(Every(time.Second), FuncJob(func() {
506 mu.Lock()
507 defer mu.Unlock()
508 switch calls {
509 case 0:
510 wg1.Done()
511 calls++
512 case 1:
513 time.Sleep(750 * time.Millisecond)
514 cron.Remove(hourJob)
515 calls++
516 case 2:
517 calls++
518 wg2.Done()
519 case 3:
520 panic("unexpected 3rd call")
521 }
522 }))
523
524 cron.Start()
525 defer cron.Stop()
526
527
528
529 wg1.Wait()
530
531 select {
532 case <-time.After(2 * OneSecond):
533 t.Error("expected job fires 2 times")
534 case <-wait(&wg2):
535 }
536 }
537
538 type ZeroSchedule struct{}
539
540 func (*ZeroSchedule) Next(time.Time) time.Time {
541 return time.Time{}
542 }
543
544
545 func TestJobWithZeroTimeDoesNotRun(t *testing.T) {
546 cron := newWithSeconds()
547 var calls int64
548 cron.AddFunc("* * * * * *", func() { atomic.AddInt64(&calls, 1) })
549 cron.Schedule(new(ZeroSchedule), FuncJob(func() { t.Error("expected zero task will not run") }))
550 cron.Start()
551 defer cron.Stop()
552 <-time.After(OneSecond)
553 if atomic.LoadInt64(&calls) != 1 {
554 t.Errorf("called %d times, expected 1\n", calls)
555 }
556 }
557
558 func TestStopAndWait(t *testing.T) {
559 t.Run("nothing running, returns immediately", func(t *testing.T) {
560 cron := newWithSeconds()
561 cron.Start()
562 ctx := cron.Stop()
563 select {
564 case <-ctx.Done():
565 case <-time.After(time.Millisecond):
566 t.Error("context was not done immediately")
567 }
568 })
569
570 t.Run("repeated calls to Stop", func(t *testing.T) {
571 cron := newWithSeconds()
572 cron.Start()
573 _ = cron.Stop()
574 time.Sleep(time.Millisecond)
575 ctx := cron.Stop()
576 select {
577 case <-ctx.Done():
578 case <-time.After(time.Millisecond):
579 t.Error("context was not done immediately")
580 }
581 })
582
583 t.Run("a couple fast jobs added, still returns immediately", func(t *testing.T) {
584 cron := newWithSeconds()
585 cron.AddFunc("* * * * * *", func() {})
586 cron.Start()
587 cron.AddFunc("* * * * * *", func() {})
588 cron.AddFunc("* * * * * *", func() {})
589 cron.AddFunc("* * * * * *", func() {})
590 time.Sleep(time.Second)
591 ctx := cron.Stop()
592 select {
593 case <-ctx.Done():
594 case <-time.After(time.Millisecond):
595 t.Error("context was not done immediately")
596 }
597 })
598
599 t.Run("a couple fast jobs and a slow job added, waits for slow job", func(t *testing.T) {
600 cron := newWithSeconds()
601 cron.AddFunc("* * * * * *", func() {})
602 cron.Start()
603 cron.AddFunc("* * * * * *", func() { time.Sleep(2 * time.Second) })
604 cron.AddFunc("* * * * * *", func() {})
605 time.Sleep(time.Second)
606
607 ctx := cron.Stop()
608
609
610 select {
611 case <-ctx.Done():
612 t.Error("context was done too quickly immediately")
613 case <-time.After(750 * time.Millisecond):
614
615 }
616
617
618 select {
619 case <-ctx.Done():
620
621 case <-time.After(1500 * time.Millisecond):
622 t.Error("context not done after job should have completed")
623 }
624 })
625
626 t.Run("repeated calls to stop, waiting for completion and after", func(t *testing.T) {
627 cron := newWithSeconds()
628 cron.AddFunc("* * * * * *", func() {})
629 cron.AddFunc("* * * * * *", func() { time.Sleep(2 * time.Second) })
630 cron.Start()
631 cron.AddFunc("* * * * * *", func() {})
632 time.Sleep(time.Second)
633 ctx := cron.Stop()
634 ctx2 := cron.Stop()
635
636
637 select {
638 case <-ctx.Done():
639 t.Error("context was done too quickly immediately")
640 case <-ctx2.Done():
641 t.Error("context2 was done too quickly immediately")
642 case <-time.After(1500 * time.Millisecond):
643
644 }
645
646
647 select {
648 case <-ctx.Done():
649
650 case <-time.After(time.Second):
651 t.Error("context not done after job should have completed")
652 }
653
654
655 select {
656 case <-ctx2.Done():
657
658 case <-time.After(time.Millisecond):
659 t.Error("context2 not done even though context1 is")
660 }
661
662
663 ctx3 := cron.Stop()
664 select {
665 case <-ctx3.Done():
666
667 case <-time.After(time.Millisecond):
668 t.Error("context not done even when cron Stop is completed")
669 }
670
671 })
672 }
673
674 func TestMultiThreadedStartAndStop(t *testing.T) {
675 cron := New()
676 go cron.Run()
677 time.Sleep(2 * time.Millisecond)
678 cron.Stop()
679 }
680
681 func wait(wg *sync.WaitGroup) chan bool {
682 ch := make(chan bool)
683 go func() {
684 wg.Wait()
685 ch <- true
686 }()
687 return ch
688 }
689
690 func stop(cron *Cron) chan bool {
691 ch := make(chan bool)
692 go func() {
693 cron.Stop()
694 ch <- true
695 }()
696 return ch
697 }
698
699
700 func newWithSeconds() *Cron {
701 return New(WithParser(secondParser), WithChain())
702 }
703
View as plain text