├── .gitignore ├── constantdelay.go ├── LICENSE ├── constantdelay_test.go ├── parser_test.go ├── spec.go ├── README.md ├── spec_test.go ├── parser.go ├── cron.go └── cron_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /constantdelay.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import "time" 4 | 5 | // ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes". 6 | // It does not support jobs more frequent than once a second. 7 | type ConstantDelaySchedule struct { 8 | Delay time.Duration 9 | } 10 | 11 | // Every returns a crontab Schedule that activates once every duration. 12 | // Delays of less than a second are not supported (will panic). 13 | // Any fields less than a Second are truncated. 14 | func Every(duration time.Duration) ConstantDelaySchedule { 15 | if duration < time.Second { 16 | panic("cron/constantdelay: delays of less than a second are not supported: " + 17 | duration.String()) 18 | } 19 | return ConstantDelaySchedule{ 20 | Delay: duration - time.Duration(duration.Nanoseconds())%time.Second, 21 | } 22 | } 23 | 24 | // Next returns the next time this should be run. 25 | // This rounds so that the next activation time will be on the second. 26 | func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time { 27 | return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond) 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Rob Figueiredo 2 | All Rights Reserved. 3 | 4 | MIT LICENSE 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /constantdelay_test.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestConstantDelayNext(t *testing.T) { 9 | tests := []struct { 10 | time string 11 | delay time.Duration 12 | expected string 13 | }{ 14 | // Simple cases 15 | {"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, 16 | {"Mon Jul 9 14:59 2012", 15 * time.Minute, "Mon Jul 9 15:14 2012"}, 17 | {"Mon Jul 9 14:59:59 2012", 15 * time.Minute, "Mon Jul 9 15:14:59 2012"}, 18 | 19 | // Wrap around hours 20 | {"Mon Jul 9 15:45 2012", 35 * time.Minute, "Mon Jul 9 16:20 2012"}, 21 | 22 | // Wrap around days 23 | {"Mon Jul 9 23:46 2012", 14 * time.Minute, "Tue Jul 10 00:00 2012"}, 24 | {"Mon Jul 9 23:45 2012", 35 * time.Minute, "Tue Jul 10 00:20 2012"}, 25 | {"Mon Jul 9 23:35:51 2012", 44*time.Minute + 24*time.Second, "Tue Jul 10 00:20:15 2012"}, 26 | {"Mon Jul 9 23:35:51 2012", 25*time.Hour + 44*time.Minute + 24*time.Second, "Thu Jul 11 01:20:15 2012"}, 27 | 28 | // Wrap around months 29 | {"Mon Jul 9 23:35 2012", 91*24*time.Hour + 25*time.Minute, "Thu Oct 9 00:00 2012"}, 30 | 31 | // Wrap around minute, hour, day, month, and year 32 | {"Mon Dec 31 23:59:45 2012", 15 * time.Second, "Tue Jan 1 00:00:00 2013"}, 33 | 34 | // Round to nearest second on the delay 35 | {"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, 36 | 37 | // Round to nearest second when calculating the next time. 38 | {"Mon Jul 9 14:45:00.005 2012", 15 * time.Minute, "Mon Jul 9 15:00 2012"}, 39 | 40 | // Round to nearest second for both. 41 | {"Mon Jul 9 14:45:00.005 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, 42 | } 43 | 44 | for _, c := range tests { 45 | actual := Every(c.delay).Next(getTime(c.time)) 46 | expected := getTime(c.expected) 47 | if actual != expected { 48 | t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.delay, expected, actual) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestRange(t *testing.T) { 10 | ranges := []struct { 11 | expr string 12 | min, max uint 13 | expected uint64 14 | }{ 15 | {"5", 0, 7, 1 << 5}, 16 | {"0", 0, 7, 1 << 0}, 17 | {"7", 0, 7, 1 << 7}, 18 | 19 | {"5-5", 0, 7, 1 << 5}, 20 | {"5-6", 0, 7, 1<<5 | 1<<6}, 21 | {"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7}, 22 | 23 | {"5-6/2", 0, 7, 1 << 5}, 24 | {"5-7/2", 0, 7, 1<<5 | 1<<7}, 25 | {"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7}, 26 | 27 | {"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit}, 28 | {"*/2", 1, 3, 1<<1 | 1<<3 | starBit}, 29 | } 30 | 31 | for _, c := range ranges { 32 | actual := getRange(c.expr, bounds{c.min, c.max, nil}) 33 | if actual != c.expected { 34 | t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual) 35 | } 36 | } 37 | } 38 | 39 | func TestField(t *testing.T) { 40 | fields := []struct { 41 | expr string 42 | min, max uint 43 | expected uint64 44 | }{ 45 | {"5", 1, 7, 1 << 5}, 46 | {"5,6", 1, 7, 1<<5 | 1<<6}, 47 | {"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7}, 48 | {"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3}, 49 | } 50 | 51 | for _, c := range fields { 52 | actual := getField(c.expr, bounds{c.min, c.max, nil}) 53 | if actual != c.expected { 54 | t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual) 55 | } 56 | } 57 | } 58 | 59 | func TestBits(t *testing.T) { 60 | allBits := []struct { 61 | r bounds 62 | expected uint64 63 | }{ 64 | {minutes, 0xfffffffffffffff}, // 0-59: 60 ones 65 | {hours, 0xffffff}, // 0-23: 24 ones 66 | {dom, 0xfffffffe}, // 1-31: 31 ones, 1 zero 67 | {months, 0x1ffe}, // 1-12: 12 ones, 1 zero 68 | {dow, 0x7f}, // 0-6: 7 ones 69 | } 70 | 71 | for _, c := range allBits { 72 | actual := all(c.r) // all() adds the starBit, so compensate for that.. 73 | if c.expected|starBit != actual { 74 | t.Errorf("%d-%d/%d => (expected) %b != %b (actual)", 75 | c.r.min, c.r.max, 1, c.expected|starBit, actual) 76 | } 77 | } 78 | 79 | bits := []struct { 80 | min, max, step uint 81 | expected uint64 82 | }{ 83 | 84 | {0, 0, 1, 0x1}, 85 | {1, 1, 1, 0x2}, 86 | {1, 5, 2, 0x2a}, // 101010 87 | {1, 4, 2, 0xa}, // 1010 88 | } 89 | 90 | for _, c := range bits { 91 | actual := getBits(c.min, c.max, c.step) 92 | if c.expected != actual { 93 | t.Errorf("%d-%d/%d => (expected) %b != %b (actual)", 94 | c.min, c.max, c.step, c.expected, actual) 95 | } 96 | } 97 | } 98 | 99 | func TestSpecSchedule(t *testing.T) { 100 | entries := []struct { 101 | expr string 102 | expected Schedule 103 | }{ 104 | {"* 5 * * * *", &SpecSchedule{all(seconds), 1 << 5, all(hours), all(dom), all(months), all(dow)}}, 105 | {"@every 5m", ConstantDelaySchedule{time.Duration(5) * time.Minute}}, 106 | } 107 | 108 | for _, c := range entries { 109 | actual := Parse(c.expr) 110 | if !reflect.DeepEqual(actual, c.expected) { 111 | t.Errorf("%s => (expected) %b != %b (actual)", c.expr, c.expected, actual) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /spec.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // SpecSchedule specifies a duty cycle (to the second granularity), based on a 8 | // traditional crontab specification. It is computed initially and stored as bit sets. 9 | type SpecSchedule struct { 10 | Second, Minute, Hour, Dom, Month, Dow uint64 11 | } 12 | 13 | // bounds provides a range of acceptable values (plus a map of name to value). 14 | type bounds struct { 15 | min, max uint 16 | names map[string]uint 17 | } 18 | 19 | // The bounds for each field. 20 | var ( 21 | seconds = bounds{0, 59, nil} 22 | minutes = bounds{0, 59, nil} 23 | hours = bounds{0, 23, nil} 24 | dom = bounds{1, 31, nil} 25 | months = bounds{1, 12, map[string]uint{ 26 | "jan": 1, 27 | "feb": 2, 28 | "mar": 3, 29 | "apr": 4, 30 | "may": 5, 31 | "jun": 6, 32 | "jul": 7, 33 | "aug": 8, 34 | "sep": 9, 35 | "oct": 10, 36 | "nov": 11, 37 | "dec": 12, 38 | }} 39 | dow = bounds{0, 6, map[string]uint{ 40 | "sun": 0, 41 | "mon": 1, 42 | "tue": 2, 43 | "wed": 3, 44 | "thu": 4, 45 | "fri": 5, 46 | "sat": 6, 47 | }} 48 | ) 49 | 50 | const ( 51 | // Set the top bit if a star was included in the expression. 52 | starBit = 1 << 63 53 | ) 54 | 55 | // Next returns the next time this schedule is activated, greater than the given 56 | // time. If no time can be found to satisfy the schedule, return the zero time. 57 | func (s *SpecSchedule) Next(t time.Time) time.Time { 58 | // General approach: 59 | // For Month, Day, Hour, Minute, Second: 60 | // Check if the time value matches. If yes, continue to the next field. 61 | // If the field doesn't match the schedule, then increment the field until it matches. 62 | // While incrementing the field, a wrap-around brings it back to the beginning 63 | // of the field list (since it is necessary to re-verify previous field 64 | // values) 65 | 66 | // Start at the earliest possible time (the upcoming second). 67 | t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond) 68 | 69 | // This flag indicates whether a field has been incremented. 70 | added := false 71 | 72 | // If no time is found within five years, return zero. 73 | yearLimit := t.Year() + 5 74 | 75 | WRAP: 76 | if t.Year() > yearLimit { 77 | return time.Time{} 78 | } 79 | 80 | // Find the first applicable month. 81 | // If it's this month, then do nothing. 82 | for 1< 0 154 | dowMatch bool = 1< 0 155 | ) 156 | 157 | if s.Dom&starBit > 0 || s.Dow&starBit > 0 { 158 | return domMatch && dowMatch 159 | } 160 | return domMatch || dowMatch 161 | } 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cron 2 | ==== 3 | 4 | A cron library for Go. See the 5 | [godoc](http://go.pkgdoc.org/github.com/robfig/cron). 6 | 7 | ## Usage 8 | 9 | Callers may register Funcs to be invoked on a given schedule. Cron will run 10 | them in their own goroutines. A name must be provided. 11 | 12 | ```go 13 | c := cron.New() 14 | c.AddFunc("0 5 * * * *", func() { fmt.Println("Every 5 minutes") }, "Often") 15 | c.AddFunc("@hourly", func() { fmt.Println("Every hour") }, "Frequent") 16 | c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") }, "Less Frequent") 17 | c.Start() 18 | .. 19 | // Funcs are invoked in their own goroutine, asynchronously. 20 | ... 21 | // Funcs may also be added to a running Cron 22 | c.AddFunc("@daily", func() { fmt.Println("Every day") }, "My Job") 23 | .. 24 | // Inspect the cron job entries' next and previous run times. 25 | inspect(c.Entries()) 26 | .. 27 | // Remove an entry from the cron by name. 28 | c.RemoveJob("My Job") 29 | .. 30 | c.Stop() // Stop the scheduler (does not stop any jobs already running). 31 | ``` 32 | 33 | ## CRON Expression 34 | 35 | This section describes the specific format accepted by this cron. Some snippets 36 | are taken from [the wikipedia article](http://en.wikipedia.org/wiki/Cron). 37 | 38 | ### Format 39 | 40 | A cron expression represents a set of times, using 6 space-separated fields. 41 | 42 | Field name | Mandatory? | Allowed values | Allowed special characters 43 | ---------- | ---------- | -------------- | -------------------------- 44 | Seconds | Yes | 0-59 | * / , - 45 | Minutes | Yes | 0-59 | * / , - 46 | Hours | Yes | 0-23 | * / , - 47 | Day of month | Yes | 1-31 | * / , - ? 48 | Month | Yes | 1-12 or JAN-DEC | * / , - 49 | Day of week | Yes | 0-6 or SUN-SAT | * / , - ? 50 | 51 | Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun", 52 | and "sun" are equally accepted. 53 | 54 | ### Special Characters 55 | 56 | #### Asterisk ( * ) 57 | 58 | The asterisk indicates that the cron expression will match for all values of the 59 | field; e.g., using an asterisk in the 5th field (month) would indicate every 60 | month. 61 | 62 | #### Slash ( / ) 63 | 64 | Slashes are used to describe increments of ranges. For example 3-59/15 in the 65 | 1st field (minutes) would indicate the 3rd minute of the hour and every 15 66 | minutes thereafter. The form "*/..." is equivalent to the form "first-last/...", 67 | that is, an increment over the largest possible range of the field. The form 68 | "N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the 69 | increment until the end of that specific range. It does not wrap around. 70 | 71 | #### Comma ( , ) 72 | 73 | Commas are used to separate items of a list. For example, using "MON,WED,FRI" in 74 | the 5th field (day of week) would mean Mondays, Wednesdays and Fridays. 75 | 76 | #### Hyphen ( - ) 77 | 78 | Hyphens are used to define ranges. For example, 9-17 would indicate every 79 | hour between 9am and 5pm inclusive. 80 | 81 | #### Question mark ( ? ) 82 | 83 | Question mark may be used instead of '*' for leaving either day-of-month or 84 | day-of-week blank. 85 | 86 | ### Predefined schedules 87 | 88 | You may use one of several pre-defined schedules in place of a cron expression. 89 | 90 | Entry | Description | Equivalent To 91 | ----- | ----------- | ------------- 92 | @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * 93 | @monthly | Run once a month, midnight, first of month | 0 0 0 1 * * 94 | @weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 95 | @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * 96 | @hourly | Run once an hour, beginning of hour | 0 0 * * * * 97 | 98 | ## Intervals 99 | 100 | You may also schedule a job to execute at fixed intervals. This is supported by 101 | formatting the cron spec like this: 102 | 103 | @every 104 | 105 | where `` is a string accepted by 106 | [`time.ParseDuration`](http://golang.org/pkg/time/#ParseDuration). 107 | 108 | For example, `@every 1h30m10s` would indicate a schedule that activates every 109 | 1 hour, 30 minutes, 10 seconds. 110 | 111 | > Note: The interval does not take the job runtime into account. For example, 112 | > if a job takes *3 minutes* to run, and it is scheduled to run every *5 minutes*, 113 | > it will have only *2 minutes* of idle time between each run. 114 | 115 | ## Time zones 116 | 117 | All interpretation and scheduling is done in the machine's local time zone (as 118 | provided by the [Go time package](http://www.golang.org/pkg/time)). 119 | 120 | Be aware that jobs scheduled during daylight-savings leap-ahead transitions will 121 | not be run! 122 | 123 | ## Thread safety 124 | 125 | Since the Cron service runs concurrently with the calling code, some amount of 126 | care must be taken to ensure proper synchronization. 127 | 128 | All [cron methods](http://go.pkgdoc.org/github.com/robfig/cron#Cron) are 129 | designed to be correctly synchronized as long as the caller ensures that 130 | invocations have a clear happens-before ordering between them. 131 | 132 | ## Implementation 133 | 134 | Cron entries are stored in an array, sorted by their next activation time. Cron 135 | sleeps until the next job is due to be run. 136 | 137 | Upon waking: 138 | * it runs each entry that is active on that second 139 | * it calculates the next run times for the jobs that were run 140 | * it re-sorts the array of entries by next activation time. 141 | * it goes to sleep until the soonest job. 142 | -------------------------------------------------------------------------------- /spec_test.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestActivation(t *testing.T) { 9 | tests := []struct { 10 | time, spec string 11 | expected bool 12 | }{ 13 | // Every fifteen minutes. 14 | {"Mon Jul 9 15:00 2012", "0 0/15 * * *", true}, 15 | {"Mon Jul 9 15:45 2012", "0 0/15 * * *", true}, 16 | {"Mon Jul 9 15:40 2012", "0 0/15 * * *", false}, 17 | 18 | // Every fifteen minutes, starting at 5 minutes. 19 | {"Mon Jul 9 15:05 2012", "0 5/15 * * *", true}, 20 | {"Mon Jul 9 15:20 2012", "0 5/15 * * *", true}, 21 | {"Mon Jul 9 15:50 2012", "0 5/15 * * *", true}, 22 | 23 | // Named months 24 | {"Sun Jul 15 15:00 2012", "0 0/15 * * Jul", true}, 25 | {"Sun Jul 15 15:00 2012", "0 0/15 * * Jun", false}, 26 | 27 | // Everything set. 28 | {"Sun Jul 15 08:30 2012", "0 30 08 ? Jul Sun", true}, 29 | {"Sun Jul 15 08:30 2012", "0 30 08 15 Jul ?", true}, 30 | {"Mon Jul 16 08:30 2012", "0 30 08 ? Jul Sun", false}, 31 | {"Mon Jul 16 08:30 2012", "0 30 08 15 Jul ?", false}, 32 | 33 | // Predefined schedules 34 | {"Mon Jul 9 15:00 2012", "@hourly", true}, 35 | {"Mon Jul 9 15:04 2012", "@hourly", false}, 36 | {"Mon Jul 9 15:00 2012", "@daily", false}, 37 | {"Mon Jul 9 00:00 2012", "@daily", true}, 38 | {"Mon Jul 9 00:00 2012", "@weekly", false}, 39 | {"Sun Jul 8 00:00 2012", "@weekly", true}, 40 | {"Sun Jul 8 01:00 2012", "@weekly", false}, 41 | {"Sun Jul 8 00:00 2012", "@monthly", false}, 42 | {"Sun Jul 1 00:00 2012", "@monthly", true}, 43 | 44 | // Test interaction of DOW and DOM. 45 | // If both are specified, then only one needs to match. 46 | {"Sun Jul 15 00:00 2012", "0 * * 1,15 * Sun", true}, 47 | {"Fri Jun 15 00:00 2012", "0 * * 1,15 * Sun", true}, 48 | {"Wed Aug 1 00:00 2012", "0 * * 1,15 * Sun", true}, 49 | 50 | // However, if one has a star, then both need to match. 51 | {"Sun Jul 15 00:00 2012", "0 * * * * Mon", false}, 52 | {"Sun Jul 15 00:00 2012", "0 * * */10 * Sun", false}, 53 | {"Mon Jul 9 00:00 2012", "0 * * 1,15 * *", false}, 54 | {"Sun Jul 15 00:00 2012", "0 * * 1,15 * *", true}, 55 | {"Sun Jul 15 00:00 2012", "0 * * */2 * Sun", true}, 56 | } 57 | 58 | for _, test := range tests { 59 | actual := Parse(test.spec).Next(getTime(test.time).Add(-1 * time.Second)) 60 | expected := getTime(test.time) 61 | if test.expected && expected != actual || !test.expected && expected == actual { 62 | t.Errorf("Fail evaluating %s on %s: (expected) %s != %s (actual)", 63 | test.spec, test.time, expected, actual) 64 | } 65 | } 66 | } 67 | 68 | func TestNext(t *testing.T) { 69 | runs := []struct { 70 | time, spec string 71 | expected string 72 | }{ 73 | // Simple cases 74 | {"Mon Jul 9 14:45 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, 75 | {"Mon Jul 9 14:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, 76 | {"Mon Jul 9 14:59:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, 77 | 78 | // Wrap around hours 79 | {"Mon Jul 9 15:45 2012", "0 20-35/15 * * *", "Mon Jul 9 16:20 2012"}, 80 | 81 | // Wrap around days 82 | {"Mon Jul 9 23:46 2012", "0 */15 * * *", "Tue Jul 10 00:00 2012"}, 83 | {"Mon Jul 9 23:45 2012", "0 20-35/15 * * *", "Tue Jul 10 00:20 2012"}, 84 | {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * * *", "Tue Jul 10 00:20:15 2012"}, 85 | {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 * *", "Tue Jul 10 01:20:15 2012"}, 86 | {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 10-12 * *", "Tue Jul 10 10:20:15 2012"}, 87 | 88 | {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 */2 * *", "Thu Jul 11 01:20:15 2012"}, 89 | {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 * *", "Wed Jul 10 00:20:15 2012"}, 90 | {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 Jul *", "Wed Jul 10 00:20:15 2012"}, 91 | 92 | // Wrap around months 93 | {"Mon Jul 9 23:35 2012", "0 0 0 9 Apr-Oct ?", "Thu Aug 9 00:00 2012"}, 94 | {"Mon Jul 9 23:35 2012", "0 0 0 */5 Apr,Aug,Oct Mon", "Mon Aug 6 00:00 2012"}, 95 | {"Mon Jul 9 23:35 2012", "0 0 0 */5 Oct Mon", "Mon Oct 1 00:00 2012"}, 96 | 97 | // Wrap around years 98 | {"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon", "Mon Feb 4 00:00 2013"}, 99 | {"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon/2", "Fri Feb 1 00:00 2013"}, 100 | 101 | // Wrap around minute, hour, day, month, and year 102 | {"Mon Dec 31 23:59:45 2012", "0 * * * * *", "Tue Jan 1 00:00:00 2013"}, 103 | 104 | // Leap year 105 | {"Mon Jul 9 23:35 2012", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2016"}, 106 | 107 | // Daylight savings time EST -> EDT 108 | {"2012-03-11T00:00:00-0500", "0 30 2 11 Mar ?", "2013-03-11T02:30:00-0400"}, 109 | 110 | // Daylight savings time EDT -> EST 111 | {"2012-11-04T00:00:00-0400", "0 30 2 04 Nov ?", "2012-11-04T02:30:00-0500"}, 112 | {"2012-11-04T01:45:00-0400", "0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"}, 113 | 114 | // Unsatisfiable 115 | {"Mon Jul 9 23:35 2012", "0 0 0 30 Feb ?", ""}, 116 | {"Mon Jul 9 23:35 2012", "0 0 0 31 Apr ?", ""}, 117 | } 118 | 119 | for _, c := range runs { 120 | actual := Parse(c.spec).Next(getTime(c.time)) 121 | expected := getTime(c.expected) 122 | if !actual.Equal(expected) { 123 | t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual) 124 | } 125 | } 126 | } 127 | 128 | func getTime(value string) time.Time { 129 | if value == "" { 130 | return time.Time{} 131 | } 132 | t, err := time.Parse("Mon Jan 2 15:04 2006", value) 133 | if err != nil { 134 | t, err = time.Parse("Mon Jan 2 15:04:05 2006", value) 135 | if err != nil { 136 | t, err = time.Parse("2006-01-02T15:04:05-0700", value) 137 | if err != nil { 138 | panic(err) 139 | } 140 | // Daylight savings time tests require location 141 | if ny, err := time.LoadLocation("America/New_York"); err == nil { 142 | t = t.In(ny) 143 | } 144 | } 145 | } 146 | 147 | return t 148 | } 149 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "log" 5 | "math" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Parse returns a new crontab schedule representing the given spec. 12 | // It panics with a descriptive error if the spec is not valid. 13 | // 14 | // It accepts 15 | // - Full crontab specs, e.g. "* * * * * ?" 16 | // - Descriptors, e.g. "@midnight", "@every 1h30m" 17 | func Parse(spec string) Schedule { 18 | if spec[0] == '@' { 19 | return parseDescriptor(spec) 20 | } 21 | 22 | // Split on whitespace. We require 5 or 6 fields. 23 | // (second) (minute) (hour) (day of month) (month) (day of week, optional) 24 | fields := strings.Fields(spec) 25 | if len(fields) != 5 && len(fields) != 6 { 26 | log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec) 27 | } 28 | 29 | // If a sixth field is not provided (DayOfWeek), then it is equivalent to star. 30 | if len(fields) == 5 { 31 | fields = append(fields, "*") 32 | } 33 | 34 | schedule := &SpecSchedule{ 35 | Second: getField(fields[0], seconds), 36 | Minute: getField(fields[1], minutes), 37 | Hour: getField(fields[2], hours), 38 | Dom: getField(fields[3], dom), 39 | Month: getField(fields[4], months), 40 | Dow: getField(fields[5], dow), 41 | } 42 | 43 | return schedule 44 | } 45 | 46 | // getField returns an Int with the bits set representing all of the times that 47 | // the field represents. A "field" is a comma-separated list of "ranges". 48 | func getField(field string, r bounds) uint64 { 49 | // list = range {"," range} 50 | var bits uint64 51 | ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' }) 52 | for _, expr := range ranges { 53 | bits |= getRange(expr, r) 54 | } 55 | return bits 56 | } 57 | 58 | // getRange returns the bits indicated by the given expression: 59 | // number | number "-" number [ "/" number ] 60 | func getRange(expr string, r bounds) uint64 { 61 | 62 | var ( 63 | start, end, step uint 64 | rangeAndStep = strings.Split(expr, "/") 65 | lowAndHigh = strings.Split(rangeAndStep[0], "-") 66 | singleDigit = len(lowAndHigh) == 1 67 | ) 68 | 69 | var extra_star uint64 70 | if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { 71 | start = r.min 72 | end = r.max 73 | extra_star = starBit 74 | } else { 75 | start = parseIntOrName(lowAndHigh[0], r.names) 76 | switch len(lowAndHigh) { 77 | case 1: 78 | end = start 79 | case 2: 80 | end = parseIntOrName(lowAndHigh[1], r.names) 81 | default: 82 | log.Panicf("Too many hyphens: %s", expr) 83 | } 84 | } 85 | 86 | switch len(rangeAndStep) { 87 | case 1: 88 | step = 1 89 | case 2: 90 | step = mustParseInt(rangeAndStep[1]) 91 | 92 | // Special handling: "N/step" means "N-max/step". 93 | if singleDigit { 94 | end = r.max 95 | } 96 | default: 97 | log.Panicf("Too many slashes: %s", expr) 98 | } 99 | 100 | if start < r.min { 101 | log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr) 102 | } 103 | if end > r.max { 104 | log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr) 105 | } 106 | if start > end { 107 | log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr) 108 | } 109 | 110 | return getBits(start, end, step) | extra_star 111 | } 112 | 113 | // parseIntOrName returns the (possibly-named) integer contained in expr. 114 | func parseIntOrName(expr string, names map[string]uint) uint { 115 | if names != nil { 116 | if namedInt, ok := names[strings.ToLower(expr)]; ok { 117 | return namedInt 118 | } 119 | } 120 | return mustParseInt(expr) 121 | } 122 | 123 | // mustParseInt parses the given expression as an int or panics. 124 | func mustParseInt(expr string) uint { 125 | num, err := strconv.Atoi(expr) 126 | if err != nil { 127 | log.Panicf("Failed to parse int from %s: %s", expr, err) 128 | } 129 | if num < 0 { 130 | log.Panicf("Negative number (%d) not allowed: %s", num, expr) 131 | } 132 | 133 | return uint(num) 134 | } 135 | 136 | // getBits sets all bits in the range [min, max], modulo the given step size. 137 | func getBits(min, max, step uint) uint64 { 138 | var bits uint64 139 | 140 | // If step is 1, use shifts. 141 | if step == 1 { 142 | return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min) 143 | } 144 | 145 | // Else, use a simple loop. 146 | for i := min; i <= max; i += step { 147 | bits |= 1 << i 148 | } 149 | return bits 150 | } 151 | 152 | // all returns all bits within the given bounds. (plus the star bit) 153 | func all(r bounds) uint64 { 154 | return getBits(r.min, r.max, 1) | starBit 155 | } 156 | 157 | // parseDescriptor returns a pre-defined schedule for the expression, or panics 158 | // if none matches. 159 | func parseDescriptor(spec string) Schedule { 160 | switch spec { 161 | case "@yearly", "@annually": 162 | return &SpecSchedule{ 163 | Second: 1 << seconds.min, 164 | Minute: 1 << minutes.min, 165 | Hour: 1 << hours.min, 166 | Dom: 1 << dom.min, 167 | Month: 1 << months.min, 168 | Dow: all(dow), 169 | } 170 | 171 | case "@monthly": 172 | return &SpecSchedule{ 173 | Second: 1 << seconds.min, 174 | Minute: 1 << minutes.min, 175 | Hour: 1 << hours.min, 176 | Dom: 1 << dom.min, 177 | Month: all(months), 178 | Dow: all(dow), 179 | } 180 | 181 | case "@weekly": 182 | return &SpecSchedule{ 183 | Second: 1 << seconds.min, 184 | Minute: 1 << minutes.min, 185 | Hour: 1 << hours.min, 186 | Dom: all(dom), 187 | Month: all(months), 188 | Dow: 1 << dow.min, 189 | } 190 | 191 | case "@daily", "@midnight": 192 | return &SpecSchedule{ 193 | Second: 1 << seconds.min, 194 | Minute: 1 << minutes.min, 195 | Hour: 1 << hours.min, 196 | Dom: all(dom), 197 | Month: all(months), 198 | Dow: all(dow), 199 | } 200 | 201 | case "@hourly": 202 | return &SpecSchedule{ 203 | Second: 1 << seconds.min, 204 | Minute: 1 << minutes.min, 205 | Hour: all(hours), 206 | Dom: all(dom), 207 | Month: all(months), 208 | Dow: all(dow), 209 | } 210 | } 211 | 212 | const every = "@every " 213 | if strings.HasPrefix(spec, every) { 214 | duration, err := time.ParseDuration(spec[len(every):]) 215 | if err != nil { 216 | log.Panicf("Failed to parse duration %s: %s", spec, err) 217 | } 218 | return Every(duration) 219 | } 220 | 221 | log.Panicf("Unrecognized descriptor: %s", spec) 222 | return nil 223 | } 224 | -------------------------------------------------------------------------------- /cron.go: -------------------------------------------------------------------------------- 1 | // This library implements a cron spec parser and runner. See the README for 2 | // more details. 3 | package cron 4 | 5 | import ( 6 | "sort" 7 | "time" 8 | ) 9 | 10 | type entries []*Entry 11 | 12 | // Cron keeps track of any number of entries, invoking the associated func as 13 | // specified by the schedule. It may be started, stopped, and the entries may 14 | // be inspected while running. 15 | type Cron struct { 16 | entries entries 17 | stop chan struct{} 18 | add chan *Entry 19 | remove chan string 20 | snapshot chan entries 21 | running bool 22 | } 23 | 24 | // Job is an interface for submitted cron jobs. 25 | type Job interface { 26 | Run() 27 | } 28 | 29 | // The Schedule describes a job's duty cycle. 30 | type Schedule interface { 31 | // Return the next activation time, later than the given time. 32 | // Next is invoked initially, and then each time the job is run. 33 | Next(time.Time) time.Time 34 | } 35 | 36 | // Entry consists of a schedule and the func to execute on that schedule. 37 | type Entry struct { 38 | // The schedule on which this job should be run. 39 | Schedule Schedule 40 | 41 | // The next time the job will run. This is the zero time if Cron has not been 42 | // started or this entry's schedule is unsatisfiable 43 | Next time.Time 44 | 45 | // The last time this job was run. This is the zero time if the job has never 46 | // been run. 47 | Prev time.Time 48 | 49 | // The Job to run. 50 | Job Job 51 | 52 | // Unique name to identify the Entry so as to be able to remove it later. 53 | Name string 54 | } 55 | 56 | // byTime is a wrapper for sorting the entry array by time 57 | // (with zero time at the end). 58 | type byTime []*Entry 59 | 60 | func (s byTime) Len() int { return len(s) } 61 | func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 62 | func (s byTime) Less(i, j int) bool { 63 | // Two zero times should return false. 64 | // Otherwise, zero is "greater" than any other time. 65 | // (To sort it at the end of the list.) 66 | if s[i].Next.IsZero() { 67 | return false 68 | } 69 | if s[j].Next.IsZero() { 70 | return true 71 | } 72 | return s[i].Next.Before(s[j].Next) 73 | } 74 | 75 | // New returns a new Cron job runner. 76 | func New() *Cron { 77 | return &Cron{ 78 | entries: nil, 79 | add: make(chan *Entry), 80 | remove: make(chan string), 81 | stop: make(chan struct{}), 82 | snapshot: make(chan entries), 83 | running: false, 84 | } 85 | } 86 | 87 | // A wrapper that turns a func() into a cron.Job 88 | type FuncJob func() 89 | 90 | func (f FuncJob) Run() { f() } 91 | 92 | // AddFunc adds a func to the Cron to be run on the given schedule. 93 | func (c *Cron) AddFunc(spec string, cmd func(), name string) { 94 | c.AddJob(spec, FuncJob(cmd), name) 95 | } 96 | 97 | // AddFunc adds a Job to the Cron to be run on the given schedule. 98 | func (c *Cron) AddJob(spec string, cmd Job, name string) { 99 | c.Schedule(Parse(spec), cmd, name) 100 | } 101 | 102 | // RemoveJob removes a Job from the Cron based on name. 103 | func (c *Cron) RemoveJob(name string) { 104 | if !c.running { 105 | i := c.entries.pos(name) 106 | 107 | if i == -1 { 108 | return 109 | } 110 | 111 | c.entries = c.entries[:i+copy(c.entries[i:], c.entries[i+1:])] 112 | return 113 | } 114 | 115 | c.remove <- name 116 | } 117 | 118 | func (entrySlice entries) pos(name string) int { 119 | for p, e := range entrySlice { 120 | if e.Name == name { 121 | return p 122 | } 123 | } 124 | return -1 125 | } 126 | 127 | // Schedule adds a Job to the Cron to be run on the given schedule. 128 | func (c *Cron) Schedule(schedule Schedule, cmd Job, name string) { 129 | entry := &Entry{ 130 | Schedule: schedule, 131 | Job: cmd, 132 | Name: name, 133 | } 134 | 135 | if !c.running { 136 | i := c.entries.pos(entry.Name) 137 | if i != -1 { 138 | return 139 | } 140 | c.entries = append(c.entries, entry) 141 | return 142 | } 143 | 144 | c.add <- entry 145 | } 146 | 147 | // Entries returns a snapshot of the cron entries. 148 | func (c *Cron) Entries() []*Entry { 149 | if c.running { 150 | c.snapshot <- nil 151 | x := <-c.snapshot 152 | return x 153 | } 154 | return c.entrySnapshot() 155 | } 156 | 157 | // Start the cron scheduler in its own go-routine. 158 | func (c *Cron) Start() { 159 | c.running = true 160 | go c.run() 161 | } 162 | 163 | // Run the scheduler.. this is private just due to the need to synchronize 164 | // access to the 'running' state variable. 165 | func (c *Cron) run() { 166 | // Figure out the next activation times for each entry. 167 | now := time.Now().Local() 168 | for _, entry := range c.entries { 169 | entry.Next = entry.Schedule.Next(now) 170 | } 171 | 172 | for { 173 | // Determine the next entry to run. 174 | sort.Sort(byTime(c.entries)) 175 | 176 | var effective time.Time 177 | if len(c.entries) == 0 || c.entries[0].Next.IsZero() { 178 | // If there are no entries yet, just sleep - it still handles new entries 179 | // and stop requests. 180 | effective = now.AddDate(10, 0, 0) 181 | } else { 182 | effective = c.entries[0].Next 183 | } 184 | 185 | select { 186 | case now = <-time.After(effective.Sub(now)): 187 | // Run every entry whose next time was this effective time. 188 | for _, e := range c.entries { 189 | if e.Next != effective { 190 | break 191 | } 192 | go e.Job.Run() 193 | e.Prev = e.Next 194 | e.Next = e.Schedule.Next(effective) 195 | } 196 | continue 197 | 198 | case newEntry := <-c.add: 199 | i := c.entries.pos(newEntry.Name) 200 | if i != -1 { 201 | break 202 | } 203 | c.entries = append(c.entries, newEntry) 204 | newEntry.Next = newEntry.Schedule.Next(time.Now().Local()) 205 | 206 | case name := <-c.remove: 207 | i := c.entries.pos(name) 208 | 209 | if i == -1 { 210 | break 211 | } 212 | 213 | c.entries = c.entries[:i+copy(c.entries[i:], c.entries[i+1:])] 214 | 215 | case <-c.snapshot: 216 | c.snapshot <- c.entrySnapshot() 217 | 218 | case <-c.stop: 219 | return 220 | } 221 | 222 | // 'now' should be updated after newEntry and snapshot cases. 223 | now = time.Now().Local() 224 | } 225 | } 226 | 227 | // Stop the cron scheduler. 228 | func (c *Cron) Stop() { 229 | c.stop <- struct{}{} 230 | c.running = false 231 | } 232 | 233 | // entrySnapshot returns a copy of the current cron entry list. 234 | func (c *Cron) entrySnapshot() []*Entry { 235 | entries := []*Entry{} 236 | for _, e := range c.entries { 237 | entries = append(entries, &Entry{ 238 | Schedule: e.Schedule, 239 | Next: e.Next, 240 | Prev: e.Prev, 241 | Job: e.Job, 242 | Name: e.Name, 243 | }) 244 | } 245 | return entries 246 | } 247 | -------------------------------------------------------------------------------- /cron_test.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // Many tests schedule a job for every second, and then wait at most a second 11 | // for it to run. This amount is just slightly larger than 1 second to 12 | // compensate for a few milliseconds of runtime. 13 | const ONE_SECOND = 1*time.Second + 10*time.Millisecond 14 | 15 | // Start and stop cron with no entries. 16 | func TestNoEntries(t *testing.T) { 17 | cron := New() 18 | cron.Start() 19 | 20 | select { 21 | case <-time.After(ONE_SECOND): 22 | t.FailNow() 23 | case <-stop(cron): 24 | } 25 | } 26 | 27 | // Start, stop, then add an entry. Verify entry doesn't run. 28 | func TestStopCausesJobsToNotRun(t *testing.T) { 29 | wg := &sync.WaitGroup{} 30 | wg.Add(1) 31 | 32 | cron := New() 33 | cron.Start() 34 | cron.Stop() 35 | cron.AddFunc("* * * * * ?", func() { wg.Done() }, "test1") 36 | 37 | select { 38 | case <-time.After(ONE_SECOND): 39 | // No job ran! 40 | case <-wait(wg): 41 | t.FailNow() 42 | } 43 | } 44 | 45 | // Add a job, start cron, expect it runs. 46 | func TestAddBeforeRunning(t *testing.T) { 47 | wg := &sync.WaitGroup{} 48 | wg.Add(1) 49 | 50 | cron := New() 51 | cron.AddFunc("* * * * * ?", func() { wg.Done() }, "test2") 52 | cron.Start() 53 | defer cron.Stop() 54 | 55 | // Give cron 2 seconds to run our job (which is always activated). 56 | select { 57 | case <-time.After(ONE_SECOND): 58 | t.FailNow() 59 | case <-wait(wg): 60 | } 61 | } 62 | 63 | // Start cron, add a job, expect it runs. 64 | func TestAddWhileRunning(t *testing.T) { 65 | wg := &sync.WaitGroup{} 66 | wg.Add(1) 67 | 68 | cron := New() 69 | cron.Start() 70 | defer cron.Stop() 71 | cron.AddFunc("* * * * * ?", func() { wg.Done() }, "test3") 72 | 73 | select { 74 | case <-time.After(ONE_SECOND): 75 | t.FailNow() 76 | case <-wait(wg): 77 | } 78 | } 79 | 80 | // Test timing with Entries. 81 | func TestSnapshotEntries(t *testing.T) { 82 | wg := &sync.WaitGroup{} 83 | wg.Add(1) 84 | 85 | cron := New() 86 | cron.AddFunc("@every 2s", func() { wg.Done() }, "test4") 87 | cron.Start() 88 | defer cron.Stop() 89 | 90 | // Cron should fire in 2 seconds. After 1 second, call Entries. 91 | select { 92 | case <-time.After(ONE_SECOND): 93 | cron.Entries() 94 | } 95 | 96 | // Even though Entries was called, the cron should fire at the 2 second mark. 97 | select { 98 | case <-time.After(ONE_SECOND): 99 | t.FailNow() 100 | case <-wait(wg): 101 | } 102 | 103 | } 104 | 105 | // Test that the entries are correctly sorted. 106 | // Add a bunch of long-in-the-future entries, and an immediate entry, and ensure 107 | // that the immediate entry runs immediately. 108 | // Also: Test that multiple jobs run in the same instant. 109 | func TestMultipleEntries(t *testing.T) { 110 | wg := &sync.WaitGroup{} 111 | wg.Add(2) 112 | 113 | cron := New() 114 | cron.AddFunc("0 0 0 1 1 ?", func() {}, "test5") 115 | cron.AddFunc("* * * * * ?", func() { wg.Done() }, "test6") 116 | cron.AddFunc("0 0 0 31 12 ?", func() {}, "test7") 117 | cron.AddFunc("* * * * * ?", func() { wg.Done() }, "test8") 118 | 119 | cron.Start() 120 | defer cron.Stop() 121 | 122 | select { 123 | case <-time.After(ONE_SECOND): 124 | t.FailNow() 125 | case <-wait(wg): 126 | } 127 | } 128 | 129 | // Test running the same job twice. 130 | func TestRunningJobTwice(t *testing.T) { 131 | wg := &sync.WaitGroup{} 132 | wg.Add(2) 133 | 134 | cron := New() 135 | cron.AddFunc("0 0 0 1 1 ?", func() {}, "test9") 136 | cron.AddFunc("0 0 0 31 12 ?", func() {}, "test10") 137 | cron.AddFunc("* * * * * ?", func() { wg.Done() }, "test11") 138 | 139 | cron.Start() 140 | defer cron.Stop() 141 | 142 | select { 143 | case <-time.After(2 * ONE_SECOND): 144 | t.FailNow() 145 | case <-wait(wg): 146 | } 147 | } 148 | 149 | func TestRunningMultipleSchedules(t *testing.T) { 150 | wg := &sync.WaitGroup{} 151 | wg.Add(2) 152 | 153 | cron := New() 154 | cron.AddFunc("0 0 0 1 1 ?", func() {}, "test12") 155 | cron.AddFunc("0 0 0 31 12 ?", func() {}, "test13") 156 | cron.AddFunc("* * * * * ?", func() { wg.Done() }, "test14") 157 | cron.Schedule(Every(time.Minute), FuncJob(func() {}), "test15") 158 | cron.Schedule(Every(time.Second), FuncJob(func() { wg.Done() }), "test16") 159 | cron.Schedule(Every(time.Hour), FuncJob(func() {}), "test17") 160 | 161 | cron.Start() 162 | defer cron.Stop() 163 | 164 | select { 165 | case <-time.After(2 * ONE_SECOND): 166 | t.FailNow() 167 | case <-wait(wg): 168 | } 169 | } 170 | 171 | // Test that the cron is run in the local time zone (as opposed to UTC). 172 | func TestLocalTimezone(t *testing.T) { 173 | wg := &sync.WaitGroup{} 174 | wg.Add(1) 175 | 176 | now := time.Now().Local() 177 | spec := fmt.Sprintf("%d %d %d %d %d ?", 178 | now.Second()+1, now.Minute(), now.Hour(), now.Day(), now.Month()) 179 | 180 | cron := New() 181 | cron.AddFunc(spec, func() { wg.Done() }, "test18") 182 | cron.Start() 183 | defer cron.Stop() 184 | 185 | select { 186 | case <-time.After(ONE_SECOND): 187 | t.FailNow() 188 | case <-wait(wg): 189 | } 190 | } 191 | 192 | type testJob struct { 193 | wg *sync.WaitGroup 194 | name string 195 | } 196 | 197 | func (t testJob) Run() { 198 | t.wg.Done() 199 | } 200 | 201 | // Simple test using Runnables. 202 | func TestJob(t *testing.T) { 203 | wg := &sync.WaitGroup{} 204 | wg.Add(1) 205 | 206 | cron := New() 207 | cron.AddJob("0 0 0 30 Feb ?", testJob{wg, "job0"}, "test19") 208 | cron.AddJob("0 0 0 1 1 ?", testJob{wg, "job1"}, "test20") 209 | cron.AddJob("* * * * * ?", testJob{wg, "job2"}, "test21") 210 | cron.AddJob("1 0 0 1 1 ?", testJob{wg, "job3"}, "test22") 211 | cron.Schedule(Every(5*time.Second+5*time.Nanosecond), testJob{wg, "job4"}, "test23") 212 | cron.Schedule(Every(5*time.Minute), testJob{wg, "job5"}, "test24") 213 | 214 | cron.Start() 215 | defer cron.Stop() 216 | 217 | select { 218 | case <-time.After(ONE_SECOND): 219 | t.FailNow() 220 | case <-wait(wg): 221 | } 222 | 223 | // Ensure the entries are in the right order. 224 | expecteds := []string{"job2", "job4", "job5", "job1", "job3", "job0"} 225 | 226 | var actuals []string 227 | for _, entry := range cron.Entries() { 228 | actuals = append(actuals, entry.Job.(testJob).name) 229 | } 230 | 231 | for i, expected := range expecteds { 232 | if actuals[i] != expected { 233 | t.Errorf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals) 234 | t.FailNow() 235 | } 236 | } 237 | } 238 | 239 | // Add a job, start cron, remove the job, expect it to have not run 240 | func TestAddBeforeRunningThenRemoveWhileRunning(t *testing.T) { 241 | wg := &sync.WaitGroup{} 242 | wg.Add(1) 243 | 244 | cron := New() 245 | cron.AddFunc("* * * * * ?", func() { wg.Done() }, "test25") 246 | cron.Start() 247 | defer cron.Stop() 248 | cron.RemoveJob("test25") 249 | 250 | // Give cron 2 seconds to run our job (which is always activated). 251 | select { 252 | case <-time.After(ONE_SECOND): 253 | case <-wait(wg): 254 | t.FailNow() 255 | } 256 | } 257 | 258 | // Add a job, remove the job, start cron, expect it to have not run 259 | func TestAddBeforeRunningThenRemoveBeforeRunning(t *testing.T) { 260 | wg := &sync.WaitGroup{} 261 | wg.Add(1) 262 | 263 | cron := New() 264 | cron.AddFunc("* * * * * ?", func() { wg.Done() }, "test26") 265 | cron.RemoveJob("test26") 266 | cron.Start() 267 | defer cron.Stop() 268 | 269 | // Give cron 2 seconds to run our job (which is always activated). 270 | select { 271 | case <-time.After(ONE_SECOND): 272 | case <-wait(wg): 273 | t.FailNow() 274 | } 275 | } 276 | 277 | func wait(wg *sync.WaitGroup) chan bool { 278 | ch := make(chan bool) 279 | go func() { 280 | wg.Wait() 281 | ch <- true 282 | }() 283 | return ch 284 | } 285 | 286 | func stop(cron *Cron) chan bool { 287 | ch := make(chan bool) 288 | go func() { 289 | cron.Stop() 290 | ch <- true 291 | }() 292 | return ch 293 | } 294 | --------------------------------------------------------------------------------