mirror of
https://github.com/usememos/memos.git
synced 2024-11-14 10:44:50 +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())
|
|
}
|