Skip to content

Commit

Permalink
Add seconds, N/step handling, named months/dows, checking activation …
Browse files Browse the repository at this point in the history
…at a time, a bunch of cron spec tests.
  • Loading branch information
robfig committed Jul 13, 2012
1 parent 4beaaae commit f16e0d5
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 59 deletions.
81 changes: 81 additions & 0 deletions cron.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,86 @@
// This library implements a cron spec parser and runner. See the README for
// more details.
package cron

import (
"time"
)

// Cron keeps track of any number of entries, invoking the associated func as
// specified by the spec. See http://en.wikipedia.org/wiki/Cron
// It may be started and stopped.
type Cron struct {
Entries []*Entry
stop chan struct{}
}

// A cron entry consists of a schedule and the func to execute on that schedule.
type Entry struct {
*Schedule
Func func()
}

func New() *Cron {
return new(Cron)
}

func (c *Cron) Add(spec string, cmd func()) {
c.Entries = append(c.Entries, &Entry{Parse(spec), cmd})
}

func (c *Cron) Run() {
ticker := time.Tick(1 * time.Minute)
for {
select {
case now := <-ticker:
for _, entry := range c.Entries {
if matches(now, entry.Schedule) {
go entry.Func()
}
}

case <-c.stop:
return
}
}
}

func (c Cron) Stop() {
c.stop <- struct{}{}
}

// Return true if the given entries overlap.
func matches(t time.Time, sched *Schedule) bool {
var (
domMatch bool = 1<<uint(t.Day())&sched.Dom > 0
dowMatch bool = 1<<uint(t.Weekday())&sched.Dow > 0
dayMatch bool
)

if sched.Dom&STAR_BIT > 0 || sched.Dow&STAR_BIT > 0 {
dayMatch = domMatch && dowMatch
} else {
dayMatch = domMatch || dowMatch
}

return 1<<uint(t.Minute())&sched.Minute > 0 &&
1<<uint(t.Hour())&sched.Hour > 0 &&
1<<uint(t.Month())&sched.Month > 0 &&
dayMatch
}

// // Return the number of units betwee now and then.
// func difference(then, now uint64, r bounds) uint {
// // Shift the current time fields left (and around) until & is non-zero.
// i := 0
// for then & now << ((i - r.min) % (r.max - r.min + 1) + r.min) == 0 {
// // A guard against no units selected.
// if i > r.max {
// panic("Entry had no minute/hour selected.")
// }

// i++
// }

// return i
// }
73 changes: 73 additions & 0 deletions cron_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package cron

import (
"testing"
"time"
)

func TestActivation(t *testing.T) {
tests := []struct {
time, spec string
expected bool
}{
// Every fifteen minutes.
{"Mon Jul 9 15:00 2012", "0 0/15 * * *", true},
{"Mon Jul 9 15:45 2012", "0 0/15 * * *", true},
{"Mon Jul 9 15:40 2012", "0 0/15 * * *", false},

// Every fifteen minutes, starting at 5 minutes.
{"Mon Jul 9 15:05 2012", "0 5/15 * * *", true},
{"Mon Jul 9 15:20 2012", "0 5/15 * * *", true},
{"Mon Jul 9 15:50 2012", "0 5/15 * * *", true},

// Named months
{"Sun Jul 15 15:00 2012", "0 0/15 * * Jul", true},
{"Sun Jul 15 15:00 2012", "0 0/15 * * Jun", false},

// Everything set.
{"Sun Jul 15 08:30 2012", "0 30 08 ? Jul Sun", true},
{"Sun Jul 15 08:30 2012", "0 30 08 15 Jul ?", true},
{"Mon Jul 16 08:30 2012", "0 30 08 ? Jul Sun", false},
{"Mon Jul 16 08:30 2012", "0 30 08 15 Jul ?", false},

// Predefined schedules
{"Mon Jul 9 15:00 2012", "@hourly", true},
{"Mon Jul 9 15:04 2012", "@hourly", false},
{"Mon Jul 9 15:00 2012", "@daily", false},
{"Mon Jul 9 00:00 2012", "@daily", true},
{"Mon Jul 9 00:00 2012", "@weekly", false},
{"Sun Jul 8 00:00 2012", "@weekly", true},
{"Sun Jul 8 01:00 2012", "@weekly", false},
{"Sun Jul 8 00:00 2012", "@monthly", false},
{"Sun Jul 1 00:00 2012", "@monthly", true},

// Test interaction of DOW and DOM.
// If both are specified, then only one needs to match.
{"Sun Jul 15 00:00 2012", "0 * * 1,15 * Sun", true},
{"Fri Jun 15 00:00 2012", "0 * * 1,15 * Sun", true},
{"Wed Aug 1 00:00 2012", "0 * * 1,15 * Sun", true},

// However, if one has a star, then both need to match.
{"Sun Jul 15 00:00 2012", "0 * * * * Mon", false},
{"Sun Jul 15 00:00 2012", "0 * * */10 * Sun", false},
{"Mon Jul 9 00:00 2012", "0 * * 1,15 * *", false},
{"Sun Jul 15 00:00 2012", "0 * * 1,15 * *", true},
}

for _, test := range tests {
actual := matches(getTime(test.time), Parse(test.spec))
if test.expected != actual {
t.Logf("Actual Minutes mask: %b", Parse(test.spec).Minute)
t.Errorf("Fail evaluating %s on %s: (expected) %t != %t (actual)",
test.spec, test.time, test.expected, actual)
}
}
}

func getTime(value string) time.Time {
t, err := time.Parse("Mon Jan 2 15:04 2006", value)
if err != nil {
panic(err)
}
return t
}
Loading

0 comments on commit f16e0d5

Please sign in to comment.