diff --git a/example_test.go b/example_test.go index 1fc60883..f4909609 100644 --- a/example_test.go +++ b/example_test.go @@ -46,6 +46,14 @@ func ExampleScheduler_StartAsync() { s.StartAsync() } +func ExampleScheduler_Every() { + s := gocron.NewScheduler(time.UTC) + _, _ = s.Every(1).Second().Do(task) + _, _ = s.Every(1 * time.Second).Do(task) + _, _ = s.Every("1s").Do(task) + s.StartAsync() +} + func ExampleScheduler_StartAt() { s := gocron.NewScheduler(time.UTC) specificTime := time.Date(2019, time.November, 10, 15, 0, 0, 0, time.UTC) diff --git a/gocron.go b/gocron.go index 85b11b0b..fca8f4be 100644 --- a/gocron.go +++ b/gocron.go @@ -27,6 +27,8 @@ var ( ErrJobNotFoundWithTag = errors.New("no jobs found with given tag") ErrUnsupportedTimeFormat = errors.New("the given time format is not supported") ErrInvalidInterval = errors.New(".Every() interval must be greater than 0") + ErrInvalidIntervalType = errors.New(".Every() interval must be int, time.Duration, or string") + ErrInvalidSelection = errors.New("an .Every() duration interval cannot be used with units (e.g. .Seconds())") ) // regex patterns for supported time formats @@ -45,6 +47,7 @@ const ( days weeks months + duration ) func callJobFuncWithParams(jobFunc interface{}, params []interface{}) ([]reflect.Value, error) { diff --git a/job.go b/job.go index cec238b7..0866b0f3 100644 --- a/job.go +++ b/job.go @@ -6,12 +6,11 @@ import ( "time" ) -type jobInterval uint64 - // Job struct stores the information necessary to run a Job type Job struct { sync.RWMutex - interval jobInterval // pause interval * unit between runs + interval int // pause interval * unit between runs + duration time.Duration // time duration between runs unit timeUnit // time units, ,e.g. 'minutes', 'hours'... startsImmediately bool // if the Job should run upon scheduler start jobFunc string // the Job jobFunc to run, func[jobFunc] @@ -37,9 +36,9 @@ type runConfig struct { } // NewJob creates a new Job with the provided interval -func NewJob(interval uint64) *Job { +func NewJob(interval int) *Job { return &Job{ - interval: jobInterval(interval), + interval: interval, lastRun: time.Time{}, nextRun: time.Time{}, funcs: make(map[string]interface{}), diff --git a/scheduler.go b/scheduler.go index 997ba390..388f3f0a 100644 --- a/scheduler.go +++ b/scheduler.go @@ -156,22 +156,24 @@ func (s *Scheduler) durationToNextRun(lastRun time.Time, job *Job) time.Duration return job.getStartAtTime().Sub(s.now()) } - var duration time.Duration + var d time.Duration switch job.unit { case seconds, minutes, hours: - duration = s.calculateDuration(job) + d = s.calculateDuration(job) case days: - duration = s.calculateDays(job, lastRun) + d = s.calculateDays(job, lastRun) case weeks: if job.scheduledWeekday != nil { // weekday selected, Every().Monday(), for example - duration = s.calculateWeekday(job, lastRun) + d = s.calculateWeekday(job, lastRun) } else { - duration = s.calculateWeeks(job, lastRun) + d = s.calculateWeeks(job, lastRun) } case months: - duration = s.calculateMonths(job, lastRun) + d = s.calculateMonths(job, lastRun) + case duration: + d = job.duration } - return duration + return d } func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) time.Duration { @@ -294,12 +296,33 @@ func (s *Scheduler) NextRun() (*Job, time.Time) { } // Every schedules a new periodic Job with interval -func (s *Scheduler) Every(interval uint64) *Scheduler { - job := NewJob(interval) - if interval == 0 { - job.err = ErrInvalidInterval +func (s *Scheduler) Every(interval interface{}) *Scheduler { + switch interval := interval.(type) { + case int: + job := NewJob(interval) + if interval <= 0 { + job.err = ErrInvalidInterval + } + s.setJobs(append(s.Jobs(), job)) + case time.Duration: + job := NewJob(0) + job.duration = interval + job.unit = duration + s.setJobs(append(s.Jobs(), job)) + case string: + job := NewJob(0) + d, err := time.ParseDuration(interval) + if err != nil { + job.err = err + } + job.duration = d + job.unit = duration + s.setJobs(append(s.Jobs(), job)) + default: + job := NewJob(0) + job.err = ErrInvalidIntervalType + s.setJobs(append(s.Jobs(), job)) } - s.setJobs(append(s.Jobs(), job)) return s } @@ -484,8 +507,11 @@ func (s *Scheduler) StartAt(t time.Time) *Scheduler { // setUnit sets the unit type func (s *Scheduler) setUnit(unit timeUnit) { - currentJob := s.getCurrentJob() - currentJob.unit = unit + job := s.getCurrentJob() + if job.unit == duration { + job.err = ErrInvalidSelection + } + job.unit = unit } // Second sets the unit with seconds diff --git a/scheduler_test.go b/scheduler_test.go index a76fcf77..23ffae46 100644 --- a/scheduler_test.go +++ b/scheduler_test.go @@ -2,6 +2,7 @@ package gocron import ( "fmt" + "strings" "sync" "testing" "time" @@ -11,6 +12,8 @@ import ( "github.com/stretchr/testify/assert" ) +var _ timeWrapper = (*fakeTime)(nil) + type fakeTime struct { onNow func(location *time.Location) time.Time } @@ -27,10 +30,6 @@ func (f fakeTime) Sleep(duration time.Duration) { panic("implement me") } -func (f fakeTime) NewTicker(duration time.Duration) *time.Ticker { - panic("implement me") -} - func task() { fmt.Println("I am a running job.") } @@ -57,11 +56,99 @@ func TestImmediateExecution(t *testing.T) { } func TestInvalidEveryInterval(t *testing.T) { + testCases := []struct { + description string + interval interface{} + expectedError string + }{ + {"zero", 0, ErrInvalidInterval.Error()}, + {"negative", -1, ErrInvalidInterval.Error()}, + {"invalid string duration", "bad", "time: invalid duration"}, + } + s := NewScheduler(time.UTC) - _, err := s.Every(0).Do(func() {}) + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + _, err := s.Every(tc.interval).Do(func() {}) + require.Error(t, err) + // wonky way to assert on the error message, but between go 1.14 + // and go 1.15 the error value was wrapped in quotes + assert.True(t, strings.HasPrefix(err.Error(), tc.expectedError)) + }) + } - assert.EqualError(t, err, ErrInvalidInterval.Error()) +} + +func TestScheduler_Every(t *testing.T) { + t.Run("time.Duration", func(t *testing.T) { + s := NewScheduler(time.UTC) + semaphore := make(chan bool) + + _, err := s.Every(100 * time.Millisecond).Do(func() { + semaphore <- true + }) + require.NoError(t, err) + + s.StartAsync() + + var counter int + + now := time.Now() + for time.Now().Before(now.Add(500 * time.Millisecond)) { + if <-semaphore { + counter++ + } + } + s.Stop() + assert.Equal(t, 6, counter) + }) + + t.Run("int", func(t *testing.T) { + s := NewScheduler(time.UTC) + semaphore := make(chan bool) + + _, err := s.Every(1).Second().Do(func() { + semaphore <- true + }) + require.NoError(t, err) + + s.StartAsync() + + var counter int + + now := time.Now() + for time.Now().Before(now.Add(1 * time.Second)) { + if <-semaphore { + counter++ + } + } + s.Stop() + assert.Equal(t, 2, counter) + }) + + t.Run("string duration", func(t *testing.T) { + s := NewScheduler(time.UTC) + semaphore := make(chan bool) + + _, err := s.Every("1s").Do(func() { + semaphore <- true + }) + require.NoError(t, err) + + s.StartAsync() + + var counter int + + now := time.Now() + for time.Now().Before(now.Add(1 * time.Second)) { + if <-semaphore { + counter++ + } + } + s.Stop() + assert.Equal(t, 2, counter) + }) } func TestExecutionSeconds(t *testing.T) { @@ -70,8 +157,8 @@ func TestExecutionSeconds(t *testing.T) { var ( executions []int64 - interval uint64 = 2 - expectedExecutions = 4 + interval = 2 + expectedExecutions = 4 mu sync.RWMutex ) @@ -158,7 +245,7 @@ func TestAt(t *testing.T) { }) } -func schedulerForNextOrPreviousWeekdayEveryNTimes(weekday time.Weekday, next bool, n uint64, s *Scheduler) *Scheduler { +func schedulerForNextOrPreviousWeekdayEveryNTimes(weekday time.Weekday, next bool, n int, s *Scheduler) *Scheduler { switch weekday { case time.Monday: if next { @@ -211,7 +298,8 @@ func TestWeekdayBeforeToday(t *testing.T) { s := NewScheduler(time.UTC) s = schedulerForNextOrPreviousWeekdayEveryNTimes(now.Weekday(), false, 1, s) - weekJob, _ := s.Do(task) + weekJob, err := s.Do(task) + require.NoError(t, err) s.scheduleNextRun(weekJob) sixDaysFromNow := now.AddDate(0, 0, 6) diff --git a/timeHelper.go b/timeHelper.go index ef5d45ab..b5baeb57 100644 --- a/timeHelper.go +++ b/timeHelper.go @@ -2,11 +2,12 @@ package gocron import "time" +var _ timeWrapper = (*trueTime)(nil) + type timeWrapper interface { Now(*time.Location) time.Time Unix(int64, int64) time.Time Sleep(time.Duration) - NewTicker(time.Duration) *time.Ticker } type trueTime struct{} @@ -22,7 +23,3 @@ func (t *trueTime) Unix(sec int64, nsec int64) time.Time { func (t *trueTime) Sleep(d time.Duration) { time.Sleep(d) } - -func (t *trueTime) NewTicker(d time.Duration) *time.Ticker { - return time.NewTicker(d) -}