mirror of
				https://github.com/usememos/memos.git
				synced 2025-11-01 01:06:04 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			702 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			702 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| //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())
 | |
| }
 |