sakuro-crontab 0.0.2

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.
@@ -0,0 +1,17 @@
1
+ = What is this?
2
+
3
+ A crontab(5) parser.
4
+
5
+ == Usage
6
+
7
+ crontab = Crontab.parse(src)
8
+
9
+ from_date = Date.parse('2009-10-10')
10
+ to_date = Date.parse('2009-10-20')
11
+
12
+ crontab.entries.each do |e|
13
+ puts e.command
14
+ e.schedule.from(from_date).until(to_date) do |timing|
15
+ puts timing
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ require 'rake/gempackagetask'
2
+ require 'rake/rdoctask'
3
+ require 'spec/rake/spectask'
4
+
5
+ task :default => :spec
6
+
7
+ spec = Gem::Specification.load('crontab.gemspec')
8
+
9
+ Rake::GemPackageTask.new(spec) do |pkg|
10
+ pkg.need_zip = true
11
+ pkg.need_tar_bz2 = true
12
+ end
13
+
14
+ Rake::RDocTask.new(:rdoc) do |rdoc|
15
+ rdoc.main = 'README.rdoc'
16
+ rdoc.rdoc_files.include('README.rdoc', 'lib/**/*.rb')
17
+ rdoc.options << '--all'
18
+ end
19
+
20
+ Spec::Rake::SpecTask.new do |t|
21
+ t.warning = true
22
+ t.rcov = false
23
+ end
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'crontab'
3
+ s.version = '0.0.2'
4
+ s.author = 'OZAWA Sakuro'
5
+ s.email = 'github@2238club.org'
6
+ s.homepage = 'http://github.com/sakuro/crontab'
7
+ s.platform = Gem::Platform::RUBY
8
+ s.summary = 'A crontab(5) entry parser'
9
+ s.files = [
10
+ 'lib/crontab.rb',
11
+ 'lib/crontab/entry.rb',
12
+ 'lib/crontab/schedule.rb',
13
+ 'spec/crontab_spec.rb',
14
+ 'spec/crontab/entry_spec.rb',
15
+ 'spec/crontab/schedule_spec.rb',
16
+ 'crontab.gemspec',
17
+ 'Rakefile',
18
+ 'README.rdoc'
19
+ ]
20
+ s.has_rdoc = true
21
+ s.rubyforge_project = 'n/a'
22
+ end
@@ -0,0 +1,34 @@
1
+ require 'crontab/schedule'
2
+ require 'crontab/entry'
3
+
4
+ # A class which represents crontab(5) content.
5
+ class Crontab
6
+ class << self
7
+ # Parses a crontab(5) text.
8
+ #
9
+ # * <tt>src</tt> string in crontab(5) format
10
+ def parse(src)
11
+ entries = []
12
+ env = {}
13
+ src.lines.each do |line|
14
+ line.strip!
15
+ case line
16
+ when /\A[\d*]/
17
+ entries << Crontab::Entry.parse(line)
18
+ when /\A([A-Z][A-Z0-9_]*)\s*=\s*/, /\A"([A-Z][A-Z0-9_]*)"\s*=\s*/, /\A'([A-Z][A-Z0-9_]*)'\s*=\s*/
19
+ name = $1
20
+ value = $'
21
+ value = $1 if /\A'(.*)'\z/ =~ value or /\A"(.*)"\z/ =~ value
22
+ env[name] = value
23
+ end
24
+ end
25
+ new(entries, env)
26
+ end
27
+ end
28
+
29
+ def initialize(entries, env)
30
+ @entries = entries.dup.freeze
31
+ @env = env.dup.freeze
32
+ end
33
+ attr_reader :entries, :env
34
+ end
@@ -0,0 +1,49 @@
1
+ class Crontab
2
+ # A class which represents a job line in crontab(5).
3
+ class Entry
4
+ # Creates a crontab(5) entry.
5
+ #
6
+ # * <tt>schedule</tt> A Crontab::Schedule instance.
7
+ # * <tt>command</tt>
8
+ # * <tt>uid</tt>
9
+ def initialize(schedule, command, uid=nil)
10
+ raise ArgumentError, 'invalid schedule' unless schedule.is_a? Schedule
11
+ raise ArgumentError, 'invalid command' unless command.is_a? String
12
+
13
+ @schedule = schedule.freeze
14
+ @command = command.freeze
15
+ @uid =
16
+ case uid
17
+ when String
18
+ Etc.getpwnam(uid).uid
19
+ when Integer
20
+ uid
21
+ when nil
22
+ Process.uid
23
+ else
24
+ raise ArgumentError, 'invalid uid'
25
+ end
26
+ end
27
+
28
+ attr_reader :schedule, :command, :uid
29
+ class << self
30
+ # Parses a string line in crontab(5) job format.
31
+ #
32
+ # * <tt>options[:system]</tt> when true system wide crontab is assumed
33
+ # and <tt>@uid</tt> is extracted from <i>line</i>.
34
+ def parse(line, options={})
35
+ options = { :system => false }.merge(options)
36
+ line = line.strip
37
+ number_of_fields = 1
38
+ number_of_fields += line.start_with?('@') ? 1 : 5
39
+ number_of_fields += 1 if options[:system]
40
+ words = line.split(/\s+/, number_of_fields)
41
+ command = words.pop
42
+ uid = options[:system] ? words.pop : Process.uid
43
+ spec = words.join(' ')
44
+ schedule = Crontab::Schedule.new(spec)
45
+ new(schedule, command, uid)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,244 @@
1
+ require 'date'
2
+
3
+ class Crontab
4
+
5
+ # A class which represents schedules in crontab(5).
6
+ class Schedule
7
+
8
+ MONTH_NAMES = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
9
+ DAY_OF_WEEK_NAMES = %w(Sun Mon Tue Wed Thu Fri Sat)
10
+ DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
11
+
12
+ # Creates a cron job schedule.
13
+ #
14
+ # * <tt>spec</tt> schedule specifier defined in crontab(5).
15
+ # Both numeric and named spec are supported.
16
+ # * <tt>start</tt> the time *or* date this schedule begins.
17
+ #
18
+ # Supported named specs are:
19
+ # * <tt>@yearly</tt>, <tt>@annually</tt>
20
+ # * <tt>@monthly</tt>
21
+ # * <tt>@weekly</tt>
22
+ # * <tt>@daily</tt>, <tt>@midnight</tt>
23
+ # * <tt>@hourly</tt>
24
+ def initialize(spec, start=Time.now)
25
+ raise ArgumentError, 'empty spec' if spec == '' or spec.nil?
26
+ @start = ensure_time(start)
27
+ spec = spec.strip
28
+ if spec.start_with?('@')
29
+ parse_symbolic_spec(spec)
30
+ else
31
+ parse_spec(spec)
32
+ end
33
+ end
34
+
35
+ def ==(other)
36
+ self.minutes == other.minutes &&
37
+ self.hours == other.hours &&
38
+ self.day_of_months == other.day_of_months &&
39
+ self.months == other.months &&
40
+ self.day_of_months_given? == other.day_of_months_given? &&
41
+ self.day_of_weeks == other.day_of_weeks &&
42
+ self.day_of_weeks_given? == other.day_of_weeks_given? &&
43
+ self.start == other.start
44
+ end
45
+
46
+ def hash
47
+ [ minutes, hours, day_of_months, months, day_of_weeks, day_of_months_given?, day_of_weeks_given?, start ].map(&:hash).inject(:^)
48
+ end
49
+
50
+ attr_reader :minutes, :hours, :day_of_months, :months, :day_of_weeks, :start
51
+
52
+ # Changes the start timing of this schedule to <i>time_or_date</i>.
53
+ def start=(time_or_date)
54
+ @start = ensure_time(time_or_date)
55
+ end
56
+
57
+ # Creates new schedule which starts at the given <i>time_or_date</i>.
58
+ def from(time_or_date)
59
+ self.dup.tap {|new_schedule| new_schedule.start = ensure_time(time_or_date) }
60
+ end
61
+
62
+ def day_of_months_given?
63
+ !!@day_of_months_given # ensures boolean
64
+ end
65
+ protected :day_of_months_given?
66
+
67
+ def day_of_weeks_given?
68
+ !!@day_of_weeks_given # ensures boolean
69
+ end
70
+ protected :day_of_weeks_given?
71
+
72
+ # Iterates over timings specified in this schedule, starting at <tt>@start</tt>.
73
+ def each
74
+ return to_enum unless block_given?
75
+ year = @start.year
76
+ seeking = Hash.new {|h,k| h[k] = true }
77
+ loop do
78
+ @months.each do |month|
79
+ next if seeking[:month] and month < @start.month and @start.month <= @months.max
80
+ seeking[:month] = false
81
+ days = matching_days(year, month)
82
+ days.each do |day_of_month|
83
+ next if seeking[:day_of_month] and day_of_month < @start.day and @start.day <= days.max
84
+ seeking[:day_of_month] = false
85
+ @hours.each do |hour|
86
+ next if seeking[:hour] and hour < @start.hour and @start.hour <= @hours.max
87
+ seeking[:hour] = false
88
+ @minutes.each do |minute|
89
+ begin
90
+ t = Time.local(year, month, day_of_month, hour, minute)
91
+ rescue ArgumentError
92
+ raise StopIteration
93
+ end
94
+ yield(t) if @start <= t
95
+ end
96
+ end
97
+ end
98
+ end
99
+ year += 1
100
+ end
101
+ end
102
+
103
+ include Enumerable
104
+
105
+ # Iterates over timings from <tt>@start</tt> until given <i>time_or_date</i>.
106
+ def until(time_or_date)
107
+ time = ensure_time(time_or_date)
108
+ if block_given?
109
+ each do |t|
110
+ break if time < t
111
+ yield(t)
112
+ end
113
+ else
114
+ inject([]) do |timings, t|
115
+ break timings if time < t
116
+ timings << t
117
+ end
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def parse_spec(spec)
124
+ args = spec.split(/\s+/)
125
+ raise ArgumentError, 'wrong number of spec fields: %d' % args.size unless args.size == 5
126
+ @minutes = parse_spec_field(args[0], 0..59, :accept_name => false)
127
+ @hours = parse_spec_field(args[1], 0..23, :accept_name => false)
128
+ @day_of_months = parse_spec_field(args[2], 1..31, :accept_name => false)
129
+ @day_of_months_given = args[2] != '*'
130
+ @months = parse_spec_field(args[3], 1..12, :names => MONTH_NAMES, :base => 1, :accept_name => true)
131
+ @day_of_weeks = parse_spec_field(args[4], 0..6, :names => DAY_OF_WEEK_NAMES, :base => 0, :accept_name => true, :allow_end => true)
132
+ @day_of_weeks_given = args[4] != '*'
133
+ end
134
+
135
+ def parse_symbolic_spec(spec)
136
+ case spec
137
+ when '@yearly', '@annually'
138
+ parse_spec('0 0 1 1 *')
139
+ when '@monthly'
140
+ parse_spec('0 0 1 * *')
141
+ when '@weekly'
142
+ parse_spec('0 0 * * 0')
143
+ when '@daily', '@midnight'
144
+ parse_spec('0 0 * * *')
145
+ when '@hourly'
146
+ parse_spec('0 * * * *')
147
+ when '@reboot'
148
+ raise NotImplementedError, '@reboot is not supported'
149
+ else
150
+ raise ArgumentError, 'unknown crontab spec: %s' % spec
151
+ end
152
+ end
153
+
154
+ def parse_spec_field(spec, range, options={})
155
+ options = { :accept_name => true, :allow_end => false }.merge(options)
156
+ case spec
157
+ when '*'
158
+ range.to_a
159
+ when /\A\d+\z/
160
+ [ parse_number(spec, range, options) ]
161
+ when /\A[a-z]{3}\z/i
162
+ [ parse_name(spec, range, options) ]
163
+ when /\A(\d+)-(\d+)\z/i
164
+ parse_range($1, $2, range, options.merge(:accept_name => false))
165
+ when /,/
166
+ parse_list(spec.split(/,/), range, options.merge(:accept_name => false))
167
+ when /\A(\*|\d+-\d+)\/(\d+)\z/
168
+ parse_step($1, $2.to_i, range, options.merge(:accept_name => false))
169
+ else
170
+ raise ArgumentError, 'wrong spec format: %s' % spec
171
+ end.tap do |result|
172
+ if options[:allow_end] && result.include?(range.first) && result.include?(range.last.succ)
173
+ result.delete(range.last.succ)
174
+ end
175
+ end.sort.uniq.freeze
176
+ end
177
+
178
+ def parse_number(spec, range, options)
179
+ v = spec.to_i
180
+ return v if range.include?(v) or options[:allow_end] && range.last.succ == v
181
+ raise ArgumentError, 'argument out of range: %s' % spec
182
+ end
183
+
184
+ def parse_name(spec, range, options)
185
+ raise ArgumentError, 'names not allowed in this field: %s' % spec unless options[:names] and options[:base]
186
+ raise ArgumentError, 'names not allowed in this context: %s' % spec unless options[:accept_name]
187
+ v = options[:names].index {|name| name.downcase == spec.downcase }
188
+ base = options[:base]
189
+ return v + base if v && range.include?(v + base)
190
+ raise ArgumentError, 'argument out of range: %s' % spec
191
+ end
192
+
193
+ def parse_range(from, to, range, options)
194
+ from = parse_number(from, range, options[:allow_end])
195
+ to = parse_number(to, range, options[:allow_end])
196
+ raise ArgumentError, 'start is after or equal to end: %s' % spec unless from < to
197
+ (from..to).to_a
198
+ end
199
+
200
+ def parse_list(specs, range, options)
201
+ specs.map{|spec| parse_spec_field(spec, range, options) }.flatten
202
+ end
203
+
204
+ def parse_step(first, step, range, options)
205
+ v = parse_spec_field(first, range, options)
206
+ v.first.step(v.size == 1 ? range.last : v.last, step).to_a
207
+ end
208
+
209
+ def matching_days(year, month)
210
+ days = number_of_days(year, month)
211
+ (1..days).select do |day|
212
+ wday = day_of_week(year, month, day)
213
+ if day_of_months_given?
214
+ if day_of_weeks_given?
215
+ @day_of_months.include?(day) or @day_of_weeks.include?(wday)
216
+ else
217
+ @day_of_months.include?(day)
218
+ end
219
+ else
220
+ if day_of_weeks_given?
221
+ @day_of_weeks.include?(wday)
222
+ else
223
+ true
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ def ensure_time(time_or_date)
230
+ time_or_date.is_a?(Date) ? Time.local(time_or_date.year, time_or_date.month, time_or_date.day) : time_or_date
231
+ end
232
+
233
+ def number_of_days(year, month)
234
+ days = DAYS_IN_MONTH[month]
235
+ days += 1 if month == 2 && (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
236
+ days
237
+ end
238
+
239
+ def day_of_week(year, month, day)
240
+ Date.new(year, month, day).wday
241
+ end
242
+ end
243
+
244
+ end
@@ -0,0 +1,128 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+ require 'crontab/entry'
3
+ require 'crontab/schedule'
4
+
5
+ describe Crontab::Entry do
6
+ describe 'when instantiating' do
7
+ before :each do
8
+ @schedule = Crontab::Schedule.new('0 0 * * *') # @daily
9
+ @command = 'echo hello'
10
+ end
11
+
12
+ it 'should accpet Crontab::Schedule and command String' do
13
+ entry = Crontab::Entry.new(@schedule, @command)
14
+ entry.uid.should == Process.uid
15
+ end
16
+
17
+ it 'should accpet Crontab::Schedule, command String and user String' do
18
+ uid = Process.uid
19
+ user = Etc.getpwuid(uid)
20
+ entry = Crontab::Entry.new(@schedule, @command, user.name)
21
+ entry.uid.should == uid
22
+ end
23
+
24
+ it 'should accpet Crontab::Schedule, command String and user ID' do
25
+ uid = Process.uid
26
+ entry = Crontab::Entry.new(@schedule, @command, uid)
27
+ entry.uid.should == uid
28
+ end
29
+
30
+ it 'should reject invalid arguments' do
31
+ lambda { Crontab::Entry.new(@schedule, nil) }.should raise_error(ArgumentError)
32
+ lambda { Crontab::Entry.new(@schedule, Object.new) }.should raise_error(ArgumentError)
33
+ lambda { Crontab::Entry.new(@schedule) }.should raise_error(ArgumentError)
34
+ lambda { Crontab::Entry.new(nil, @command) }.should raise_error(ArgumentError)
35
+ lambda { Crontab::Entry.new(Object.new, @command) }.should raise_error(ArgumentError)
36
+ lambda { Crontab::Entry.new(@command) }.should raise_error(ArgumentError)
37
+ lambda { Crontab::Entry.new }.should raise_error(ArgumentError)
38
+ lambda { Crontab::Entry.new(nil) }.should raise_error(ArgumentError)
39
+ end
40
+
41
+ describe 'by parsing' do
42
+ it 'should parse crontab entry line' do
43
+ entry = Crontab::Entry.parse('0 0 * * * echo hello')
44
+ entry.schedule.from(@schedule.start).should == @schedule and
45
+ entry.command.should == @command and
46
+ entry.uid.should == Process.uid
47
+
48
+ entry = Crontab::Entry.parse('0 0 * * * echo hello', :system => false)
49
+ entry.schedule.from(@schedule.start).should == @schedule and
50
+ entry.command.should == @command and
51
+ entry.uid.should == Process.uid
52
+ end
53
+
54
+ it 'should parse crontab entry line with symbolic schedule' do
55
+ entry = Crontab::Entry.parse('@daily echo hello')
56
+ entry.schedule.from(@schedule.start).should == @schedule and
57
+ entry.command.should == @command and
58
+ entry.uid.should == Process.uid
59
+
60
+ entry = Crontab::Entry.parse('0 0 * * * echo hello', :system => false)
61
+ entry.schedule.from(@schedule.start).should == @schedule and
62
+ entry.command.should == @command and
63
+ entry.uid.should == Process.uid
64
+ end
65
+
66
+ it 'should parse system crontab entry line' do
67
+ entry = Crontab::Entry.parse('0 0 * * * root echo hello', :system => true)
68
+ entry.schedule.from(@schedule.start).should == @schedule and
69
+ entry.command.should == @command and
70
+ entry.uid.should == Etc.getpwnam('root').uid
71
+ end
72
+
73
+ it 'should parse system crontab entry line with symbolic schedule' do
74
+ entry = Crontab::Entry.parse('@daily root echo hello', :system => true)
75
+ entry.schedule.from(@schedule.start).should == @schedule and
76
+ entry.command.should == @command and
77
+ entry.uid.should == Etc.getpwnam('root').uid
78
+ end
79
+
80
+ it 'should ignore leading whitespaces' do
81
+ entry = Crontab::Entry.parse(' @daily echo hello')
82
+ entry.schedule.from(@schedule.start).should == @schedule and
83
+ entry.command.should == @command
84
+
85
+ entry = Crontab::Entry.parse(' 0 0 * * * echo hello')
86
+ entry.schedule.from(@schedule.start).should == @schedule and
87
+ entry.command.should == @command
88
+ end
89
+ end
90
+ end
91
+
92
+ describe 'when accessing' do
93
+ before :each do
94
+ @schedule = Crontab::Schedule.new('@hourly')
95
+ @other_schedule = Crontab::Schedule.new('@monthly')
96
+ @command = 'ehco hello'
97
+ @other_command = 'echo bonjour'
98
+ @entry = Crontab::Entry.new(@schedule, @command)
99
+ end
100
+
101
+ it 'should be read-accessible to schedule' do
102
+ @entry.schedule.should == @schedule and
103
+ lambda { @entry.schedule = @other_schedule }.should raise_error(NoMethodError)
104
+ @entry.schedule.should == @schedule
105
+ end
106
+
107
+ it 'should freeze schedule' do
108
+ @entry.schedule.should be_frozen
109
+ end
110
+
111
+ it 'should be read-accessible to command' do
112
+ @entry.command.should == @command and
113
+ lambda { @entry.command = @other_command }.should raise_error(NoMethodError)
114
+ @entry.command.should == @command
115
+ end
116
+
117
+ it 'should freeze command' do
118
+ @entry.command.should be_frozen
119
+ end
120
+
121
+ it 'should be read-accessible to uid' do
122
+ @entry.uid.should == Process.uid and
123
+ Process.uid.should_not == 0 and
124
+ lambda { @entry.uid = 0 }.should raise_error(NoMethodError)
125
+ @entry.uid.should == Process.uid
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,356 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+ require 'crontab/schedule'
3
+
4
+ describe Crontab::Schedule do
5
+ describe 'when parsing spec' do
6
+ it 'should accept spec with asterisks' do
7
+ schedule = Crontab::Schedule.new('* * * * *')
8
+ schedule.minutes.should == (0..59).to_a and
9
+ schedule.hours.should == (0..23).to_a and
10
+ schedule.day_of_months.should == (1..31).to_a and
11
+ schedule.months.should == (1..12).to_a and
12
+ schedule.day_of_weeks.should == (0..6).to_a
13
+ end
14
+
15
+ it 'should accept spec with simple numbers' do
16
+ schedule = Crontab::Schedule.new('0 0 1 1 0')
17
+ schedule.minutes.should == [0] and
18
+ schedule.hours.should == [0] and
19
+ schedule.day_of_months.should == [1] and
20
+ schedule.months.should == [1] and
21
+ schedule.day_of_weeks.should == [0]
22
+ end
23
+
24
+ it 'should accept spec with names(Jan, Sun etc.)' do
25
+ schedule = Crontab::Schedule.new('0 0 1 Jan sUn')
26
+ schedule.minutes.should == [0] and
27
+ schedule.hours.should == [0] and
28
+ schedule.day_of_months.should == [1] and
29
+ schedule.months.should == [1] and
30
+ schedule.day_of_weeks.should == [0]
31
+ end
32
+
33
+ it 'should accept spec with ranges(N-N)' do
34
+ schedule = Crontab::Schedule.new('0-1 0-1 1-2 1-2 0-1')
35
+ schedule.minutes.should == [0, 1] and
36
+ schedule.hours.should == [0, 1] and
37
+ schedule.day_of_months.should == [1, 2] and
38
+ schedule.months.should == [1, 2] and
39
+ schedule.day_of_weeks.should == [0, 1]
40
+ end
41
+
42
+ it 'should accept spec with lists(comma separaged ranges/numbers)' do
43
+ schedule = Crontab::Schedule.new('0,10,20 0,10,20 1,11,21 1,3,5 0,4,5,6')
44
+ schedule.minutes.should == [0, 10, 20] and
45
+ schedule.hours.should == [0, 10, 20] and
46
+ schedule.day_of_months.should == [1, 11, 21] and
47
+ schedule.months.should == [1, 3, 5] and
48
+ schedule.day_of_weeks.should == [0, 4, 5, 6]
49
+
50
+ schedule = Crontab::Schedule.new('0,1-5,10 0,1-5,10 1,2-5,10 1,2-4,10 0,1-3,7')
51
+ schedule.minutes.should == [0, 1, 2, 3, 4, 5, 10] and
52
+ schedule.hours.should == [0, 1, 2, 3, 4, 5, 10] and
53
+ schedule.day_of_months.should == [1, 2, 3, 4, 5, 10] and
54
+ schedule.months.should == [1, 2, 3, 4, 10] and
55
+ schedule.day_of_weeks.should == [0, 1, 2, 3]
56
+ end
57
+
58
+ it 'should accept spec with steps(range/STEP or */STEP)' do
59
+ schedule = Crontab::Schedule.new('0-30/10 1-20/5 5-30/7 4-10/2 1-5/2')
60
+ schedule.minutes.should == [0, 10, 20, 30] and
61
+ schedule.hours.should == [1, 6, 11, 16 ] and
62
+ schedule.day_of_months.should == [5, 12, 19, 26 ] and
63
+ schedule.months.should == [4, 6, 8, 10] and
64
+ schedule.day_of_weeks.should == [1, 3, 5]
65
+
66
+ schedule = Crontab::Schedule.new('*/25 */5 */10 */4 */2')
67
+ schedule.minutes.should == [0, 25, 50] and
68
+ schedule.hours.should == [0, 5, 10, 15, 20] and
69
+ schedule.day_of_months.should == [1, 11, 21, 31] and
70
+ schedule.months.should == [1, 5, 9] and
71
+ schedule.day_of_weeks.should == [0, 2, 4, 6]
72
+ end
73
+
74
+ it 'should reject spec with incorrect number of fields' do
75
+ lambda { Crontab::Schedule.new }.should raise_error(ArgumentError)
76
+ lambda { Crontab::Schedule.new(nil) }.should raise_error(ArgumentError)
77
+ lambda { Crontab::Schedule.new('') }.should raise_error(ArgumentError)
78
+ lambda { Crontab::Schedule.new('x') }.should raise_error(ArgumentError)
79
+ lambda { Crontab::Schedule.new('* * * *') }.should raise_error(ArgumentError)
80
+ lambda { Crontab::Schedule.new('* * * * * *') }.should raise_error(ArgumentError)
81
+ end
82
+
83
+ it 'should reject spec with incorrect values' do
84
+ lambda { Crontab::Schedule.new('x 0 1 0 0') }.should raise_error(ArgumentError)
85
+ lambda { Crontab::Schedule.new('-1 0 1 1 0') }.should raise_error(ArgumentError)
86
+ lambda { Crontab::Schedule.new('60 0 1 1 0') }.should raise_error(ArgumentError)
87
+ lambda { Crontab::Schedule.new('0 24 1 1 0') }.should raise_error(ArgumentError)
88
+ lambda { Crontab::Schedule.new('0 0 32 1 0') }.should raise_error(ArgumentError)
89
+ lambda { Crontab::Schedule.new('0 0 1 13 0') }.should raise_error(ArgumentError)
90
+ lambda { Crontab::Schedule.new('0 0 1 1 8') }.should raise_error(ArgumentError)
91
+ lambda { Crontab::Schedule.new('0 0 1 Mon 8') }.should raise_error(ArgumentError)
92
+ lambda { Crontab::Schedule.new('Jan Sun 1 1 8') }.should raise_error(ArgumentError)
93
+ end
94
+
95
+ it 'should reject spec with names in lists and ranges' do
96
+ lambda { Crontab::Schedule.new('0 0 1 Jan-Feb *') }.should raise_error(ArgumentError)
97
+ lambda { Crontab::Schedule.new('0 0 1 Jan,Feb *') }.should raise_error(ArgumentError)
98
+ lambda { Crontab::Schedule.new('0 0 1 * Sun-Fri') }.should raise_error(ArgumentError)
99
+ lambda { Crontab::Schedule.new('0 0 1 * Sun,Fri') }.should raise_error(ArgumentError)
100
+ end
101
+
102
+ it 'should reject spec with simple numbers with step' do
103
+ lambda { Crontab::Schedule.new('0/2 0 1 1 0') }.should raise_error(ArgumentError)
104
+ lambda { Crontab::Schedule.new('0 0/2 1 1 0') }.should raise_error(ArgumentError)
105
+ lambda { Crontab::Schedule.new('0 0 1/2 1 0') }.should raise_error(ArgumentError)
106
+ lambda { Crontab::Schedule.new('0 0 1 1/2 0') }.should raise_error(ArgumentError)
107
+ lambda { Crontab::Schedule.new('0 0 1 1 0/2') }.should raise_error(ArgumentError)
108
+ end
109
+
110
+ it 'should accept symblic specs' do
111
+ start = Time.now
112
+ Crontab::Schedule.new('@yearly', start).should == Crontab::Schedule.new('0 0 1 1 *', start) and
113
+ Crontab::Schedule.new('@annually', start).should == Crontab::Schedule.new('0 0 1 1 *', start) and
114
+ Crontab::Schedule.new('@monthly', start).should == Crontab::Schedule.new('0 0 1 * *', start) and
115
+ Crontab::Schedule.new('@weekly', start).should == Crontab::Schedule.new('0 0 * * 0', start) and
116
+ Crontab::Schedule.new('@daily', start).should == Crontab::Schedule.new('0 0 * * *', start) and
117
+ Crontab::Schedule.new('@midnight', start).should == Crontab::Schedule.new('0 0 * * *', start) and
118
+ Crontab::Schedule.new('@hourly', start).should == Crontab::Schedule.new('0 * * * *', start)
119
+ end
120
+
121
+ it 'should reject @reboot' do
122
+ lambda { Crontab::Schedule.new('@reboot') }.should raise_error(NotImplementedError)
123
+ end
124
+
125
+ it 'should reject unknown symbolic spec' do
126
+ lambda { Crontab::Schedule.new('@unknown') }.should raise_error(ArgumentError)
127
+ end
128
+
129
+ it 'should ignore leading/trailing whitespaces' do
130
+ lambda {
131
+ Crontab::Schedule.new(' @daily')
132
+ Crontab::Schedule.new('@daily ')
133
+ Crontab::Schedule.new(' * * * * *')
134
+ Crontab::Schedule.new('* * * * * ')
135
+ }.should_not raise_error
136
+ end
137
+ end
138
+
139
+ describe 'when accessing' do
140
+ it 'should accept Time as start parameter' do
141
+ start = Time.local(2009, 3, 25, 10, 20, 30)
142
+ lambda {
143
+ schedule = Crontab::Schedule.new('* * * * *', start)
144
+ schedule.start.should == start
145
+ }.should_not raise_error
146
+ end
147
+
148
+ it 'should accept Date as start parameter' do
149
+ start = Date.new(2009, 3, 25)
150
+ lambda {
151
+ schedule = Crontab::Schedule.new('* * * * *', start)
152
+ schedule.start.should == Time.local(start.year, start.month, start.day)
153
+ }.should_not raise_error
154
+ end
155
+
156
+ it 'should set start to given Time' do
157
+ initial_start = Time.local(2009, 3, 25, 10, 20, 30)
158
+ schedule = Crontab::Schedule.new('* * * * *', initial_start)
159
+ schedule.start.should == initial_start and
160
+
161
+ new_start = Time.local(2009, 4, 25, 11, 21, 31)
162
+ schedule.start = new_start
163
+ schedule.start.should == new_start
164
+ end
165
+
166
+ it 'should set start to given Date' do
167
+ initial_start = Time.local(2009, 3, 25, 10, 20, 30)
168
+ schedule = Crontab::Schedule.new('* * * * *', initial_start)
169
+ schedule.start.should == initial_start and
170
+
171
+ new_start = Date.new(2009, 4, 25)
172
+ new_start_as_time = Time.local(new_start.year, new_start.month, new_start.day)
173
+ schedule.start = new_start
174
+ schedule.start.should == new_start_as_time
175
+ end
176
+ end
177
+
178
+ describe 'when duplicating' do
179
+ it 'should return a new schedule starting at given Time' do
180
+ initial_start = Time.local(2009, 3, 25, 10, 20, 30)
181
+ schedule = Crontab::Schedule.new('* * * * *', initial_start)
182
+ new_start = Time.local(2009, 4, 25, 11, 21, 31)
183
+ new_schedule = schedule.from(new_start)
184
+
185
+ new_schedule.object_id.should_not == schedule.object_id and
186
+ new_schedule.hash.should_not == schedule.hash and
187
+ new_schedule.should_not == schedule and
188
+
189
+ new_schedule.start.should == new_start and
190
+ new_schedule.minutes.should == schedule.minutes and
191
+ new_schedule.hours.should == schedule.hours and
192
+ new_schedule.day_of_months.should == schedule.day_of_months and
193
+ new_schedule.months.should == schedule.months and
194
+ new_schedule.day_of_weeks.should == schedule.day_of_weeks and
195
+ new_schedule.send(:day_of_months_given?).should == schedule.send(:day_of_months_given?) and
196
+ new_schedule.send(:day_of_weeks_given?).should == schedule.send(:day_of_weeks_given?)
197
+ end
198
+
199
+ it 'should return a new schedule starting at given Date' do
200
+ initial_start = Time.local(2009, 3, 25, 10, 20, 30)
201
+ schedule = Crontab::Schedule.new('* * * * *', initial_start)
202
+ new_start = Date.new(2009, 4, 25)
203
+ new_start_as_time = Time.local(new_start.year, new_start.month, new_start.day)
204
+ new_schedule = schedule.from(new_start)
205
+
206
+ new_schedule.object_id.should_not == schedule.object_id and
207
+ new_schedule.hash.should_not == schedule.hash and
208
+ new_schedule.should_not == schedule and
209
+
210
+ new_schedule.start.should == new_start_as_time and
211
+ new_schedule.minutes.should == schedule.minutes and
212
+ new_schedule.hours.should == schedule.hours and
213
+ new_schedule.day_of_months.should == schedule.day_of_months and
214
+ new_schedule.months.should == schedule.months and
215
+ new_schedule.day_of_weeks.should == schedule.day_of_weeks and
216
+ new_schedule.send(:day_of_months_given?).should == schedule.send(:day_of_months_given?) and
217
+ new_schedule.send(:day_of_weeks_given?).should == schedule.send(:day_of_weeks_given?)
218
+ end
219
+ end
220
+
221
+ describe 'when comparing' do
222
+ it 'should equal to other if and only if both are created from equivalent spec and start time' do
223
+ start = Time.now
224
+ Crontab::Schedule.new('0 0 1 1 0', start).should == Crontab::Schedule.new('0 0 1 1 0', start) and
225
+ Crontab::Schedule.new('0 0 1 1 0', start).should == Crontab::Schedule.new('0 0 1 Jan 0', start) and
226
+ Crontab::Schedule.new('0 0 1 1 0', start).should == Crontab::Schedule.new('0 0 1 1 Sun', start) and
227
+
228
+ Crontab::Schedule.new('0 0 1 1 0', start).should_not == Crontab::Schedule.new('1 0 1 1 0', start) and
229
+ Crontab::Schedule.new('0 0 1 1 0', start).should_not == Crontab::Schedule.new('0 1 1 1 0', start) and
230
+ Crontab::Schedule.new('0 0 1 1 0', start).should_not == Crontab::Schedule.new('0 0 2 1 0', start) and
231
+ Crontab::Schedule.new('0 0 1 1 0', start).should_not == Crontab::Schedule.new('0 0 1 2 0', start) and
232
+ Crontab::Schedule.new('0 0 1 1 0', start).should_not == Crontab::Schedule.new('0 0 1 1 1', start) and
233
+
234
+ Crontab::Schedule.new('0 0 1-5 1 0', start).should == Crontab::Schedule.new('0 0 1,2,3,4,5 1 0', start) and
235
+ Crontab::Schedule.new('0 0 1 1 0-4', start).should == Crontab::Schedule.new('0 0 1 1 0,1,2,3,4', start)
236
+ end
237
+ end
238
+
239
+ describe 'when enumerating' do
240
+ it 'should return given number of timings' do
241
+ schedule = Crontab::Schedule.new('* * * * *', Time.local(2009, 3, 8, 4, 30, 15))
242
+ expected = [
243
+ Time.local(2009, 3, 8, 4, 31),
244
+ Time.local(2009, 3, 8, 4, 32),
245
+ Time.local(2009, 3, 8, 4, 33),
246
+ Time.local(2009, 3, 8, 4, 34),
247
+ Time.local(2009, 3, 8, 4, 35),
248
+ ]
249
+ schedule.first(expected.size).should == expected and
250
+
251
+ schedule = Crontab::Schedule.new('5 10 3-5,10 * *', Time.local(2009, 3, 8, 10, 30, 15))
252
+ expected = [
253
+ Time.local(2009, 3, 10, 10, 5),
254
+ Time.local(2009, 4, 3, 10, 5),
255
+ Time.local(2009, 4, 4, 10, 5),
256
+ Time.local(2009, 4, 5, 10, 5),
257
+ Time.local(2009, 4, 10, 10, 5),
258
+ Time.local(2009, 5, 3, 10, 5),
259
+ Time.local(2009, 5, 4, 10, 5),
260
+ Time.local(2009, 5, 5, 10, 5),
261
+ Time.local(2009, 5, 10, 10, 5),
262
+ Time.local(2009, 6, 3, 10, 5),
263
+ ]
264
+ schedule.first(expected.size).should == expected
265
+ end
266
+
267
+ it 'should return timings matching day of month or day of week' do
268
+ schedule = Crontab::Schedule.new('0 0 1,10,11 3 0,1', Time.local(2009, 3, 8, 10, 30, 15))
269
+ expected = [
270
+ Time.local(2009, 3, 9, 0, 0),
271
+ Time.local(2009, 3, 10, 0, 0),
272
+ Time.local(2009, 3, 11, 0, 0),
273
+ Time.local(2009, 3, 15, 0, 0),
274
+ Time.local(2009, 3, 16, 0, 0),
275
+ Time.local(2009, 3, 22, 0, 0),
276
+ Time.local(2009, 3, 23, 0, 0),
277
+ Time.local(2009, 3, 29, 0, 0),
278
+ Time.local(2009, 3, 30, 0, 0),
279
+ ]
280
+ schedule.first(expected.size).should == expected and
281
+
282
+ schedule = Crontab::Schedule.new('0 0 1,10,11 3 *', Time.local(2009, 3, 8, 10, 30, 15))
283
+ expected = [
284
+ Time.local(2009, 3, 10, 0, 0),
285
+ Time.local(2009, 3, 11, 0, 0),
286
+ ]
287
+ schedule.first(expected.size).should == expected and
288
+
289
+ schedule = Crontab::Schedule.new('0 0 * 3 0,1', Time.local(2009, 3, 8, 10, 30, 15))
290
+ expected = [
291
+ Time.local(2009, 3, 9, 0, 0),
292
+ Time.local(2009, 3, 15, 0, 0),
293
+ Time.local(2009, 3, 16, 0, 0),
294
+ Time.local(2009, 3, 22, 0, 0),
295
+ Time.local(2009, 3, 23, 0, 0),
296
+ Time.local(2009, 3, 29, 0, 0),
297
+ Time.local(2009, 3, 30, 0, 0),
298
+ ]
299
+ schedule.first(expected.size).should == expected
300
+ end
301
+
302
+ it 'should return timings until given time' do
303
+ schedule = Crontab::Schedule.new('* * * * *', Time.local(2009, 3, 8, 4, 30, 15))
304
+ expected = [
305
+ Time.local(2009, 3, 8, 4, 31),
306
+ Time.local(2009, 3, 8, 4, 32),
307
+ Time.local(2009, 3, 8, 4, 33),
308
+ Time.local(2009, 3, 8, 4, 34),
309
+ Time.local(2009, 3, 8, 4, 35),
310
+ Time.local(2009, 3, 8, 4, 36),
311
+ Time.local(2009, 3, 8, 4, 37),
312
+ Time.local(2009, 3, 8, 4, 38),
313
+ Time.local(2009, 3, 8, 4, 39),
314
+ Time.local(2009, 3, 8, 4, 40),
315
+ ]
316
+ schedule.until(Time.local(2009, 3, 8, 4, 40, 10)).should == expected and
317
+
318
+ result = []
319
+ schedule.until(Time.local(2009, 3, 8, 4, 40, 10)) do |t|
320
+ result << t
321
+ end
322
+ result.should == expected and
323
+
324
+ schedule = Crontab::Schedule.new('*/20 0 21 * *', Time.local(2009, 3, 8, 4, 30, 15))
325
+ expected = [
326
+ Time.local(2009, 3, 21, 0, 0),
327
+ Time.local(2009, 3, 21, 0, 20),
328
+ Time.local(2009, 3, 21, 0, 40),
329
+ Time.local(2009, 4, 21, 0, 0),
330
+ Time.local(2009, 4, 21, 0, 20),
331
+ Time.local(2009, 4, 21, 0, 40),
332
+ Time.local(2009, 5, 21, 0, 00),
333
+ Time.local(2009, 5, 21, 0, 20),
334
+ Time.local(2009, 5, 21, 0, 40),
335
+ ]
336
+ schedule.until(Time.local(2009, 6, 1)).should == expected
337
+ end
338
+
339
+ it 'should exclude invalid day of month' do
340
+ schedule = Crontab::Schedule.new('5 0 27,29,31 * *', Time.local(2009, 1, 1, 10, 30, 15))
341
+ expected = [
342
+ Time.local(2009, 1, 27, 0, 5),
343
+ Time.local(2009, 1, 29, 0, 5),
344
+ Time.local(2009, 1, 31, 0, 5),
345
+ Time.local(2009, 2, 27, 0, 5),
346
+ # Time.local(2009, 2, 29, 0, 5), # should exclude this!
347
+ # Time.local(2009, 2, 31, 0, 5), # should exclude this!
348
+ Time.local(2009, 3, 27, 0, 5),
349
+ Time.local(2009, 3, 29, 0, 5),
350
+ Time.local(2009, 3, 31, 0, 5),
351
+ ]
352
+
353
+ schedule.first(expected.size).should == expected
354
+ end
355
+ end
356
+ end
@@ -0,0 +1,37 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+ require 'crontab'
3
+
4
+ describe Crontab do
5
+ describe 'parsing' do
6
+ it 'should recognize entries' do
7
+ crontab = Crontab.parse(File.read('spec/data/crontab.txt'))
8
+ crontab.entries.size.should == 5 and
9
+ crontab.entries.should be_frozen
10
+ crontab.entries[0].tap do |e|
11
+ e.schedule.minutes.should == [8] and
12
+ e.schedule.hours.should == [3] and
13
+ e.schedule.day_of_months.should == (1..31).to_a and
14
+ e.schedule.months.should == (1..12).to_a and
15
+ e.schedule.day_of_weeks.should == [6]
16
+ end
17
+ end
18
+
19
+ it 'should recognize variables assignments' do
20
+ crontab = Crontab.parse(File.read('spec/data/crontab.txt'))
21
+ crontab.env.should == {
22
+ 'SHELL' => '/bin/zsh',
23
+ 'MAIL_TO' => 'nobody@example.com',
24
+ 'FOO' => 'foo',
25
+ 'BAR' => 'bar',
26
+ 'BAZ' => 'baz'
27
+ }
28
+ end
29
+ end
30
+ end
31
+ # 8 3 * * 6 run-parts $HOME/.cron.d/weekly
32
+ # 50 2 * * * run-parts $HOME/.cron.d/daily
33
+ # 3 * * * * run-parts $HOME/.cron.d/hourly
34
+ #
35
+ # */5 * * * * curl -sI http://example.com >/dev/null
36
+ #
37
+ # * * * * * awk 'BEGIN{ srand(); exit(rand() * 24 * 60.0 < 12.0 ? 0 : 1); }' && fortune
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sakuro-crontab
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - OZAWA Sakuro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-16 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: github@2238club.org
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/crontab.rb
26
+ - lib/crontab/entry.rb
27
+ - lib/crontab/schedule.rb
28
+ - spec/crontab_spec.rb
29
+ - spec/crontab/entry_spec.rb
30
+ - spec/crontab/schedule_spec.rb
31
+ - crontab.gemspec
32
+ - Rakefile
33
+ - README.rdoc
34
+ has_rdoc: true
35
+ homepage: http://github.com/sakuro/crontab
36
+ post_install_message:
37
+ rdoc_options: []
38
+
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ requirements: []
54
+
55
+ rubyforge_project: n/a
56
+ rubygems_version: 1.2.0
57
+ signing_key:
58
+ specification_version: 2
59
+ summary: A crontab(5) entry parser
60
+ test_files: []
61
+