whedon 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|