whedon 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/README.md +28 -0
- data/Rakefile +11 -0
- data/lib/whedon/schedule.rb +374 -0
- data/lib/whedon/version.rb +5 -0
- data/lib/whedon.rb +2 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/whedon/schedule_spec.rb +383 -0
- data/whedon.gemspec +27 -0
- metadata +87 -8
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# whedon - parse crontab syntax
|
2
|
+
|
3
|
+
The goal of this gem is to parse a crontab timing specification and produce an
|
4
|
+
object that can be queried about the schedule.
|
5
|
+
|
6
|
+
This gem began as an extraction of Rufus::CronLine from the rufus-schedule gem.
|
7
|
+
|
8
|
+
## API example
|
9
|
+
|
10
|
+
```
|
11
|
+
sch = Whedon::Schedule.new('30 * * * *')
|
12
|
+
|
13
|
+
# Most Recent
|
14
|
+
sch.last
|
15
|
+
|
16
|
+
# Upcoming
|
17
|
+
sch.next
|
18
|
+
|
19
|
+
# Next after date/time argument
|
20
|
+
sch.next("2020/07/01")
|
21
|
+
|
22
|
+
# Given date/time matches cron string
|
23
|
+
sch.matches?("2020/07/01 14:00:00")
|
24
|
+
|
25
|
+
# Give cron string represented as an array
|
26
|
+
# [seconds minutes hours days months weekdays monthdays timezone]
|
27
|
+
sch.to_a
|
28
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
RSpec::Core::RakeTask.new
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
desc 'Start IRB with preloaded environment'
|
9
|
+
task :console do
|
10
|
+
exec 'irb', "-I#{File.join(File.dirname(__FILE__), 'lib')}", '-rwhedon'
|
11
|
+
end
|
@@ -0,0 +1,374 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2006-2013, John Mettraux, jmettraux@gmail.com
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
9
|
+
# furnished to do so, subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
12
|
+
# all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
# THE SOFTWARE.
|
21
|
+
#
|
22
|
+
# Made in Japan.
|
23
|
+
#++
|
24
|
+
|
25
|
+
require 'tzinfo'
|
26
|
+
|
27
|
+
|
28
|
+
module Whedon
|
29
|
+
|
30
|
+
#
|
31
|
+
# A 'cron line' is a line in the sense of a crontab
|
32
|
+
# (man 5 crontab) file line.
|
33
|
+
#
|
34
|
+
class Schedule
|
35
|
+
|
36
|
+
DAY_S = 24 * 3600
|
37
|
+
WEEK_S = 7 * DAY_S
|
38
|
+
|
39
|
+
# The string used for creating this cronline instance.
|
40
|
+
#
|
41
|
+
attr_reader :original
|
42
|
+
|
43
|
+
attr_reader :seconds
|
44
|
+
attr_reader :minutes
|
45
|
+
attr_reader :hours
|
46
|
+
attr_reader :days
|
47
|
+
attr_reader :months
|
48
|
+
attr_reader :weekdays
|
49
|
+
attr_reader :monthdays
|
50
|
+
attr_reader :timezone
|
51
|
+
|
52
|
+
attr_accessor :raise_error_on_duplicate
|
53
|
+
|
54
|
+
def initialize(line)
|
55
|
+
|
56
|
+
super()
|
57
|
+
|
58
|
+
@original = line
|
59
|
+
|
60
|
+
items = line.split
|
61
|
+
|
62
|
+
@timezone = (TZInfo::Timezone.get(items.last) rescue nil)
|
63
|
+
items.pop if @timezone
|
64
|
+
|
65
|
+
raise ArgumentError.new(
|
66
|
+
"not a valid cronline : '#{line}'"
|
67
|
+
) unless items.length == 5 or items.length == 6
|
68
|
+
|
69
|
+
offset = items.length - 5
|
70
|
+
|
71
|
+
@seconds = offset == 1 ? parse_item(items[0], 0, 59) : [ 0 ]
|
72
|
+
@minutes = parse_item(items[0 + offset], 0, 59)
|
73
|
+
@hours = parse_item(items[1 + offset], 0, 24)
|
74
|
+
@days = parse_item(items[2 + offset], 1, 31)
|
75
|
+
@months = parse_item(items[3 + offset], 1, 12)
|
76
|
+
@weekdays, @monthdays = parse_weekdays(items[4 + offset])
|
77
|
+
|
78
|
+
[ @seconds, @minutes, @hours, @months ].each do |es|
|
79
|
+
|
80
|
+
raise ArgumentError.new(
|
81
|
+
"invalid cronline: '#{line}'"
|
82
|
+
) if es && es.find { |e| ! e.is_a?(Fixnum) }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns true if the given time matches this cron line.
|
87
|
+
#
|
88
|
+
def matches?(time)
|
89
|
+
|
90
|
+
time = as_time(time)
|
91
|
+
|
92
|
+
return false unless sub_match?(time, :sec, @seconds)
|
93
|
+
return false unless sub_match?(time, :min, @minutes)
|
94
|
+
return false unless sub_match?(time, :hour, @hours)
|
95
|
+
return false unless date_match?(time)
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
def now?(time=Time.now)
|
100
|
+
matches?(time)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns the next time that this cron line is supposed to 'fire'
|
104
|
+
#
|
105
|
+
# This is raw, 3 secs to iterate over 1 year on my macbook :( brutal.
|
106
|
+
# (Well, I was wrong, takes 0.001 sec on 1.8.7 and 1.9.1)
|
107
|
+
#
|
108
|
+
# This method accepts an optional Time parameter. It's the starting point
|
109
|
+
# for the 'search'. By default, it's Time.now
|
110
|
+
#
|
111
|
+
# Note that the time instance returned will be in the same time zone that
|
112
|
+
# the given start point Time (thus a result in the local time zone will
|
113
|
+
# be passed if no start time is specified (search start time set to
|
114
|
+
# Time.now))
|
115
|
+
#
|
116
|
+
# Whedon::CronLine.new('30 7 * * *').next_time(
|
117
|
+
# Time.mktime(2008, 10, 24, 7, 29))
|
118
|
+
# #=> Fri Oct 24 07:30:00 -0500 2008
|
119
|
+
#
|
120
|
+
# Whedon::CronLine.new('30 7 * * *').next_time(
|
121
|
+
# Time.utc(2008, 10, 24, 7, 29))
|
122
|
+
# #=> Fri Oct 24 07:30:00 UTC 2008
|
123
|
+
#
|
124
|
+
# Whedon::CronLine.new('30 7 * * *').next_time(
|
125
|
+
# Time.utc(2008, 10, 24, 7, 29)).localtime
|
126
|
+
# #=> Fri Oct 24 02:30:00 -0500 2008
|
127
|
+
#
|
128
|
+
# (Thanks to K Liu for the note and the examples)
|
129
|
+
#
|
130
|
+
def next_time(now=Time.now)
|
131
|
+
|
132
|
+
time = as_time(now)
|
133
|
+
time = time - time.usec * 1e-6 + 1
|
134
|
+
# small adjustment before starting
|
135
|
+
|
136
|
+
loop do
|
137
|
+
|
138
|
+
unless date_match?(time)
|
139
|
+
time += (24 - time.hour) * 3600 - time.min * 60 - time.sec; next
|
140
|
+
end
|
141
|
+
unless sub_match?(time, :hour, @hours)
|
142
|
+
time += (60 - time.min) * 60 - time.sec; next
|
143
|
+
end
|
144
|
+
unless sub_match?(time, :min, @minutes)
|
145
|
+
time += 60 - time.sec; next
|
146
|
+
end
|
147
|
+
unless sub_match?(time, :sec, @seconds)
|
148
|
+
time += 1; next
|
149
|
+
end
|
150
|
+
|
151
|
+
break
|
152
|
+
end
|
153
|
+
|
154
|
+
if @timezone
|
155
|
+
time = @timezone.local_to_utc(time)
|
156
|
+
time = time.getlocal unless now.utc?
|
157
|
+
end
|
158
|
+
|
159
|
+
time
|
160
|
+
end
|
161
|
+
alias_method :next, :next_time
|
162
|
+
|
163
|
+
# Returns the previous the cronline matched. It's like next_time, but
|
164
|
+
# for the past.
|
165
|
+
#
|
166
|
+
def previous_time(now=Time.now)
|
167
|
+
|
168
|
+
# looks back by slices of two hours,
|
169
|
+
#
|
170
|
+
# finds for '* * * * sun', '* * 13 * *' and '0 12 13 * *'
|
171
|
+
# starting 1970, 1, 1 in 1.8 to 2 seconds (says Rspec)
|
172
|
+
|
173
|
+
start = current = now - 2 * 3600
|
174
|
+
result = nil
|
175
|
+
|
176
|
+
loop do
|
177
|
+
nex = next_time(current)
|
178
|
+
return (result ? result : previous_time(start)) if nex > now
|
179
|
+
result = current = nex
|
180
|
+
end
|
181
|
+
|
182
|
+
# never reached
|
183
|
+
end
|
184
|
+
alias_method :previous, :previous_time
|
185
|
+
alias_method :last, :previous_time
|
186
|
+
|
187
|
+
# Returns an array of 6 arrays (seconds, minutes, hours, days,
|
188
|
+
# months, weekdays).
|
189
|
+
# This method is used by the cronline unit tests.
|
190
|
+
#
|
191
|
+
def to_array
|
192
|
+
|
193
|
+
[
|
194
|
+
@seconds,
|
195
|
+
@minutes,
|
196
|
+
@hours,
|
197
|
+
@days,
|
198
|
+
@months,
|
199
|
+
@weekdays,
|
200
|
+
@monthdays,
|
201
|
+
@timezone ? @timezone.name : nil
|
202
|
+
]
|
203
|
+
end
|
204
|
+
alias_method :to_a, :to_array
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
WEEKDAYS = %w[ sun mon tue wed thu fri sat ]
|
209
|
+
|
210
|
+
def raise_error_on_duplicate?
|
211
|
+
!!(raise_error_on_duplicate)
|
212
|
+
end
|
213
|
+
|
214
|
+
def as_time(time)
|
215
|
+
unless time.kind_of?(Time)
|
216
|
+
time = ( time.to_s =~ /^\d+$/ ) ?
|
217
|
+
Time.at(time.to_s) : Time.parse(time.to_s)
|
218
|
+
end
|
219
|
+
|
220
|
+
time = @timezone.utc_to_local(time.getutc) if @timezone
|
221
|
+
time
|
222
|
+
end
|
223
|
+
|
224
|
+
def parse_weekdays(item)
|
225
|
+
|
226
|
+
return nil if item == '*'
|
227
|
+
|
228
|
+
items = item.downcase.split(',')
|
229
|
+
|
230
|
+
weekdays = nil
|
231
|
+
monthdays = nil
|
232
|
+
|
233
|
+
items.each do |it|
|
234
|
+
|
235
|
+
if m = it.match(/^(.+)#(l|-?[12345])$/)
|
236
|
+
|
237
|
+
raise ArgumentError.new(
|
238
|
+
"ranges are not supported for monthdays (#{it})"
|
239
|
+
) if m[1].index('-')
|
240
|
+
|
241
|
+
expr = it.gsub(/#l/, '#-1')
|
242
|
+
|
243
|
+
(monthdays ||= []) << expr
|
244
|
+
|
245
|
+
else
|
246
|
+
|
247
|
+
expr = it.dup
|
248
|
+
WEEKDAYS.each_with_index { |a, i| expr.gsub!(/#{a}/, i.to_s) }
|
249
|
+
|
250
|
+
raise ArgumentError.new(
|
251
|
+
"invalid weekday expression (#{it})"
|
252
|
+
) if expr !~ /^0*[0-7](-0*[0-7])?$/
|
253
|
+
|
254
|
+
its = expr.index('-') ? parse_range(expr, 0, 7) : [ Integer(expr) ]
|
255
|
+
its = its.collect { |i| i == 7 ? 0 : i }
|
256
|
+
|
257
|
+
(weekdays ||= []).concat(its)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
weekdays = weekdays.uniq if weekdays
|
262
|
+
|
263
|
+
[ weekdays, monthdays ]
|
264
|
+
end
|
265
|
+
|
266
|
+
def parse_item(item, min, max)
|
267
|
+
|
268
|
+
return nil if item == '*'
|
269
|
+
|
270
|
+
r = item.split(',').map { |i| parse_range(i.strip, min, max) }.flatten
|
271
|
+
|
272
|
+
raise ArgumentError.new(
|
273
|
+
"found duplicates in #{item.inspect}"
|
274
|
+
) if raise_error_on_duplicate? && r.uniq.size < r.size
|
275
|
+
|
276
|
+
r.uniq
|
277
|
+
end
|
278
|
+
|
279
|
+
RANGE_REGEX = /^(\*|\d{1,2})(?:-(\d{1,2}))?(?:\/(\d{1,2}))?$/
|
280
|
+
|
281
|
+
def parse_range(item, min, max)
|
282
|
+
|
283
|
+
return %w[ L ] if item == 'L'
|
284
|
+
|
285
|
+
m = item.match(RANGE_REGEX)
|
286
|
+
|
287
|
+
raise ArgumentError.new(
|
288
|
+
"cannot parse #{item.inspect}"
|
289
|
+
) unless m
|
290
|
+
|
291
|
+
sta = m[1]
|
292
|
+
sta = sta == '*' ? min : sta.to_i
|
293
|
+
|
294
|
+
edn = m[2]
|
295
|
+
edn = edn ? edn.to_i : sta
|
296
|
+
edn = max if m[1] == '*'
|
297
|
+
|
298
|
+
inc = m[3]
|
299
|
+
inc = inc ? inc.to_i : 1
|
300
|
+
|
301
|
+
raise ArgumentError.new(
|
302
|
+
"#{item.inspect} is not in range #{min}..#{max}"
|
303
|
+
) if sta < min or edn > max
|
304
|
+
|
305
|
+
r = []
|
306
|
+
val = sta
|
307
|
+
|
308
|
+
loop do
|
309
|
+
v = val
|
310
|
+
v = 0 if max == 24 && v == 24
|
311
|
+
r << v
|
312
|
+
break if inc == 1 && val == edn
|
313
|
+
val += inc
|
314
|
+
break if inc > 1 && val > edn
|
315
|
+
val = min if val > max
|
316
|
+
end
|
317
|
+
|
318
|
+
r.uniq
|
319
|
+
end
|
320
|
+
|
321
|
+
def sub_match?(time, accessor, values)
|
322
|
+
|
323
|
+
value = time.send(accessor)
|
324
|
+
|
325
|
+
return true if values.nil?
|
326
|
+
return true if values.include?('L') && (time + DAY_S).day == 1
|
327
|
+
|
328
|
+
return true if value == 0 && accessor == :hour && values.include?(24)
|
329
|
+
|
330
|
+
values.include?(value)
|
331
|
+
end
|
332
|
+
|
333
|
+
def monthday_match?(date, values)
|
334
|
+
|
335
|
+
return true if values.nil?
|
336
|
+
|
337
|
+
today_values = monthdays(date)
|
338
|
+
|
339
|
+
(today_values & values).any?
|
340
|
+
end
|
341
|
+
|
342
|
+
def date_match?(date)
|
343
|
+
|
344
|
+
return false unless sub_match?(date, :day, @days)
|
345
|
+
return false unless sub_match?(date, :month, @months)
|
346
|
+
return false unless sub_match?(date, :wday, @weekdays)
|
347
|
+
return false unless monthday_match?(date, @monthdays)
|
348
|
+
true
|
349
|
+
end
|
350
|
+
|
351
|
+
def monthdays(date)
|
352
|
+
|
353
|
+
pos = 1
|
354
|
+
d = date.dup
|
355
|
+
|
356
|
+
loop do
|
357
|
+
d = d - WEEK_S
|
358
|
+
break if d.month != date.month
|
359
|
+
pos = pos + 1
|
360
|
+
end
|
361
|
+
|
362
|
+
neg = -1
|
363
|
+
d = date.dup
|
364
|
+
|
365
|
+
loop do
|
366
|
+
d = d + WEEK_S
|
367
|
+
break if d.month != date.month
|
368
|
+
neg = neg - 1
|
369
|
+
end
|
370
|
+
|
371
|
+
[ "#{WEEKDAYS[date.wday]}##{pos}", "#{WEEKDAYS[date.wday]}##{neg}" ]
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
data/lib/whedon.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.expand_path("../../lib/whedon", __FILE__)
|
2
|
+
|
3
|
+
class Ex < Whedon::Schedule
|
4
|
+
def initialize(line)
|
5
|
+
self.raise_error_on_duplicate = true
|
6
|
+
super(line)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def local(*args)
|
11
|
+
Time.local(*args)
|
12
|
+
end
|
13
|
+
alias lo local
|
14
|
+
|
15
|
+
def utc(*args)
|
16
|
+
Time.utc(*args)
|
17
|
+
end
|
18
|
+
|
19
|
+
def cl(line)
|
20
|
+
Whedon::Schedule.new(line)
|
21
|
+
end
|
22
|
+
|
23
|
+
def compare(line, array)
|
24
|
+
cl(line).to_array.should == array
|
25
|
+
end
|
@@ -0,0 +1,383 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Specifying rufus-scheduler
|
4
|
+
#
|
5
|
+
# Sat Mar 21 12:55:27 JST 2009
|
6
|
+
#
|
7
|
+
|
8
|
+
#require 'spec_base'
|
9
|
+
require "spec_helper"
|
10
|
+
require "whedon/schedule"
|
11
|
+
|
12
|
+
describe Whedon::Schedule do
|
13
|
+
#
|
14
|
+
# See spec_helper.rb for definitions for the class & methods
|
15
|
+
# used in these tests. This includes Ex, local, utc, cl,
|
16
|
+
# match, no_match, and compare
|
17
|
+
|
18
|
+
describe '.new' do
|
19
|
+
|
20
|
+
it 'interprets cron strings correctly' do
|
21
|
+
|
22
|
+
compare '* * * * *', [ [0], nil, nil, nil, nil, nil, nil, nil ]
|
23
|
+
compare '10-12 * * * *', [ [0], [10, 11, 12], nil, nil, nil, nil, nil, nil ]
|
24
|
+
compare '* * * * sun,mon', [ [0], nil, nil, nil, nil, [0, 1], nil, nil ]
|
25
|
+
compare '* * * * mon-wed', [ [0], nil, nil, nil, nil, [1, 2, 3], nil, nil ]
|
26
|
+
compare '* * * * 7', [ [0], nil, nil, nil, nil, [0], nil, nil ]
|
27
|
+
compare '* * * * 0', [ [0], nil, nil, nil, nil, [0], nil, nil ]
|
28
|
+
compare '* * * * 0,1', [ [0], nil, nil, nil, nil, [0,1], nil, nil ]
|
29
|
+
compare '* * * * 7,1', [ [0], nil, nil, nil, nil, [0,1], nil, nil ]
|
30
|
+
compare '* * * * 7,0', [ [0], nil, nil, nil, nil, [0], nil, nil ]
|
31
|
+
compare '* * * * sun,2-4', [ [0], nil, nil, nil, nil, [0, 2, 3, 4], nil, nil ]
|
32
|
+
|
33
|
+
compare '* * * * sun,mon-tue', [ [0], nil, nil, nil, nil, [0, 1, 2], nil, nil ]
|
34
|
+
|
35
|
+
compare '* * * * * *', [ nil, nil, nil, nil, nil, nil, nil, nil ]
|
36
|
+
compare '1 * * * * *', [ [1], nil, nil, nil, nil, nil, nil, nil ]
|
37
|
+
compare '7 10-12 * * * *', [ [7], [10, 11, 12], nil, nil, nil, nil, nil, nil ]
|
38
|
+
compare '1-5 * * * * *', [ [1,2,3,4,5], nil, nil, nil, nil, nil, nil, nil ]
|
39
|
+
|
40
|
+
compare '0 0 1 1 *', [ [0], [0], [0], [1], [1], nil, nil, nil ]
|
41
|
+
|
42
|
+
compare '0 23-24 * * *', [ [0], [0], [23, 0], nil, nil, nil, nil, nil ]
|
43
|
+
#
|
44
|
+
# as reported by Aimee Rose in
|
45
|
+
# https://github.com/jmettraux/rufus-scheduler/issues/56
|
46
|
+
|
47
|
+
compare '0 23-2 * * *', [ [0], [0], [23, 0, 1, 2], nil, nil, nil, nil, nil ]
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'rejects invalid weekday expressions' do
|
51
|
+
|
52
|
+
lambda { cl '0 17 * * MON_FRI' }.should raise_error
|
53
|
+
# underline instead of dash
|
54
|
+
|
55
|
+
lambda { cl '* * * * 9' }.should raise_error
|
56
|
+
lambda { cl '* * * * 0-12' }.should raise_error
|
57
|
+
lambda { cl '* * * * BLABLA' }.should raise_error
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'rejects invalid cronlines' do
|
61
|
+
|
62
|
+
lambda { cl '* nada * * 9' }.should raise_error(ArgumentError)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'interprets cron strings with TZ correctly' do
|
66
|
+
|
67
|
+
compare '* * * * * EST', [ [0], nil, nil, nil, nil, nil, nil, 'EST' ]
|
68
|
+
compare '* * * * * * EST', [ nil, nil, nil, nil, nil, nil, nil, 'EST' ]
|
69
|
+
|
70
|
+
lambda { cl '* * * * * NotATimeZone' }.should raise_error
|
71
|
+
lambda { cl '* * * * * * NotATimeZone' }.should raise_error
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'interprets cron strings with / (slashes) correctly' do
|
75
|
+
|
76
|
+
compare(
|
77
|
+
'0 */2 * * *',
|
78
|
+
[ [0], [0], (0..11).collect { |e| e * 2 }, nil, nil, nil, nil, nil ])
|
79
|
+
compare(
|
80
|
+
'0 7-23/2 * * *',
|
81
|
+
[ [0], [0], (7..23).select { |e| e.odd? }, nil, nil, nil, nil, nil ])
|
82
|
+
compare(
|
83
|
+
'*/10 * * * *',
|
84
|
+
[ [0], [0, 10, 20, 30, 40, 50], nil, nil, nil, nil, nil, nil ])
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'does not support ranges for monthdays (sun#1-sun#2)' do
|
88
|
+
|
89
|
+
lambda {
|
90
|
+
Whedon::Schedule.new('* * * * sun#1-sun#2')
|
91
|
+
}.should raise_error(ArgumentError)
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'accepts items with initial 0' do
|
95
|
+
|
96
|
+
compare '09 * * * *', [ [0], [9], nil, nil, nil, nil, nil, nil ]
|
97
|
+
compare '09-12 * * * *', [ [0], [9, 10, 11, 12], nil, nil, nil, nil, nil, nil ]
|
98
|
+
compare '07-08 * * * *', [ [0], [7, 8], nil, nil, nil, nil, nil, nil ]
|
99
|
+
compare '* */08 * * *', [ [0], nil, [0, 8, 16], nil, nil, nil, nil, nil ]
|
100
|
+
compare '* */07 * * *', [ [0], nil, [0, 7, 14, 21], nil, nil, nil, nil, nil ]
|
101
|
+
compare '* 01-09/04 * * *', [ [0], nil, [1, 5, 9], nil, nil, nil, nil, nil ]
|
102
|
+
compare '* * * * 06', [ [0], nil, nil, nil, nil, [6], nil, nil ]
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'ignores duplicates by default' do
|
106
|
+
|
107
|
+
compare '* * L,L * *', [[0], nil, nil, ['L'], nil, nil, nil, nil ]
|
108
|
+
compare '*/20,40 * * * *', [ [0], [0, 20, 40 ], nil, nil, nil, nil, nil, nil ]
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'raises an error for duplicates when configured to do so' do
|
112
|
+
|
113
|
+
lambda { Ex.new('* * L,L * *') }.should raise_error(ArgumentError)
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'interprets cron strings with L correctly' do
|
117
|
+
|
118
|
+
compare '* * L * *', [[0], nil, nil, ['L'], nil, nil, nil, nil ]
|
119
|
+
compare '* * 2-5,L * *', [[0], nil, nil, [2,3,4,5,'L'], nil, nil, nil, nil ]
|
120
|
+
compare '* * */8,L * *', [[0], nil, nil, [1,9,17,25,'L'], nil, nil, nil, nil ]
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'does not support ranges for L' do
|
124
|
+
|
125
|
+
lambda { cl '* * 15-L * *'}.should raise_error(ArgumentError)
|
126
|
+
lambda { cl '* * L/4 * *'}.should raise_error(ArgumentError)
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'raises if L is used for something else than days' do
|
130
|
+
|
131
|
+
lambda { cl '* L * * *'}.should raise_error(ArgumentError)
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'raises for out of range input' do
|
135
|
+
|
136
|
+
lambda { cl '60-62 * * * *'}.should raise_error(ArgumentError)
|
137
|
+
lambda { cl '62 * * * *'}.should raise_error(ArgumentError)
|
138
|
+
lambda { cl '60 * * * *'}.should raise_error(ArgumentError)
|
139
|
+
lambda { cl '* 25-26 * * *'}.should raise_error(ArgumentError)
|
140
|
+
lambda { cl '* 25 * * *'}.should raise_error(ArgumentError)
|
141
|
+
#
|
142
|
+
# as reported by Aimee Rose in
|
143
|
+
# https://github.com/jmettraux/rufus-scheduler/pull/58
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe '#next_time' do
|
148
|
+
|
149
|
+
def nt(cronline, now)
|
150
|
+
Whedon::Schedule.new(cronline).next_time(now)
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'computes the next occurence correctly' do
|
154
|
+
|
155
|
+
now = Time.at(0).getutc # Thu Jan 01 00:00:00 UTC 1970
|
156
|
+
|
157
|
+
nt('* * * * *', now).should == now + 60
|
158
|
+
nt('* * * * sun', now).should == now + 259200
|
159
|
+
nt('* * * * * *', now).should == now + 1
|
160
|
+
nt('* * 13 * fri', now).should == now + 3715200
|
161
|
+
|
162
|
+
nt('10 12 13 12 *', now).should == now + 29938200
|
163
|
+
# this one is slow (1 year == 3 seconds)
|
164
|
+
#
|
165
|
+
# historical note:
|
166
|
+
# (comment made in 2006 or 2007, the underlying libs got better and
|
167
|
+
# that slowness is gone)
|
168
|
+
|
169
|
+
nt('0 0 * * thu', now).should == now + 604800
|
170
|
+
|
171
|
+
nt('0 0 * * *', now).should == now + 24 * 3600
|
172
|
+
nt('0 24 * * *', now).should == now + 24 * 3600
|
173
|
+
|
174
|
+
now = local(2008, 12, 31, 23, 59, 59, 0)
|
175
|
+
|
176
|
+
nt('* * * * *', now).should == now + 1
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'computes the next occurence correctly in UTC (TZ not specified)' do
|
180
|
+
|
181
|
+
now = utc(1970, 1, 1)
|
182
|
+
|
183
|
+
nt('* * * * *', now).should == utc(1970, 1, 1, 0, 1)
|
184
|
+
nt('* * * * sun', now).should == utc(1970, 1, 4)
|
185
|
+
nt('* * * * * *', now).should == utc(1970, 1, 1, 0, 0, 1)
|
186
|
+
nt('* * 13 * fri', now).should == utc(1970, 2, 13)
|
187
|
+
|
188
|
+
nt('10 12 13 12 *', now).should == utc(1970, 12, 13, 12, 10)
|
189
|
+
# this one is slow (1 year == 3 seconds)
|
190
|
+
nt('* * 1 6 *', now).should == utc(1970, 6, 1)
|
191
|
+
|
192
|
+
nt('0 0 * * thu', now).should == utc(1970, 1, 8)
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'computes the next occurence correctly in local TZ (TZ not specified)' do
|
196
|
+
|
197
|
+
now = local(1970, 1, 1)
|
198
|
+
|
199
|
+
nt('* * * * *', now).should == local(1970, 1, 1, 0, 1)
|
200
|
+
nt('* * * * sun', now).should == local(1970, 1, 4)
|
201
|
+
nt('* * * * * *', now).should == local(1970, 1, 1, 0, 0, 1)
|
202
|
+
nt('* * 13 * fri', now).should == local(1970, 2, 13)
|
203
|
+
|
204
|
+
nt('10 12 13 12 *', now).should == local(1970, 12, 13, 12, 10)
|
205
|
+
# this one is slow (1 year == 3 seconds)
|
206
|
+
nt('* * 1 6 *', now).should == local(1970, 6, 1)
|
207
|
+
|
208
|
+
nt('0 0 * * thu', now).should == local(1970, 1, 8)
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'computes the next occurence correctly in UTC (TZ specified)' do
|
212
|
+
|
213
|
+
zone = 'Europe/Stockholm'
|
214
|
+
tz = TZInfo::Timezone.get(zone)
|
215
|
+
now = tz.local_to_utc(local(1970, 1, 1))
|
216
|
+
# Midnight in zone, UTC
|
217
|
+
|
218
|
+
nt("* * * * * #{zone}", now).should == utc(1969, 12, 31, 23, 1)
|
219
|
+
nt("* * * * sun #{zone}", now).should == utc(1970, 1, 3, 23)
|
220
|
+
nt("* * * * * * #{zone}", now).should == utc(1969, 12, 31, 23, 0, 1)
|
221
|
+
nt("* * 13 * fri #{zone}", now).should == utc(1970, 2, 12, 23)
|
222
|
+
|
223
|
+
nt("10 12 13 12 * #{zone}", now).should == utc(1970, 12, 13, 11, 10)
|
224
|
+
nt("* * 1 6 * #{zone}", now).should == utc(1970, 5, 31, 23)
|
225
|
+
|
226
|
+
nt("0 0 * * thu #{zone}", now).should == utc(1970, 1, 7, 23)
|
227
|
+
end
|
228
|
+
|
229
|
+
#it 'computes the next occurence correctly in local TZ (TZ specified)' do
|
230
|
+
# zone = 'Europe/Stockholm'
|
231
|
+
# tz = TZInfo::Timezone.get(zone)
|
232
|
+
# now = tz.local_to_utc(utc(1970, 1, 1)).localtime
|
233
|
+
# # Midnight in zone, local time
|
234
|
+
# nt("* * * * * #{zone}", now).should == local(1969, 12, 31, 18, 1)
|
235
|
+
# nt("* * * * sun #{zone}", now).should == local(1970, 1, 3, 18)
|
236
|
+
# nt("* * * * * * #{zone}", now).should == local(1969, 12, 31, 18, 0, 1)
|
237
|
+
# nt("* * 13 * fri #{zone}", now).should == local(1970, 2, 12, 18)
|
238
|
+
# nt("10 12 13 12 * #{zone}", now).should == local(1970, 12, 13, 6, 10)
|
239
|
+
# nt("* * 1 6 * #{zone}", now).should == local(1970, 5, 31, 19)
|
240
|
+
# nt("0 0 * * thu #{zone}", now).should == local(1970, 1, 7, 18)
|
241
|
+
#end
|
242
|
+
|
243
|
+
it 'computes the next time correctly when there is a sun#2 involved' do
|
244
|
+
|
245
|
+
nt('* * * * sun#1', local(1970, 1, 1)).should == local(1970, 1, 4)
|
246
|
+
nt('* * * * sun#2', local(1970, 1, 1)).should == local(1970, 1, 11)
|
247
|
+
|
248
|
+
nt('* * * * sun#2', local(1970, 1, 12)).should == local(1970, 2, 8)
|
249
|
+
end
|
250
|
+
|
251
|
+
it 'computes the next time correctly when there is a sun#2,sun#3 involved' do
|
252
|
+
|
253
|
+
nt('* * * * sun#2,sun#3', local(1970, 1, 1)).should == local(1970, 1, 11)
|
254
|
+
nt('* * * * sun#2,sun#3', local(1970, 1, 12)).should == local(1970, 1, 18)
|
255
|
+
end
|
256
|
+
|
257
|
+
it 'understands sun#L' do
|
258
|
+
|
259
|
+
nt('* * * * sun#L', local(1970, 1, 1)).should == local(1970, 1, 25)
|
260
|
+
end
|
261
|
+
|
262
|
+
it 'understands sun#-1' do
|
263
|
+
|
264
|
+
nt('* * * * sun#-1', local(1970, 1, 1)).should == local(1970, 1, 25)
|
265
|
+
end
|
266
|
+
|
267
|
+
it 'understands sun#-2' do
|
268
|
+
|
269
|
+
nt('* * * * sun#-2', local(1970, 1, 1)).should == local(1970, 1, 18)
|
270
|
+
end
|
271
|
+
|
272
|
+
it 'computes the next time correctly when "L" (last day of month)' do
|
273
|
+
|
274
|
+
nt('* * L * *', lo(1970, 1, 1)).should == lo(1970, 1, 31)
|
275
|
+
nt('* * L * *', lo(1970, 2, 1)).should == lo(1970, 2, 28)
|
276
|
+
nt('* * L * *', lo(1972, 2, 1)).should == lo(1972, 2, 29)
|
277
|
+
nt('* * L * *', lo(1970, 4, 1)).should == lo(1970, 4, 30)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
describe '#previous_time' do
|
282
|
+
|
283
|
+
def pt(cronline, now)
|
284
|
+
Whedon::Schedule.new(cronline).previous_time(now)
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'returns the previous time the cron should have triggered' do
|
288
|
+
|
289
|
+
pt('* * * * sun', lo(1970, 1, 1)).should == lo(1969, 12, 28, 23, 59, 00)
|
290
|
+
pt('* * 13 * *', lo(1970, 1, 1)).should == lo(1969, 12, 13, 23, 59, 00)
|
291
|
+
pt('0 12 13 * *', lo(1970, 1, 1)).should == lo(1969, 12, 13, 12, 00)
|
292
|
+
|
293
|
+
pt('* * * * * sun', lo(1970, 1, 1)).should == lo(1969, 12, 28, 23, 59, 59)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
describe '#matches?' do
|
298
|
+
|
299
|
+
|
300
|
+
[ ['* * * * *', utc(1970, 1, 1, 0, 1), true],
|
301
|
+
['* * * * sun', utc(1970, 1, 4), true],
|
302
|
+
['* * * * * *', utc(1970, 1, 1, 0, 0, 1), true],
|
303
|
+
['* * 13 * fri', utc(1970, 2, 13), true],
|
304
|
+
['10 12 13 12 *', utc(1970, 12, 13, 12, 10), true],
|
305
|
+
['* * 1 6 *', utc(1970, 6, 1), true],
|
306
|
+
['0 0 * * thu', utc(1970, 1, 8), true],
|
307
|
+
['0 0 1 1 *', utc(2012, 1, 1), true],
|
308
|
+
['0 0 1 1 *', utc(2012, 1, 1, 1, 0), false]
|
309
|
+
].each do |line, time, result|
|
310
|
+
it 'matches correctly in UTC (TZ not specified)' do
|
311
|
+
cl(line).matches?(time).should eql(result)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
[ ['* * * * *', local(1970, 1, 1, 0, 1), true],
|
316
|
+
['* * * * sun', local(1970, 1, 4), true],
|
317
|
+
['* * * * * *', local(1970, 1, 1, 0, 0, 1), true],
|
318
|
+
['* * 13 * fri', local(1970, 2, 13), true],
|
319
|
+
['10 12 13 12 *', local(1970, 12, 13, 12, 10), true],
|
320
|
+
['* * 1 6 *', local(1970, 6, 1), true],
|
321
|
+
['0 0 * * thu', local(1970, 1, 8), true],
|
322
|
+
['0 0 1 1 *', local(2012, 1, 1), true],
|
323
|
+
['0 0 1 1 *', local(2012, 1, 1, 1, 0), false]
|
324
|
+
].each do |line, time, result|
|
325
|
+
it 'matches correctly in local TZ (TZ not specified)' do
|
326
|
+
cl(line).matches?(time).should eql(result)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
zone = 'Europe/Stockholm'
|
331
|
+
[ ["* * * * * #{zone}", utc(1969, 12, 31, 23, 1), true],
|
332
|
+
["* * * * sun #{zone}", utc(1970, 1, 3, 23), true],
|
333
|
+
["* * * * * * #{zone}", utc(1969, 12, 31, 23, 0, 1), true],
|
334
|
+
["* * 13 * fri #{zone}", utc(1970, 2, 12, 23), true],
|
335
|
+
["10 12 13 12 * #{zone}", utc(1970, 12, 13, 11, 10), true],
|
336
|
+
["* * 1 6 * #{zone}", utc(1970, 5, 31, 23), true],
|
337
|
+
["0 0 * * thu #{zone}", utc(1970, 1, 7, 23), true],
|
338
|
+
].each do |line, time, result|
|
339
|
+
it 'matches correctly in UTC (TZ specified)' do
|
340
|
+
cl(line).matches?(time).should eql(result)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
it 'matches correctly when there is a sun#2 involved' do
|
345
|
+
|
346
|
+
cl('* * 13 * fri#2').matches?(utc(1970, 2, 13)).should be_true
|
347
|
+
cl('* * 13 * fri#2').matches?(utc(1970, 2, 20)).should be_false
|
348
|
+
end
|
349
|
+
|
350
|
+
it 'matches correctly when there is a L involved' do
|
351
|
+
|
352
|
+
cl('* * L * *').matches?(utc(1970, 1, 31)).should be_true
|
353
|
+
cl('* * L * *').matches?(utc(1970, 1, 30)).should be_false
|
354
|
+
end
|
355
|
+
|
356
|
+
it 'matches correctly when there is a sun#2,sun#3 involved' do
|
357
|
+
|
358
|
+
cl('* * * * sun#2,sun#3').matches?( local(1970, 1, 4) ).should be_false
|
359
|
+
cl('* * * * sun#2,sun#3').matches?( local(1970, 1, 11) ).should be_true
|
360
|
+
cl('* * * * sun#2,sun#3').matches?( local(1970, 1, 18) ).should be_true
|
361
|
+
cl('* * * * sun#2,sun#3').matches?( local(1970, 1, 25) ).should be_false
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
describe '#monthdays' do
|
366
|
+
|
367
|
+
it 'returns the appropriate "sun#2"-like string' do
|
368
|
+
|
369
|
+
class Whedon::Schedule
|
370
|
+
public :monthdays
|
371
|
+
end
|
372
|
+
|
373
|
+
cl = Whedon::Schedule.new('* * * * *')
|
374
|
+
|
375
|
+
cl.monthdays(local(1970, 1, 1)).should == %w[ thu#1 thu#-5 ]
|
376
|
+
cl.monthdays(local(1970, 1, 7)).should == %w[ wed#1 wed#-4 ]
|
377
|
+
cl.monthdays(local(1970, 1, 14)).should == %w[ wed#2 wed#-3 ]
|
378
|
+
|
379
|
+
cl.monthdays(local(2011, 3, 11)).should == %w[ fri#2 fri#-3 ]
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
data/whedon.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "whedon/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "whedon"
|
7
|
+
s.version = Parse::Cron::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["John Mettraux", "Blake Thomas"]
|
10
|
+
s.email = ["jmettraux@gmail.com", "bwthomas@gmail.com"]
|
11
|
+
s.homepage = "https://github.com/bwthomas/whedon"
|
12
|
+
s.summary = %q{Parses cron lines}
|
13
|
+
s.description = %q{Parses cron lines into a Schedule instance that can be queried.}
|
14
|
+
|
15
|
+
s.rubyforge_project = "whedon"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_dependency 'tzinfo'
|
23
|
+
|
24
|
+
s.add_development_dependency 'rspec', '~>2.6.0'
|
25
|
+
s.add_development_dependency 'rake'
|
26
|
+
s.add_development_dependency 'pry'
|
27
|
+
end
|
metadata
CHANGED
@@ -1,24 +1,101 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: whedon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
|
+
- John Mettraux
|
8
9
|
- Blake Thomas
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2013-06-
|
13
|
-
dependencies:
|
14
|
-
|
13
|
+
date: 2013-06-30 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: tzinfo
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: rspec
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ~>
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: 2.6.0
|
39
|
+
type: :development
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 2.6.0
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: pry
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
type: :development
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
description: Parses cron lines into a Schedule instance that can be queried.
|
15
80
|
email:
|
81
|
+
- jmettraux@gmail.com
|
16
82
|
- bwthomas@gmail.com
|
17
83
|
executables: []
|
18
84
|
extensions: []
|
19
85
|
extra_rdoc_files: []
|
20
|
-
files:
|
21
|
-
|
86
|
+
files:
|
87
|
+
- .gitignore
|
88
|
+
- .rspec
|
89
|
+
- Gemfile
|
90
|
+
- README.md
|
91
|
+
- Rakefile
|
92
|
+
- lib/whedon.rb
|
93
|
+
- lib/whedon/schedule.rb
|
94
|
+
- lib/whedon/version.rb
|
95
|
+
- spec/spec_helper.rb
|
96
|
+
- spec/whedon/schedule_spec.rb
|
97
|
+
- whedon.gemspec
|
98
|
+
homepage: https://github.com/bwthomas/whedon
|
22
99
|
licenses: []
|
23
100
|
post_install_message:
|
24
101
|
rdoc_options: []
|
@@ -41,6 +118,8 @@ rubyforge_project: whedon
|
|
41
118
|
rubygems_version: 1.8.23
|
42
119
|
signing_key:
|
43
120
|
specification_version: 3
|
44
|
-
summary:
|
45
|
-
test_files:
|
121
|
+
summary: Parses cron lines
|
122
|
+
test_files:
|
123
|
+
- spec/spec_helper.rb
|
124
|
+
- spec/whedon/schedule_spec.rb
|
46
125
|
has_rdoc:
|