Make it easier to keep track of upstream changes and bug fixestags/v1.21.12.1
@@ -17,6 +17,7 @@ github.com/go-sql-driver/mysql = commit:b4db83c | |||
github.com/go-xorm/core = commit:1e2868c | |||
github.com/go-xorm/xorm = | |||
github.com/gogits/chardet = commit:2404f77725 | |||
github.com/gogits/cron = | |||
github.com/gogits/git-module = commit:3b40eae | |||
github.com/gogits/go-gogs-client = | |||
github.com/issue9/identicon = commit:f8c0d2c | |||
@@ -1,59 +0,0 @@ | |||
// Copyright 2014 The Gogs Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
package cron | |||
import ( | |||
"time" | |||
"github.com/gogits/gogs/models" | |||
"github.com/gogits/gogs/modules/cron" | |||
"github.com/gogits/gogs/modules/log" | |||
"github.com/gogits/gogs/modules/setting" | |||
) | |||
var c = cron.New() | |||
func NewContext() { | |||
var ( | |||
entry *cron.Entry | |||
err error | |||
) | |||
if setting.Cron.UpdateMirror.Enabled { | |||
entry, err = c.AddFunc("Update mirrors", setting.Cron.UpdateMirror.Schedule, models.MirrorUpdate) | |||
if err != nil { | |||
log.Fatal(4, "Cron[Update mirrors]: %v", err) | |||
} | |||
if setting.Cron.UpdateMirror.RunAtStart { | |||
entry.Prev = time.Now() | |||
go models.MirrorUpdate() | |||
} | |||
} | |||
if setting.Cron.RepoHealthCheck.Enabled { | |||
entry, err = c.AddFunc("Repository health check", setting.Cron.RepoHealthCheck.Schedule, models.GitFsck) | |||
if err != nil { | |||
log.Fatal(4, "Cron[Repository health check]: %v", err) | |||
} | |||
if setting.Cron.RepoHealthCheck.RunAtStart { | |||
entry.Prev = time.Now() | |||
go models.GitFsck() | |||
} | |||
} | |||
if setting.Cron.CheckRepoStats.Enabled { | |||
entry, err = c.AddFunc("Check repository statistics", setting.Cron.CheckRepoStats.Schedule, models.CheckRepoStats) | |||
if err != nil { | |||
log.Fatal(4, "Cron[Check repository statistics]: %v", err) | |||
} | |||
if setting.Cron.CheckRepoStats.RunAtStart { | |||
entry.Prev = time.Now() | |||
go models.CheckRepoStats() | |||
} | |||
} | |||
c.Start() | |||
} | |||
// ListTasks returns all running cron tasks. | |||
func ListTasks() []*cron.Entry { | |||
return c.Entries() | |||
} |
@@ -1,27 +0,0 @@ | |||
package cron | |||
import "time" | |||
// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes". | |||
// It does not support jobs more frequent than once a second. | |||
type ConstantDelaySchedule struct { | |||
Delay time.Duration | |||
} | |||
// Every returns a crontab Schedule that activates once every duration. | |||
// Delays of less than a second are not supported (will round up to 1 second). | |||
// Any fields less than a Second are truncated. | |||
func Every(duration time.Duration) ConstantDelaySchedule { | |||
if duration < time.Second { | |||
duration = time.Second | |||
} | |||
return ConstantDelaySchedule{ | |||
Delay: duration - time.Duration(duration.Nanoseconds())%time.Second, | |||
} | |||
} | |||
// Next returns the next time this should be run. | |||
// This rounds so that the next activation time will be on the second. | |||
func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time { | |||
return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond) | |||
} |
@@ -1,54 +0,0 @@ | |||
package cron | |||
import ( | |||
"testing" | |||
"time" | |||
) | |||
func TestConstantDelayNext(t *testing.T) { | |||
tests := []struct { | |||
time string | |||
delay time.Duration | |||
expected string | |||
}{ | |||
// Simple cases | |||
{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, | |||
{"Mon Jul 9 14:59 2012", 15 * time.Minute, "Mon Jul 9 15:14 2012"}, | |||
{"Mon Jul 9 14:59:59 2012", 15 * time.Minute, "Mon Jul 9 15:14:59 2012"}, | |||
// Wrap around hours | |||
{"Mon Jul 9 15:45 2012", 35 * time.Minute, "Mon Jul 9 16:20 2012"}, | |||
// Wrap around days | |||
{"Mon Jul 9 23:46 2012", 14 * time.Minute, "Tue Jul 10 00:00 2012"}, | |||
{"Mon Jul 9 23:45 2012", 35 * time.Minute, "Tue Jul 10 00:20 2012"}, | |||
{"Mon Jul 9 23:35:51 2012", 44*time.Minute + 24*time.Second, "Tue Jul 10 00:20:15 2012"}, | |||
{"Mon Jul 9 23:35:51 2012", 25*time.Hour + 44*time.Minute + 24*time.Second, "Thu Jul 11 01:20:15 2012"}, | |||
// Wrap around months | |||
{"Mon Jul 9 23:35 2012", 91*24*time.Hour + 25*time.Minute, "Thu Oct 9 00:00 2012"}, | |||
// Wrap around minute, hour, day, month, and year | |||
{"Mon Dec 31 23:59:45 2012", 15 * time.Second, "Tue Jan 1 00:00:00 2013"}, | |||
// Round to nearest second on the delay | |||
{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, | |||
// Round up to 1 second if the duration is less. | |||
{"Mon Jul 9 14:45:00 2012", 15 * time.Millisecond, "Mon Jul 9 14:45:01 2012"}, | |||
// Round to nearest second when calculating the next time. | |||
{"Mon Jul 9 14:45:00.005 2012", 15 * time.Minute, "Mon Jul 9 15:00 2012"}, | |||
// Round to nearest second for both. | |||
{"Mon Jul 9 14:45:00.005 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, | |||
} | |||
for _, c := range tests { | |||
actual := Every(c.delay).Next(getTime(c.time)) | |||
expected := getTime(c.expected) | |||
if actual != expected { | |||
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.delay, expected, actual) | |||
} | |||
} | |||
} |
@@ -1,4 +1,3 @@ | |||
// Copyright 2012 Rob Figueiredo. All rights reserved. | |||
// Copyright 2014 The Gogs Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
@@ -6,207 +5,59 @@ | |||
package cron | |||
import ( | |||
"sort" | |||
"time" | |||
) | |||
// Cron keeps track of any number of entries, invoking the associated func as | |||
// specified by the schedule. It may be started, stopped, and the entries may | |||
// be inspected while running. | |||
type Cron struct { | |||
entries []*Entry | |||
stop chan struct{} | |||
add chan *Entry | |||
snapshot chan []*Entry | |||
running bool | |||
} | |||
// Job is an interface for submitted cron jobs. | |||
type Job interface { | |||
Run() | |||
} | |||
// The Schedule describes a job's duty cycle. | |||
type Schedule interface { | |||
// Return the next activation time, later than the given time. | |||
// Next is invoked initially, and then each time the job is run. | |||
Next(time.Time) time.Time | |||
} | |||
// Entry consists of a schedule and the func to execute on that schedule. | |||
type Entry struct { | |||
Description string | |||
Spec string | |||
// The schedule on which this job should be run. | |||
Schedule Schedule | |||
// The next time the job will run. This is the zero time if Cron has not been | |||
// started or this entry's schedule is unsatisfiable | |||
Next time.Time | |||
// The last time this job was run. This is the zero time if the job has never | |||
// been run. | |||
Prev time.Time | |||
// The Job to run. | |||
Job Job | |||
ExecTimes int // Execute times count. | |||
} | |||
// byTime is a wrapper for sorting the entry array by time | |||
// (with zero time at the end). | |||
type byTime []*Entry | |||
func (s byTime) Len() int { return len(s) } | |||
func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } | |||
func (s byTime) Less(i, j int) bool { | |||
// Two zero times should return false. | |||
// Otherwise, zero is "greater" than any other time. | |||
// (To sort it at the end of the list.) | |||
if s[i].Next.IsZero() { | |||
return false | |||
} | |||
if s[j].Next.IsZero() { | |||
return true | |||
} | |||
return s[i].Next.Before(s[j].Next) | |||
} | |||
// New returns a new Cron job runner. | |||
func New() *Cron { | |||
return &Cron{ | |||
entries: nil, | |||
add: make(chan *Entry), | |||
stop: make(chan struct{}), | |||
snapshot: make(chan []*Entry), | |||
running: false, | |||
} | |||
} | |||
// A wrapper that turns a func() into a cron.Job | |||
type FuncJob func() | |||
func (f FuncJob) Run() { f() } | |||
// AddFunc adds a func to the Cron to be run on the given schedule. | |||
func (c *Cron) AddFunc(desc, spec string, cmd func()) (*Entry, error) { | |||
return c.AddJob(desc, spec, FuncJob(cmd)) | |||
} | |||
// AddFunc adds a Job to the Cron to be run on the given schedule. | |||
func (c *Cron) AddJob(desc, spec string, cmd Job) (*Entry, error) { | |||
schedule, err := Parse(spec) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return c.Schedule(desc, spec, schedule, cmd), nil | |||
} | |||
"github.com/gogits/cron" | |||
// Schedule adds a Job to the Cron to be run on the given schedule. | |||
func (c *Cron) Schedule(desc, spec string, schedule Schedule, cmd Job) *Entry { | |||
entry := &Entry{ | |||
Description: desc, | |||
Spec: spec, | |||
Schedule: schedule, | |||
Job: cmd, | |||
} | |||
if c.running { | |||
c.add <- entry | |||
} else { | |||
c.entries = append(c.entries, entry) | |||
} | |||
return entry | |||
} | |||
"github.com/gogits/gogs/models" | |||
"github.com/gogits/gogs/modules/log" | |||
"github.com/gogits/gogs/modules/setting" | |||
) | |||
// Entries returns a snapshot of the cron entries. | |||
func (c *Cron) Entries() []*Entry { | |||
if c.running { | |||
c.snapshot <- nil | |||
x := <-c.snapshot | |||
return x | |||
var c = cron.New() | |||
func NewContext() { | |||
var ( | |||
entry *cron.Entry | |||
err error | |||
) | |||
if setting.Cron.UpdateMirror.Enabled { | |||
entry, err = c.AddFunc("Update mirrors", setting.Cron.UpdateMirror.Schedule, models.MirrorUpdate) | |||
if err != nil { | |||
log.Fatal(4, "Cron[Update mirrors]: %v", err) | |||
} | |||
if setting.Cron.UpdateMirror.RunAtStart { | |||
entry.Prev = time.Now() | |||
entry.ExecTimes++ | |||
go models.MirrorUpdate() | |||
} | |||
} | |||
return c.entrySnapshot() | |||
} | |||
// Start the cron scheduler in its own go-routine. | |||
func (c *Cron) Start() { | |||
c.running = true | |||
go c.run() | |||
} | |||
// Run the scheduler.. this is private just due to the need to synchronize | |||
// access to the 'running' state variable. | |||
func (c *Cron) run() { | |||
// Figure out the next activation times for each entry. | |||
now := time.Now().Local() | |||
for _, entry := range c.entries { | |||
entry.Next = entry.Schedule.Next(now) | |||
if setting.Cron.RepoHealthCheck.Enabled { | |||
entry, err = c.AddFunc("Repository health check", setting.Cron.RepoHealthCheck.Schedule, models.GitFsck) | |||
if err != nil { | |||
log.Fatal(4, "Cron[Repository health check]: %v", err) | |||
} | |||
if setting.Cron.RepoHealthCheck.RunAtStart { | |||
entry.Prev = time.Now() | |||
entry.ExecTimes++ | |||
go models.GitFsck() | |||
} | |||
} | |||
for { | |||
// Determine the next entry to run. | |||
sort.Sort(byTime(c.entries)) | |||
var effective time.Time | |||
if len(c.entries) == 0 || c.entries[0].Next.IsZero() { | |||
// If there are no entries yet, just sleep - it still handles new entries | |||
// and stop requests. | |||
effective = now.AddDate(10, 0, 0) | |||
} else { | |||
effective = c.entries[0].Next | |||
if setting.Cron.CheckRepoStats.Enabled { | |||
entry, err = c.AddFunc("Check repository statistics", setting.Cron.CheckRepoStats.Schedule, models.CheckRepoStats) | |||
if err != nil { | |||
log.Fatal(4, "Cron[Check repository statistics]: %v", err) | |||
} | |||
select { | |||
case now = <-time.After(effective.Sub(now)): | |||
// Run every entry whose next time was this effective time. | |||
for _, e := range c.entries { | |||
if e.Next != effective { | |||
break | |||
} | |||
go e.Job.Run() | |||
e.ExecTimes++ | |||
e.Prev = e.Next | |||
e.Next = e.Schedule.Next(effective) | |||
} | |||
continue | |||
case newEntry := <-c.add: | |||
c.entries = append(c.entries, newEntry) | |||
newEntry.Next = newEntry.Schedule.Next(now) | |||
case <-c.snapshot: | |||
c.snapshot <- c.entrySnapshot() | |||
case <-c.stop: | |||
return | |||
if setting.Cron.CheckRepoStats.RunAtStart { | |||
entry.Prev = time.Now() | |||
entry.ExecTimes++ | |||
go models.CheckRepoStats() | |||
} | |||
// 'now' should be updated after newEntry and snapshot cases. | |||
now = time.Now().Local() | |||
} | |||
c.Start() | |||
} | |||
// Stop the cron scheduler. | |||
func (c *Cron) Stop() { | |||
c.stop <- struct{}{} | |||
c.running = false | |||
} | |||
// entrySnapshot returns a copy of the current cron entry list. | |||
func (c *Cron) entrySnapshot() []*Entry { | |||
entries := make([]*Entry, 0, len(c.entries)) | |||
for _, e := range c.entries { | |||
entries = append(entries, &Entry{ | |||
Description: e.Description, | |||
Spec: e.Spec, | |||
Schedule: e.Schedule, | |||
Next: e.Next, | |||
Prev: e.Prev, | |||
Job: e.Job, | |||
ExecTimes: e.ExecTimes, | |||
}) | |||
} | |||
return entries | |||
// ListTasks returns all running cron tasks. | |||
func ListTasks() []*cron.Entry { | |||
return c.Entries() | |||
} |
@@ -1,255 +0,0 @@ | |||
package cron | |||
import ( | |||
"fmt" | |||
"sync" | |||
"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 ONE_SECOND = 1*time.Second + 10*time.Millisecond | |||
// Start and stop cron with no entries. | |||
func TestNoEntries(t *testing.T) { | |||
cron := New() | |||
cron.Start() | |||
select { | |||
case <-time.After(ONE_SECOND): | |||
t.FailNow() | |||
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 := New() | |||
cron.Start() | |||
cron.Stop() | |||
cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) | |||
select { | |||
case <-time.After(ONE_SECOND): | |||
// No job ran! | |||
case <-wait(wg): | |||
t.FailNow() | |||
} | |||
} | |||
// Add a job, start cron, expect it runs. | |||
func TestAddBeforeRunning(t *testing.T) { | |||
wg := &sync.WaitGroup{} | |||
wg.Add(1) | |||
cron := New() | |||
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(ONE_SECOND): | |||
t.FailNow() | |||
case <-wait(wg): | |||
} | |||
} | |||
// Start cron, add a job, expect it runs. | |||
func TestAddWhileRunning(t *testing.T) { | |||
wg := &sync.WaitGroup{} | |||
wg.Add(1) | |||
cron := New() | |||
cron.Start() | |||
defer cron.Stop() | |||
cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) | |||
select { | |||
case <-time.After(ONE_SECOND): | |||
t.FailNow() | |||
case <-wait(wg): | |||
} | |||
} | |||
// 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(ONE_SECOND): | |||
cron.Entries() | |||
} | |||
// Even though Entries was called, the cron should fire at the 2 second mark. | |||
select { | |||
case <-time.After(ONE_SECOND): | |||
t.FailNow() | |||
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 := New() | |||
cron.AddFunc("", "0 0 0 1 1 ?", func() {}) | |||
cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) | |||
cron.AddFunc("", "0 0 0 31 12 ?", func() {}) | |||
cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) | |||
cron.Start() | |||
defer cron.Stop() | |||
select { | |||
case <-time.After(ONE_SECOND): | |||
t.FailNow() | |||
case <-wait(wg): | |||
} | |||
} | |||
// Test running the same job twice. | |||
func TestRunningJobTwice(t *testing.T) { | |||
wg := &sync.WaitGroup{} | |||
wg.Add(2) | |||
cron := New() | |||
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 * ONE_SECOND): | |||
t.FailNow() | |||
case <-wait(wg): | |||
} | |||
} | |||
func TestRunningMultipleSchedules(t *testing.T) { | |||
wg := &sync.WaitGroup{} | |||
wg.Add(2) | |||
cron := New() | |||
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 * ONE_SECOND): | |||
t.FailNow() | |||
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(1) | |||
now := time.Now().Local() | |||
spec := fmt.Sprintf("%d %d %d %d %d ?", | |||
now.Second()+1, now.Minute(), now.Hour(), now.Day(), now.Month()) | |||
cron := New() | |||
cron.AddFunc("", spec, func() { wg.Done() }) | |||
cron.Start() | |||
defer cron.Stop() | |||
select { | |||
case <-time.After(ONE_SECOND): | |||
t.FailNow() | |||
case <-wait(wg): | |||
} | |||
} | |||
type testJob struct { | |||
wg *sync.WaitGroup | |||
name string | |||
} | |||
func (t testJob) Run() { | |||
t.wg.Done() | |||
} | |||
// Simple test using Runnables. | |||
func TestJob(t *testing.T) { | |||
wg := &sync.WaitGroup{} | |||
wg.Add(1) | |||
cron := New() | |||
cron.AddJob("", "0 0 0 30 Feb ?", testJob{wg, "job0"}) | |||
cron.AddJob("", "0 0 0 1 1 ?", testJob{wg, "job1"}) | |||
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"}) | |||
cron.Schedule("", "", Every(5*time.Minute), testJob{wg, "job5"}) | |||
cron.Start() | |||
defer cron.Stop() | |||
select { | |||
case <-time.After(ONE_SECOND): | |||
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.Errorf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals) | |||
t.FailNow() | |||
} | |||
} | |||
} | |||
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 | |||
} |
@@ -1,129 +0,0 @@ | |||
/* | |||
Package cron implements a cron spec parser and job runner. | |||
Usage | |||
Callers may register Funcs to be invoked on a given schedule. Cron will run | |||
them in their own goroutines. | |||
c := cron.New() | |||
c.AddFunc("Every hour on the half hour","0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) | |||
c.AddFunc("Every hour","@hourly", func() { fmt.Println("Every hour") }) | |||
c.AddFunc("Every hour and a half","@every 1h30m", func() { fmt.Println("Every hour thirty") }) | |||
c.Start() | |||
.. | |||
// Funcs are invoked in their own goroutine, asynchronously. | |||
... | |||
// Funcs may also be added to a running Cron | |||
c.AddFunc("@daily", func() { fmt.Println("Every day") }) | |||
.. | |||
// Inspect the cron job entries' next and previous run times. | |||
inspect(c.Entries()) | |||
.. | |||
c.Stop() // Stop the scheduler (does not stop any jobs already running). | |||
CRON Expression Format | |||
A cron expression represents a set of times, using 6 space-separated fields. | |||
Field name | Mandatory? | Allowed values | Allowed special characters | |||
---------- | ---------- | -------------- | -------------------------- | |||
Seconds | Yes | 0-59 | * / , - | |||
Minutes | Yes | 0-59 | * / , - | |||
Hours | Yes | 0-23 | * / , - | |||
Day of month | Yes | 1-31 | * / , - ? | |||
Month | Yes | 1-12 or JAN-DEC | * / , - | |||
Day of week | Yes | 0-6 or SUN-SAT | * / , - ? | |||
Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun", | |||
and "sun" are equally accepted. | |||
Special Characters | |||
Asterisk ( * ) | |||
The asterisk indicates that the cron expression will match for all values of the | |||
field; e.g., using an asterisk in the 5th field (month) would indicate every | |||
month. | |||
Slash ( / ) | |||
Slashes are used to describe increments of ranges. For example 3-59/15 in the | |||
1st field (minutes) would indicate the 3rd minute of the hour and every 15 | |||
minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...", | |||
that is, an increment over the largest possible range of the field. The form | |||
"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the | |||
increment until the end of that specific range. It does not wrap around. | |||
Comma ( , ) | |||
Commas are used to separate items of a list. For example, using "MON,WED,FRI" in | |||
the 5th field (day of week) would mean Mondays, Wednesdays and Fridays. | |||
Hyphen ( - ) | |||
Hyphens are used to define ranges. For example, 9-17 would indicate every | |||
hour between 9am and 5pm inclusive. | |||
Question mark ( ? ) | |||
Question mark may be used instead of '*' for leaving either day-of-month or | |||
day-of-week blank. | |||
Predefined schedules | |||
You may use one of several pre-defined schedules in place of a cron expression. | |||
Entry | Description | Equivalent To | |||
----- | ----------- | ------------- | |||
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * | |||
@monthly | Run once a month, midnight, first of month | 0 0 0 1 * * | |||
@weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 | |||
@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * | |||
@hourly | Run once an hour, beginning of hour | 0 0 * * * * | |||
Intervals | |||
You may also schedule a job to execute at fixed intervals. This is supported by | |||
formatting the cron spec like this: | |||
@every <duration> | |||
where "duration" is a string accepted by time.ParseDuration | |||
(http://golang.org/pkg/time/#ParseDuration). | |||
For example, "@every 1h30m10s" would indicate a schedule that activates every | |||
1 hour, 30 minutes, 10 seconds. | |||
Note: The interval does not take the job runtime into account. For example, | |||
if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes, | |||
it will have only 2 minutes of idle time between each run. | |||
Time zones | |||
All interpretation and scheduling is done in the machine's local time zone (as | |||
provided by the Go time package (http://www.golang.org/pkg/time). | |||
Be aware that jobs scheduled during daylight-savings leap-ahead transitions will | |||
not be run! | |||
Thread safety | |||
Since the Cron service runs concurrently with the calling code, some amount of | |||
care must be taken to ensure proper synchronization. | |||
All cron methods are designed to be correctly synchronized as long as the caller | |||
ensures that invocations have a clear happens-before ordering between them. | |||
Implementation | |||
Cron entries are stored in an array, sorted by their next activation time. Cron | |||
sleeps until the next job is due to be run. | |||
Upon waking: | |||
- it runs each entry that is active on that second | |||
- it calculates the next run times for the jobs that were run | |||
- it re-sorts the array of entries by next activation time. | |||
- it goes to sleep until the soonest job. | |||
*/ | |||
package cron |
@@ -1,231 +0,0 @@ | |||
package cron | |||
import ( | |||
"fmt" | |||
"log" | |||
"math" | |||
"strconv" | |||
"strings" | |||
"time" | |||
) | |||
// Parse returns a new crontab schedule representing the given spec. | |||
// It returns a descriptive error if the spec is not valid. | |||
// | |||
// It accepts | |||
// - Full crontab specs, e.g. "* * * * * ?" | |||
// - Descriptors, e.g. "@midnight", "@every 1h30m" | |||
func Parse(spec string) (_ Schedule, err error) { | |||
// Convert panics into errors | |||
defer func() { | |||
if recovered := recover(); recovered != nil { | |||
err = fmt.Errorf("%v", recovered) | |||
} | |||
}() | |||
if spec[0] == '@' { | |||
return parseDescriptor(spec), nil | |||
} | |||
// Split on whitespace. We require 5 or 6 fields. | |||
// (second) (minute) (hour) (day of month) (month) (day of week, optional) | |||
fields := strings.Fields(spec) | |||
if len(fields) != 5 && len(fields) != 6 { | |||
log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec) | |||
} | |||
// If a sixth field is not provided (DayOfWeek), then it is equivalent to star. | |||
if len(fields) == 5 { | |||
fields = append(fields, "*") | |||
} | |||
schedule := &SpecSchedule{ | |||
Second: getField(fields[0], seconds), | |||
Minute: getField(fields[1], minutes), | |||
Hour: getField(fields[2], hours), | |||
Dom: getField(fields[3], dom), | |||
Month: getField(fields[4], months), | |||
Dow: getField(fields[5], dow), | |||
} | |||
return schedule, nil | |||
} | |||
// getField returns an Int with the bits set representing all of the times that | |||
// the field represents. A "field" is a comma-separated list of "ranges". | |||
func getField(field string, r bounds) uint64 { | |||
// list = range {"," range} | |||
var bits uint64 | |||
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' }) | |||
for _, expr := range ranges { | |||
bits |= getRange(expr, r) | |||
} | |||
return bits | |||
} | |||
// getRange returns the bits indicated by the given expression: | |||
// number | number "-" number [ "/" number ] | |||
func getRange(expr string, r bounds) uint64 { | |||
var ( | |||
start, end, step uint | |||
rangeAndStep = strings.Split(expr, "/") | |||
lowAndHigh = strings.Split(rangeAndStep[0], "-") | |||
singleDigit = len(lowAndHigh) == 1 | |||
) | |||
var extra_star uint64 | |||
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { | |||
start = r.min | |||
end = r.max | |||
extra_star = starBit | |||
} else { | |||
start = parseIntOrName(lowAndHigh[0], r.names) | |||
switch len(lowAndHigh) { | |||
case 1: | |||
end = start | |||
case 2: | |||
end = parseIntOrName(lowAndHigh[1], r.names) | |||
default: | |||
log.Panicf("Too many hyphens: %s", expr) | |||
} | |||
} | |||
switch len(rangeAndStep) { | |||
case 1: | |||
step = 1 | |||
case 2: | |||
step = mustParseInt(rangeAndStep[1]) | |||
// Special handling: "N/step" means "N-max/step". | |||
if singleDigit { | |||
end = r.max | |||
} | |||
default: | |||
log.Panicf("Too many slashes: %s", expr) | |||
} | |||
if start < r.min { | |||
log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr) | |||
} | |||
if end > r.max { | |||
log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr) | |||
} | |||
if start > end { | |||
log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr) | |||
} | |||
return getBits(start, end, step) | extra_star | |||
} | |||
// parseIntOrName returns the (possibly-named) integer contained in expr. | |||
func parseIntOrName(expr string, names map[string]uint) uint { | |||
if names != nil { | |||
if namedInt, ok := names[strings.ToLower(expr)]; ok { | |||
return namedInt | |||
} | |||
} | |||
return mustParseInt(expr) | |||
} | |||
// mustParseInt parses the given expression as an int or panics. | |||
func mustParseInt(expr string) uint { | |||
num, err := strconv.Atoi(expr) | |||
if err != nil { | |||
log.Panicf("Failed to parse int from %s: %s", expr, err) | |||
} | |||
if num < 0 { | |||
log.Panicf("Negative number (%d) not allowed: %s", num, expr) | |||
} | |||
return uint(num) | |||
} | |||
// getBits sets all bits in the range [min, max], modulo the given step size. | |||
func getBits(min, max, step uint) uint64 { | |||
var bits uint64 | |||
// If step is 1, use shifts. | |||
if step == 1 { | |||
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min) | |||
} | |||
// Else, use a simple loop. | |||
for i := min; i <= max; i += step { | |||
bits |= 1 << i | |||
} | |||
return bits | |||
} | |||
// all returns all bits within the given bounds. (plus the star bit) | |||
func all(r bounds) uint64 { | |||
return getBits(r.min, r.max, 1) | starBit | |||
} | |||
// parseDescriptor returns a pre-defined schedule for the expression, or panics | |||
// if none matches. | |||
func parseDescriptor(spec string) Schedule { | |||
switch spec { | |||
case "@yearly", "@annually": | |||
return &SpecSchedule{ | |||
Second: 1 << seconds.min, | |||
Minute: 1 << minutes.min, | |||
Hour: 1 << hours.min, | |||
Dom: 1 << dom.min, | |||
Month: 1 << months.min, | |||
Dow: all(dow), | |||
} | |||
case "@monthly": | |||
return &SpecSchedule{ | |||
Second: 1 << seconds.min, | |||
Minute: 1 << minutes.min, | |||
Hour: 1 << hours.min, | |||
Dom: 1 << dom.min, | |||
Month: all(months), | |||
Dow: all(dow), | |||
} | |||
case "@weekly": | |||
return &SpecSchedule{ | |||
Second: 1 << seconds.min, | |||
Minute: 1 << minutes.min, | |||
Hour: 1 << hours.min, | |||
Dom: all(dom), | |||
Month: all(months), | |||
Dow: 1 << dow.min, | |||
} | |||
case "@daily", "@midnight": | |||
return &SpecSchedule{ | |||
Second: 1 << seconds.min, | |||
Minute: 1 << minutes.min, | |||
Hour: 1 << hours.min, | |||
Dom: all(dom), | |||
Month: all(months), | |||
Dow: all(dow), | |||
} | |||
case "@hourly": | |||
return &SpecSchedule{ | |||
Second: 1 << seconds.min, | |||
Minute: 1 << minutes.min, | |||
Hour: all(hours), | |||
Dom: all(dom), | |||
Month: all(months), | |||
Dow: all(dow), | |||
} | |||
} | |||
const every = "@every " | |||
if strings.HasPrefix(spec, every) { | |||
duration, err := time.ParseDuration(spec[len(every):]) | |||
if err != nil { | |||
log.Panicf("Failed to parse duration %s: %s", spec, err) | |||
} | |||
return Every(duration) | |||
} | |||
log.Panicf("Unrecognized descriptor: %s", spec) | |||
return nil | |||
} |
@@ -1,117 +0,0 @@ | |||
package cron | |||
import ( | |||
"reflect" | |||
"testing" | |||
"time" | |||
) | |||
func TestRange(t *testing.T) { | |||
ranges := []struct { | |||
expr string | |||
min, max uint | |||
expected uint64 | |||
}{ | |||
{"5", 0, 7, 1 << 5}, | |||
{"0", 0, 7, 1 << 0}, | |||
{"7", 0, 7, 1 << 7}, | |||
{"5-5", 0, 7, 1 << 5}, | |||
{"5-6", 0, 7, 1<<5 | 1<<6}, | |||
{"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7}, | |||
{"5-6/2", 0, 7, 1 << 5}, | |||
{"5-7/2", 0, 7, 1<<5 | 1<<7}, | |||
{"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7}, | |||
{"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit}, | |||
{"*/2", 1, 3, 1<<1 | 1<<3 | starBit}, | |||
} | |||
for _, c := range ranges { | |||
actual := getRange(c.expr, bounds{c.min, c.max, nil}) | |||
if actual != c.expected { | |||
t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual) | |||
} | |||
} | |||
} | |||
func TestField(t *testing.T) { | |||
fields := []struct { | |||
expr string | |||
min, max uint | |||
expected uint64 | |||
}{ | |||
{"5", 1, 7, 1 << 5}, | |||
{"5,6", 1, 7, 1<<5 | 1<<6}, | |||
{"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7}, | |||
{"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3}, | |||
} | |||
for _, c := range fields { | |||
actual := getField(c.expr, bounds{c.min, c.max, nil}) | |||
if actual != c.expected { | |||
t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual) | |||
} | |||
} | |||
} | |||
func TestBits(t *testing.T) { | |||
allBits := []struct { | |||
r bounds | |||
expected uint64 | |||
}{ | |||
{minutes, 0xfffffffffffffff}, // 0-59: 60 ones | |||
{hours, 0xffffff}, // 0-23: 24 ones | |||
{dom, 0xfffffffe}, // 1-31: 31 ones, 1 zero | |||
{months, 0x1ffe}, // 1-12: 12 ones, 1 zero | |||
{dow, 0x7f}, // 0-6: 7 ones | |||
} | |||
for _, c := range allBits { | |||
actual := all(c.r) // all() adds the starBit, so compensate for that.. | |||
if c.expected|starBit != actual { | |||
t.Errorf("%d-%d/%d => (expected) %b != %b (actual)", | |||
c.r.min, c.r.max, 1, c.expected|starBit, actual) | |||
} | |||
} | |||
bits := []struct { | |||
min, max, step uint | |||
expected uint64 | |||
}{ | |||
{0, 0, 1, 0x1}, | |||
{1, 1, 1, 0x2}, | |||
{1, 5, 2, 0x2a}, // 101010 | |||
{1, 4, 2, 0xa}, // 1010 | |||
} | |||
for _, c := range bits { | |||
actual := getBits(c.min, c.max, c.step) | |||
if c.expected != actual { | |||
t.Errorf("%d-%d/%d => (expected) %b != %b (actual)", | |||
c.min, c.max, c.step, c.expected, actual) | |||
} | |||
} | |||
} | |||
func TestSpecSchedule(t *testing.T) { | |||
entries := []struct { | |||
expr string | |||
expected Schedule | |||
}{ | |||
{"* 5 * * * *", &SpecSchedule{all(seconds), 1 << 5, all(hours), all(dom), all(months), all(dow)}}, | |||
{"@every 5m", ConstantDelaySchedule{time.Duration(5) * time.Minute}}, | |||
} | |||
for _, c := range entries { | |||
actual, err := Parse(c.expr) | |||
if err != nil { | |||
t.Error(err) | |||
} | |||
if !reflect.DeepEqual(actual, c.expected) { | |||
t.Errorf("%s => (expected) %v != %v (actual)", c.expr, c.expected, actual) | |||
} | |||
} | |||
} |
@@ -1,161 +0,0 @@ | |||
package cron | |||
import ( | |||
"time" | |||
) | |||
// SpecSchedule specifies a duty cycle (to the second granularity), based on a | |||
// traditional crontab specification. It is computed initially and stored as bit sets. | |||
type SpecSchedule struct { | |||
Second, Minute, Hour, Dom, Month, Dow uint64 | |||
} | |||
// bounds provides a range of acceptable values (plus a map of name to value). | |||
type bounds struct { | |||
min, max uint | |||
names map[string]uint | |||
} | |||
// The bounds for each field. | |||
var ( | |||
seconds = bounds{0, 59, nil} | |||
minutes = bounds{0, 59, nil} | |||
hours = bounds{0, 23, nil} | |||
dom = bounds{1, 31, nil} | |||
months = bounds{1, 12, map[string]uint{ | |||
"jan": 1, | |||
"feb": 2, | |||
"mar": 3, | |||
"apr": 4, | |||
"may": 5, | |||
"jun": 6, | |||
"jul": 7, | |||
"aug": 8, | |||
"sep": 9, | |||
"oct": 10, | |||
"nov": 11, | |||
"dec": 12, | |||
}} | |||
dow = bounds{0, 6, map[string]uint{ | |||
"sun": 0, | |||
"mon": 1, | |||
"tue": 2, | |||
"wed": 3, | |||
"thu": 4, | |||
"fri": 5, | |||
"sat": 6, | |||
}} | |||
) | |||
const ( | |||
// Set the top bit if a star was included in the expression. | |||
starBit = 1 << 63 | |||
) | |||
// Next returns the next time this schedule is activated, greater than the given | |||
// time. If no time can be found to satisfy the schedule, return the zero time. | |||
func (s *SpecSchedule) Next(t time.Time) time.Time { | |||
// General approach: | |||
// For Month, Day, Hour, Minute, Second: | |||
// Check if the time value matches. If yes, continue to the next field. | |||
// If the field doesn't match the schedule, then increment the field until it matches. | |||
// While incrementing the field, a wrap-around brings it back to the beginning | |||
// of the field list (since it is necessary to re-verify previous field | |||
// values) | |||
// Start at the earliest possible time (the upcoming second). | |||
t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond) | |||
// This flag indicates whether a field has been incremented. | |||
added := false | |||
// If no time is found within five years, return zero. | |||
yearLimit := t.Year() + 5 | |||
WRAP: | |||
if t.Year() > yearLimit { | |||
return time.Time{} | |||
} | |||
// Find the first applicable month. | |||
// If it's this month, then do nothing. | |||
for 1<<uint(t.Month())&s.Month == 0 { | |||
// If we have to add a month, reset the other parts to 0. | |||
if !added { | |||
added = true | |||
// Otherwise, set the date at the beginning (since the current time is irrelevant). | |||
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()) | |||
} | |||
t = t.AddDate(0, 1, 0) | |||
// Wrapped around. | |||
if t.Month() == time.January { | |||
goto WRAP | |||
} | |||
} | |||
// Now get a day in that month. | |||
for !dayMatches(s, t) { | |||
if !added { | |||
added = true | |||
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) | |||
} | |||
t = t.AddDate(0, 0, 1) | |||
if t.Day() == 1 { | |||
goto WRAP | |||
} | |||
} | |||
for 1<<uint(t.Hour())&s.Hour == 0 { | |||
if !added { | |||
added = true | |||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location()) | |||
} | |||
t = t.Add(1 * time.Hour) | |||
if t.Hour() == 0 { | |||
goto WRAP | |||
} | |||
} | |||
for 1<<uint(t.Minute())&s.Minute == 0 { | |||
if !added { | |||
added = true | |||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, t.Location()) | |||
} | |||
t = t.Add(1 * time.Minute) | |||
if t.Minute() == 0 { | |||
goto WRAP | |||
} | |||
} | |||
for 1<<uint(t.Second())&s.Second == 0 { | |||
if !added { | |||
added = true | |||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, t.Location()) | |||
} | |||
t = t.Add(1 * time.Second) | |||
if t.Second() == 0 { | |||
goto WRAP | |||
} | |||
} | |||
return t | |||
} | |||
// dayMatches returns true if the schedule's day-of-week and day-of-month | |||
// restrictions are satisfied by the given time. | |||
func dayMatches(s *SpecSchedule, t time.Time) bool { | |||
var ( | |||
domMatch bool = 1<<uint(t.Day())&s.Dom > 0 | |||
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0 | |||
) | |||
if s.Dom&starBit > 0 || s.Dow&starBit > 0 { | |||
return domMatch && dowMatch | |||
} | |||
return domMatch || dowMatch | |||
} |
@@ -1,173 +0,0 @@ | |||
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}, | |||
{"Sun Jul 15 00:00 2012", "0 * * */2 * Sun", true}, | |||
} | |||
for _, test := range tests { | |||
sched, err := Parse(test.spec) | |||
if err != nil { | |||
t.Error(err) | |||
continue | |||
} | |||
actual := sched.Next(getTime(test.time).Add(-1 * time.Second)) | |||
expected := getTime(test.time) | |||
if test.expected && expected != actual || !test.expected && expected == actual { | |||
t.Errorf("Fail evaluating %s on %s: (expected) %s != %s (actual)", | |||
test.spec, test.time, expected, actual) | |||
} | |||
} | |||
} | |||
func TestNext(t *testing.T) { | |||
runs := []struct { | |||
time, spec string | |||
expected string | |||
}{ | |||
// Simple cases | |||
{"Mon Jul 9 14:45 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, | |||
{"Mon Jul 9 14:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, | |||
{"Mon Jul 9 14:59:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, | |||
// Wrap around hours | |||
{"Mon Jul 9 15:45 2012", "0 20-35/15 * * *", "Mon Jul 9 16:20 2012"}, | |||
// Wrap around days | |||
{"Mon Jul 9 23:46 2012", "0 */15 * * *", "Tue Jul 10 00:00 2012"}, | |||
{"Mon Jul 9 23:45 2012", "0 20-35/15 * * *", "Tue Jul 10 00:20 2012"}, | |||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * * *", "Tue Jul 10 00:20:15 2012"}, | |||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 * *", "Tue Jul 10 01:20:15 2012"}, | |||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 10-12 * *", "Tue Jul 10 10:20:15 2012"}, | |||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 */2 * *", "Thu Jul 11 01:20:15 2012"}, | |||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 * *", "Wed Jul 10 00:20:15 2012"}, | |||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 Jul *", "Wed Jul 10 00:20:15 2012"}, | |||
// Wrap around months | |||
{"Mon Jul 9 23:35 2012", "0 0 0 9 Apr-Oct ?", "Thu Aug 9 00:00 2012"}, | |||
{"Mon Jul 9 23:35 2012", "0 0 0 */5 Apr,Aug,Oct Mon", "Mon Aug 6 00:00 2012"}, | |||
{"Mon Jul 9 23:35 2012", "0 0 0 */5 Oct Mon", "Mon Oct 1 00:00 2012"}, | |||
// Wrap around years | |||
{"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon", "Mon Feb 4 00:00 2013"}, | |||
{"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon/2", "Fri Feb 1 00:00 2013"}, | |||
// Wrap around minute, hour, day, month, and year | |||
{"Mon Dec 31 23:59:45 2012", "0 * * * * *", "Tue Jan 1 00:00:00 2013"}, | |||
// Leap year | |||
{"Mon Jul 9 23:35 2012", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2016"}, | |||
// Daylight savings time EST -> EDT | |||
{"2012-03-11T00:00:00-0500", "0 30 2 11 Mar ?", "2013-03-11T02:30:00-0400"}, | |||
// Daylight savings time EDT -> EST | |||
{"2012-11-04T00:00:00-0400", "0 30 2 04 Nov ?", "2012-11-04T02:30:00-0500"}, | |||
{"2012-11-04T01:45:00-0400", "0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"}, | |||
// Unsatisfiable | |||
{"Mon Jul 9 23:35 2012", "0 0 0 30 Feb ?", ""}, | |||
{"Mon Jul 9 23:35 2012", "0 0 0 31 Apr ?", ""}, | |||
} | |||
for _, c := range runs { | |||
sched, err := Parse(c.spec) | |||
if err != nil { | |||
t.Error(err) | |||
continue | |||
} | |||
actual := sched.Next(getTime(c.time)) | |||
expected := getTime(c.expected) | |||
if !actual.Equal(expected) { | |||
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual) | |||
} | |||
} | |||
} | |||
func TestErrors(t *testing.T) { | |||
invalidSpecs := []string{ | |||
"xyz", | |||
"60 0 * * *", | |||
"0 60 * * *", | |||
"0 0 * * XYZ", | |||
} | |||
for _, spec := range invalidSpecs { | |||
_, err := Parse(spec) | |||
if err == nil { | |||
t.Error("expected an error parsing: ", spec) | |||
} | |||
} | |||
} | |||
func getTime(value string) time.Time { | |||
if value == "" { | |||
return time.Time{} | |||
} | |||
t, err := time.Parse("Mon Jan 2 15:04 2006", value) | |||
if err != nil { | |||
t, err = time.Parse("Mon Jan 2 15:04:05 2006", value) | |||
if err != nil { | |||
t, err = time.Parse("2006-01-02T15:04:05-0700", value) | |||
if err != nil { | |||
panic(err) | |||
} | |||
// Daylight savings time tests require location | |||
if ny, err := time.LoadLocation("America/New_York"); err == nil { | |||
t = t.In(ny) | |||
} | |||
} | |||
} | |||
return t | |||
} |
@@ -14,8 +14,8 @@ import ( | |||
"gopkg.in/macaron.v1" | |||
"github.com/gogits/gogs/models" | |||
"github.com/gogits/gogs/models/cron" | |||
"github.com/gogits/gogs/modules/base" | |||
"github.com/gogits/gogs/modules/cron" | |||
"github.com/gogits/gogs/modules/middleware" | |||
"github.com/gogits/gogs/modules/process" | |||
"github.com/gogits/gogs/modules/setting" | |||
@@ -20,9 +20,9 @@ import ( | |||
"github.com/gogits/git-module" | |||
"github.com/gogits/gogs/models" | |||
"github.com/gogits/gogs/models/cron" | |||
"github.com/gogits/gogs/modules/auth" | |||
"github.com/gogits/gogs/modules/base" | |||
"github.com/gogits/gogs/modules/cron" | |||
"github.com/gogits/gogs/modules/log" | |||
"github.com/gogits/gogs/modules/mailer" | |||
"github.com/gogits/gogs/modules/middleware" | |||