123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702 |
- //nolint:all
- package cron
- import (
- "bytes"
- "fmt"
- "log"
- "strings"
- "sync"
- "sync/atomic"
- "testing"
- "time"
- )
- // Many tests schedule a job for every second, and then wait at most a second
- // for it to run. This amount is just slightly larger than 1 second to
- // compensate for a few milliseconds of runtime.
- const OneSecond = 1*time.Second + 50*time.Millisecond
- type syncWriter struct {
- wr bytes.Buffer
- m sync.Mutex
- }
- func (sw *syncWriter) Write(data []byte) (n int, err error) {
- sw.m.Lock()
- n, err = sw.wr.Write(data)
- sw.m.Unlock()
- return
- }
- func (sw *syncWriter) String() string {
- sw.m.Lock()
- defer sw.m.Unlock()
- return sw.wr.String()
- }
- func newBufLogger(sw *syncWriter) Logger {
- return PrintfLogger(log.New(sw, "", log.LstdFlags))
- }
- func TestFuncPanicRecovery(t *testing.T) {
- var buf syncWriter
- cron := New(WithParser(secondParser),
- WithChain(Recover(newBufLogger(&buf))))
- cron.Start()
- defer cron.Stop()
- cron.AddFunc("* * * * * ?", func() {
- panic("YOLO")
- })
- select {
- case <-time.After(OneSecond):
- if !strings.Contains(buf.String(), "YOLO") {
- t.Error("expected a panic to be logged, got none")
- }
- return
- }
- }
- type DummyJob struct{}
- func (DummyJob) Run() {
- panic("YOLO")
- }
- func TestJobPanicRecovery(t *testing.T) {
- var job DummyJob
- var buf syncWriter
- cron := New(WithParser(secondParser),
- WithChain(Recover(newBufLogger(&buf))))
- cron.Start()
- defer cron.Stop()
- cron.AddJob("* * * * * ?", job)
- select {
- case <-time.After(OneSecond):
- if !strings.Contains(buf.String(), "YOLO") {
- t.Error("expected a panic to be logged, got none")
- }
- return
- }
- }
- // Start and stop cron with no entries.
- func TestNoEntries(t *testing.T) {
- cron := newWithSeconds()
- cron.Start()
- select {
- case <-time.After(OneSecond):
- t.Fatal("expected cron will be stopped immediately")
- case <-stop(cron):
- }
- }
- // Start, stop, then add an entry. Verify entry doesn't run.
- func TestStopCausesJobsToNotRun(t *testing.T) {
- wg := &sync.WaitGroup{}
- wg.Add(1)
- cron := newWithSeconds()
- cron.Start()
- cron.Stop()
- cron.AddFunc("* * * * * ?", func() { wg.Done() })
- select {
- case <-time.After(OneSecond):
- // No job ran!
- case <-wait(wg):
- t.Fatal("expected stopped cron does not run any job")
- }
- }
- // Add a job, start cron, expect it runs.
- func TestAddBeforeRunning(t *testing.T) {
- wg := &sync.WaitGroup{}
- wg.Add(1)
- cron := newWithSeconds()
- cron.AddFunc("* * * * * ?", func() { wg.Done() })
- cron.Start()
- defer cron.Stop()
- // Give cron 2 seconds to run our job (which is always activated).
- select {
- case <-time.After(OneSecond):
- t.Fatal("expected job runs")
- case <-wait(wg):
- }
- }
- // Start cron, add a job, expect it runs.
- func TestAddWhileRunning(t *testing.T) {
- wg := &sync.WaitGroup{}
- wg.Add(1)
- cron := newWithSeconds()
- cron.Start()
- defer cron.Stop()
- cron.AddFunc("* * * * * ?", func() { wg.Done() })
- select {
- case <-time.After(OneSecond):
- t.Fatal("expected job runs")
- case <-wait(wg):
- }
- }
- // Test for #34. Adding a job after calling start results in multiple job invocations
- func TestAddWhileRunningWithDelay(t *testing.T) {
- cron := newWithSeconds()
- cron.Start()
- defer cron.Stop()
- time.Sleep(5 * time.Second)
- var calls int64
- cron.AddFunc("* * * * * *", func() { atomic.AddInt64(&calls, 1) })
- <-time.After(OneSecond)
- if atomic.LoadInt64(&calls) != 1 {
- t.Errorf("called %d times, expected 1\n", calls)
- }
- }
- // Add a job, remove a job, start cron, expect nothing runs.
- func TestRemoveBeforeRunning(t *testing.T) {
- wg := &sync.WaitGroup{}
- wg.Add(1)
- cron := newWithSeconds()
- id, _ := cron.AddFunc("* * * * * ?", func() { wg.Done() })
- cron.Remove(id)
- cron.Start()
- defer cron.Stop()
- select {
- case <-time.After(OneSecond):
- // Success, shouldn't run
- case <-wait(wg):
- t.FailNow()
- }
- }
- // Start cron, add a job, remove it, expect it doesn't run.
- func TestRemoveWhileRunning(t *testing.T) {
- wg := &sync.WaitGroup{}
- wg.Add(1)
- cron := newWithSeconds()
- cron.Start()
- defer cron.Stop()
- id, _ := cron.AddFunc("* * * * * ?", func() { wg.Done() })
- cron.Remove(id)
- select {
- case <-time.After(OneSecond):
- case <-wait(wg):
- t.FailNow()
- }
- }
- // Test timing with Entries.
- func TestSnapshotEntries(t *testing.T) {
- wg := &sync.WaitGroup{}
- wg.Add(1)
- cron := New()
- cron.AddFunc("@every 2s", func() { wg.Done() })
- cron.Start()
- defer cron.Stop()
- // Cron should fire in 2 seconds. After 1 second, call Entries.
- select {
- case <-time.After(OneSecond):
- cron.Entries()
- }
- // Even though Entries was called, the cron should fire at the 2 second mark.
- select {
- case <-time.After(OneSecond):
- t.Error("expected job runs at 2 second mark")
- case <-wait(wg):
- }
- }
- // Test that the entries are correctly sorted.
- // Add a bunch of long-in-the-future entries, and an immediate entry, and ensure
- // that the immediate entry runs immediately.
- // Also: Test that multiple jobs run in the same instant.
- func TestMultipleEntries(t *testing.T) {
- wg := &sync.WaitGroup{}
- wg.Add(2)
- cron := newWithSeconds()
- cron.AddFunc("0 0 0 1 1 ?", func() {})
- cron.AddFunc("* * * * * ?", func() { wg.Done() })
- id1, _ := cron.AddFunc("* * * * * ?", func() { t.Fatal() })
- id2, _ := cron.AddFunc("* * * * * ?", func() { t.Fatal() })
- cron.AddFunc("0 0 0 31 12 ?", func() {})
- cron.AddFunc("* * * * * ?", func() { wg.Done() })
- cron.Remove(id1)
- cron.Start()
- cron.Remove(id2)
- defer cron.Stop()
- select {
- case <-time.After(OneSecond):
- t.Error("expected job run in proper order")
- case <-wait(wg):
- }
- }
- // Test running the same job twice.
- func TestRunningJobTwice(t *testing.T) {
- wg := &sync.WaitGroup{}
- wg.Add(2)
- cron := newWithSeconds()
- cron.AddFunc("0 0 0 1 1 ?", func() {})
- cron.AddFunc("0 0 0 31 12 ?", func() {})
- cron.AddFunc("* * * * * ?", func() { wg.Done() })
- cron.Start()
- defer cron.Stop()
- select {
- case <-time.After(2 * OneSecond):
- t.Error("expected job fires 2 times")
- case <-wait(wg):
- }
- }
- func TestRunningMultipleSchedules(t *testing.T) {
- wg := &sync.WaitGroup{}
- wg.Add(2)
- cron := newWithSeconds()
- cron.AddFunc("0 0 0 1 1 ?", func() {})
- cron.AddFunc("0 0 0 31 12 ?", func() {})
- cron.AddFunc("* * * * * ?", func() { wg.Done() })
- cron.Schedule(Every(time.Minute), FuncJob(func() {}))
- cron.Schedule(Every(time.Second), FuncJob(func() { wg.Done() }))
- cron.Schedule(Every(time.Hour), FuncJob(func() {}))
- cron.Start()
- defer cron.Stop()
- select {
- case <-time.After(2 * OneSecond):
- t.Error("expected job fires 2 times")
- case <-wait(wg):
- }
- }
- // Test that the cron is run in the local time zone (as opposed to UTC).
- func TestLocalTimezone(t *testing.T) {
- wg := &sync.WaitGroup{}
- wg.Add(2)
- now := time.Now()
- // FIX: Issue #205
- // This calculation doesn't work in seconds 58 or 59.
- // Take the easy way out and sleep.
- if now.Second() >= 58 {
- time.Sleep(2 * time.Second)
- now = time.Now()
- }
- spec := fmt.Sprintf("%d,%d %d %d %d %d ?",
- now.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month())
- cron := newWithSeconds()
- cron.AddFunc(spec, func() { wg.Done() })
- cron.Start()
- defer cron.Stop()
- select {
- case <-time.After(OneSecond * 2):
- t.Error("expected job fires 2 times")
- case <-wait(wg):
- }
- }
- // Test that the cron is run in the given time zone (as opposed to local).
- func TestNonLocalTimezone(t *testing.T) {
- wg := &sync.WaitGroup{}
- wg.Add(2)
- loc, err := time.LoadLocation("Atlantic/Cape_Verde")
- if err != nil {
- fmt.Printf("Failed to load time zone Atlantic/Cape_Verde: %+v", err)
- t.Fail()
- }
- now := time.Now().In(loc)
- // FIX: Issue #205
- // This calculation doesn't work in seconds 58 or 59.
- // Take the easy way out and sleep.
- if now.Second() >= 58 {
- time.Sleep(2 * time.Second)
- now = time.Now().In(loc)
- }
- spec := fmt.Sprintf("%d,%d %d %d %d %d ?",
- now.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month())
- cron := New(WithLocation(loc), WithParser(secondParser))
- cron.AddFunc(spec, func() { wg.Done() })
- cron.Start()
- defer cron.Stop()
- select {
- case <-time.After(OneSecond * 2):
- t.Error("expected job fires 2 times")
- case <-wait(wg):
- }
- }
- // Test that calling stop before start silently returns without
- // blocking the stop channel.
- func TestStopWithoutStart(t *testing.T) {
- cron := New()
- cron.Stop()
- }
- type testJob struct {
- wg *sync.WaitGroup
- name string
- }
- func (t testJob) Run() {
- t.wg.Done()
- }
- // Test that adding an invalid job spec returns an error
- func TestInvalidJobSpec(t *testing.T) {
- cron := New()
- _, err := cron.AddJob("this will not parse", nil)
- if err == nil {
- t.Errorf("expected an error with invalid spec, got nil")
- }
- }
- // Test blocking run method behaves as Start()
- func TestBlockingRun(t *testing.T) {
- wg := &sync.WaitGroup{}
- wg.Add(1)
- cron := newWithSeconds()
- cron.AddFunc("* * * * * ?", func() { wg.Done() })
- var unblockChan = make(chan struct{})
- go func() {
- cron.Run()
- close(unblockChan)
- }()
- defer cron.Stop()
- select {
- case <-time.After(OneSecond):
- t.Error("expected job fires")
- case <-unblockChan:
- t.Error("expected that Run() blocks")
- case <-wait(wg):
- }
- }
- // Test that double-running is a no-op
- func TestStartNoop(t *testing.T) {
- var tickChan = make(chan struct{}, 2)
- cron := newWithSeconds()
- cron.AddFunc("* * * * * ?", func() {
- tickChan <- struct{}{}
- })
- cron.Start()
- defer cron.Stop()
- // Wait for the first firing to ensure the runner is going
- <-tickChan
- cron.Start()
- <-tickChan
- // Fail if this job fires again in a short period, indicating a double-run
- select {
- case <-time.After(time.Millisecond):
- case <-tickChan:
- t.Error("expected job fires exactly twice")
- }
- }
- // Simple test using Runnables.
- func TestJob(t *testing.T) {
- wg := &sync.WaitGroup{}
- wg.Add(1)
- cron := newWithSeconds()
- cron.AddJob("0 0 0 30 Feb ?", testJob{wg, "job0"})
- cron.AddJob("0 0 0 1 1 ?", testJob{wg, "job1"})
- job2, _ := cron.AddJob("* * * * * ?", testJob{wg, "job2"})
- cron.AddJob("1 0 0 1 1 ?", testJob{wg, "job3"})
- cron.Schedule(Every(5*time.Second+5*time.Nanosecond), testJob{wg, "job4"})
- job5 := cron.Schedule(Every(5*time.Minute), testJob{wg, "job5"})
- // Test getting an Entry pre-Start.
- if actualName := cron.Entry(job2).Job.(testJob).name; actualName != "job2" {
- t.Error("wrong job retrieved:", actualName)
- }
- if actualName := cron.Entry(job5).Job.(testJob).name; actualName != "job5" {
- t.Error("wrong job retrieved:", actualName)
- }
- cron.Start()
- defer cron.Stop()
- select {
- case <-time.After(OneSecond):
- t.FailNow()
- case <-wait(wg):
- }
- // Ensure the entries are in the right order.
- expecteds := []string{"job2", "job4", "job5", "job1", "job3", "job0"}
- var actuals []string
- for _, entry := range cron.Entries() {
- actuals = append(actuals, entry.Job.(testJob).name)
- }
- for i, expected := range expecteds {
- if actuals[i] != expected {
- t.Fatalf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals)
- }
- }
- // Test getting Entries.
- if actualName := cron.Entry(job2).Job.(testJob).name; actualName != "job2" {
- t.Error("wrong job retrieved:", actualName)
- }
- if actualName := cron.Entry(job5).Job.(testJob).name; actualName != "job5" {
- t.Error("wrong job retrieved:", actualName)
- }
- }
- // Issue #206
- // Ensure that the next run of a job after removing an entry is accurate.
- func TestScheduleAfterRemoval(t *testing.T) {
- var wg1 sync.WaitGroup
- var wg2 sync.WaitGroup
- wg1.Add(1)
- wg2.Add(1)
- // The first time this job is run, set a timer and remove the other job
- // 750ms later. Correct behavior would be to still run the job again in
- // 250ms, but the bug would cause it to run instead 1s later.
- var calls int
- var mu sync.Mutex
- cron := newWithSeconds()
- hourJob := cron.Schedule(Every(time.Hour), FuncJob(func() {}))
- cron.Schedule(Every(time.Second), FuncJob(func() {
- mu.Lock()
- defer mu.Unlock()
- switch calls {
- case 0:
- wg1.Done()
- calls++
- case 1:
- time.Sleep(750 * time.Millisecond)
- cron.Remove(hourJob)
- calls++
- case 2:
- calls++
- wg2.Done()
- case 3:
- panic("unexpected 3rd call")
- }
- }))
- cron.Start()
- defer cron.Stop()
- // the first run might be any length of time 0 - 1s, since the schedule
- // rounds to the second. wait for the first run to true up.
- wg1.Wait()
- select {
- case <-time.After(2 * OneSecond):
- t.Error("expected job fires 2 times")
- case <-wait(&wg2):
- }
- }
- type ZeroSchedule struct{}
- func (*ZeroSchedule) Next(time.Time) time.Time {
- return time.Time{}
- }
- // Tests that job without time does not run
- func TestJobWithZeroTimeDoesNotRun(t *testing.T) {
- cron := newWithSeconds()
- var calls int64
- cron.AddFunc("* * * * * *", func() { atomic.AddInt64(&calls, 1) })
- cron.Schedule(new(ZeroSchedule), FuncJob(func() { t.Error("expected zero task will not run") }))
- cron.Start()
- defer cron.Stop()
- <-time.After(OneSecond)
- if atomic.LoadInt64(&calls) != 1 {
- t.Errorf("called %d times, expected 1\n", calls)
- }
- }
- func TestStopAndWait(t *testing.T) {
- t.Run("nothing running, returns immediately", func(*testing.T) {
- cron := newWithSeconds()
- cron.Start()
- ctx := cron.Stop()
- select {
- case <-ctx.Done():
- case <-time.After(time.Millisecond):
- t.Error("context was not done immediately")
- }
- })
- t.Run("repeated calls to Stop", func(*testing.T) {
- cron := newWithSeconds()
- cron.Start()
- _ = cron.Stop()
- time.Sleep(time.Millisecond)
- ctx := cron.Stop()
- select {
- case <-ctx.Done():
- case <-time.After(time.Millisecond):
- t.Error("context was not done immediately")
- }
- })
- t.Run("a couple fast jobs added, still returns immediately", func(*testing.T) {
- cron := newWithSeconds()
- cron.AddFunc("* * * * * *", func() {})
- cron.Start()
- cron.AddFunc("* * * * * *", func() {})
- cron.AddFunc("* * * * * *", func() {})
- cron.AddFunc("* * * * * *", func() {})
- time.Sleep(time.Second)
- ctx := cron.Stop()
- select {
- case <-ctx.Done():
- case <-time.After(time.Millisecond):
- t.Error("context was not done immediately")
- }
- })
- t.Run("a couple fast jobs and a slow job added, waits for slow job", func(*testing.T) {
- cron := newWithSeconds()
- cron.AddFunc("* * * * * *", func() {})
- cron.Start()
- cron.AddFunc("* * * * * *", func() { time.Sleep(2 * time.Second) })
- cron.AddFunc("* * * * * *", func() {})
- time.Sleep(time.Second)
- ctx := cron.Stop()
- // Verify that it is not done for at least 750ms
- select {
- case <-ctx.Done():
- t.Error("context was done too quickly immediately")
- case <-time.After(750 * time.Millisecond):
- // expected, because the job sleeping for 1 second is still running
- }
- // Verify that it IS done in the next 500ms (giving 250ms buffer)
- select {
- case <-ctx.Done():
- // expected
- case <-time.After(1500 * time.Millisecond):
- t.Error("context not done after job should have completed")
- }
- })
- t.Run("repeated calls to stop, waiting for completion and after", func(*testing.T) {
- cron := newWithSeconds()
- cron.AddFunc("* * * * * *", func() {})
- cron.AddFunc("* * * * * *", func() { time.Sleep(2 * time.Second) })
- cron.Start()
- cron.AddFunc("* * * * * *", func() {})
- time.Sleep(time.Second)
- ctx := cron.Stop()
- ctx2 := cron.Stop()
- // Verify that it is not done for at least 1500ms
- select {
- case <-ctx.Done():
- t.Error("context was done too quickly immediately")
- case <-ctx2.Done():
- t.Error("context2 was done too quickly immediately")
- case <-time.After(1500 * time.Millisecond):
- // expected, because the job sleeping for 2 seconds is still running
- }
- // Verify that it IS done in the next 1s (giving 500ms buffer)
- select {
- case <-ctx.Done():
- // expected
- case <-time.After(time.Second):
- t.Error("context not done after job should have completed")
- }
- // Verify that ctx2 is also done.
- select {
- case <-ctx2.Done():
- // expected
- case <-time.After(time.Millisecond):
- t.Error("context2 not done even though context1 is")
- }
- // Verify that a new context retrieved from stop is immediately done.
- ctx3 := cron.Stop()
- select {
- case <-ctx3.Done():
- // expected
- case <-time.After(time.Millisecond):
- t.Error("context not done even when cron Stop is completed")
- }
- })
- }
- func TestMultiThreadedStartAndStop(t *testing.T) {
- cron := New()
- go cron.Run()
- time.Sleep(2 * time.Millisecond)
- cron.Stop()
- }
- func wait(wg *sync.WaitGroup) chan bool {
- ch := make(chan bool)
- go func() {
- wg.Wait()
- ch <- true
- }()
- return ch
- }
- func stop(cron *Cron) chan bool {
- ch := make(chan bool)
- go func() {
- cron.Stop()
- ch <- true
- }()
- return ch
- }
- // newWithSeconds returns a Cron with the seconds field enabled.
- func newWithSeconds() *Cron {
- return New(WithParser(secondParser), WithChain())
- }
|