cronman 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # What is this?
2
+
3
+ A crontab(5) parser.
4
+
5
+ # How to install?
6
+
7
+ In your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'cronman', 'git: git@github.com:renatomoya/cronman.git'
11
+ ```
12
+
13
+ # How to use?
14
+
15
+ ```ruby
16
+ crontab = Cronman.parse(src)
17
+
18
+ from_date = Date.parse('2009-10-10')
19
+ to_date = Date.parse('2009-10-20')
20
+
21
+ crontab.entries.each do |entry|
22
+ # Access the cron definition
23
+ puts entry.cron_definition
24
+
25
+ # Access the cron definition translation
26
+ puts entry.translation.join(' ')
27
+
28
+ # Access the command
29
+ puts entry.command
30
+
31
+ # Access parsed cron definition
32
+ puts entry.schedule
33
+
34
+ # Filtering is easy!
35
+ e.schedule.from(from_date).until(to_date) do |timing|
36
+ puts timing
37
+ end
38
+ end
39
+ ```
40
+
41
+ Most of this code is not mine. I borrowed it from [@sakuro](https://github.com/sakuro) gem [crontab](https://github.com/sakuro/crontab). I added a few missing features for a personal project called cronman.
data/Rakefile ADDED
@@ -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
data/cronman.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'cronman'
3
+ s.version = '0.0.1'
4
+ s.author = 'Renato Moya'
5
+ s.email = 'imexto@gmail.com'
6
+ s.homepage = 'http://github.com/renatomoya/cronman'
7
+ s.platform = Gem::Platform::RUBY
8
+ s.summary = 'A crontab(5) entry parser'
9
+ s.files = [
10
+ 'lib/cronman.rb',
11
+ 'lib/cronman/entry.rb',
12
+ 'lib/cronman/schedule.rb',
13
+ 'spec/cronman_spec.rb',
14
+ 'spec/cronman/entry_spec.rb',
15
+ 'spec/cronman/schedule_spec.rb',
16
+ 'cronman.gemspec',
17
+ 'Rakefile',
18
+ 'README.md'
19
+ ]
20
+ s.add_runtime_dependency('cron2english', ['0.1.3'])
21
+ s.has_rdoc = true
22
+ s.rubyforge_project = 'n/a'
23
+ end
@@ -0,0 +1,54 @@
1
+ require 'cron2english'
2
+
3
+ class Cronman
4
+ # A class which represents a job line in crontab(5).
5
+ class Entry
6
+ # Creates a crontab(5) entry.
7
+ #
8
+ # * <tt>schedule</tt> A Cronman::Schedule instance.
9
+ # * <tt>command</tt>
10
+ # * <tt>uid</tt>
11
+ def initialize(schedule, command, cron_definition, uid=nil)
12
+ raise ArgumentError, 'invalid schedule' unless schedule.is_a? Schedule
13
+ raise ArgumentError, 'invalid command' unless command.is_a? String
14
+
15
+ @schedule = schedule.freeze
16
+ @command = command.freeze
17
+ @cron_definition = cron_definition.freeze
18
+ @translation = Cron2English.parse(@cron_definition).freeze
19
+ @uid =
20
+ case uid
21
+ when String
22
+ Etc.getpwnam(uid).uid
23
+ when Integer
24
+ uid
25
+ when nil
26
+ Process.uid
27
+ else
28
+ raise ArgumentError, 'invalid uid'
29
+ end
30
+ end
31
+
32
+ attr_reader :schedule, :command, :cron_definition, :translation, :uid
33
+ class << self
34
+ # Parses a string line in crontab(5) job format.
35
+ #
36
+ # * <tt>options[:system]</tt> when true system wide crontab is assumed
37
+ # and <tt>@uid</tt> is extracted from <i>line</i>.
38
+ def parse(line, options={})
39
+ options = { :system => false }.merge(options)
40
+ line = line.strip
41
+ number_of_fields = 1
42
+ number_of_fields += line.start_with?('@') ? 1 : 5
43
+ number_of_fields += 1 if options[:system]
44
+ words = line.split(/\s+/, number_of_fields)
45
+ command = words.pop
46
+ uid = options[:system] ? words.pop : Process.uid
47
+ cron_definition = words.first(5).join(' ')
48
+ spec = words.join(' ')
49
+ schedule = Cronman::Schedule.new(spec)
50
+ new(schedule, command, cron_definition, uid)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,242 @@
1
+ require 'date'
2
+
3
+ class Cronman
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
+
66
+ def day_of_weeks_given?
67
+ !!@day_of_weeks_given # ensures boolean
68
+ end
69
+
70
+ # Iterates over timings specified in this schedule, starting at <tt>@start</tt>.
71
+ def each
72
+ return to_enum unless block_given?
73
+ year = @start.year
74
+ seeking = Hash.new {|h,k| h[k] = true }
75
+ loop do
76
+ @months.each do |month|
77
+ next if seeking[:month] and month < @start.month and @start.month <= @months.max
78
+ seeking[:month] = false
79
+ days = matching_days(year, month)
80
+ days.each do |day_of_month|
81
+ next if seeking[:day_of_month] and day_of_month < @start.day and @start.day <= days.max
82
+ seeking[:day_of_month] = false
83
+ @hours.each do |hour|
84
+ next if seeking[:hour] and hour < @start.hour and @start.hour <= @hours.max
85
+ seeking[:hour] = false
86
+ @minutes.each do |minute|
87
+ begin
88
+ t = Time.local(year, month, day_of_month, hour, minute)
89
+ rescue ArgumentError
90
+ raise StopIteration
91
+ end
92
+ yield(t) if @start <= t
93
+ end
94
+ end
95
+ end
96
+ end
97
+ year += 1
98
+ end
99
+ end
100
+
101
+ include Enumerable
102
+
103
+ # Iterates over timings from <tt>@start</tt> until given <i>time_or_date</i>.
104
+ def until(time_or_date)
105
+ time = ensure_time(time_or_date)
106
+ if block_given?
107
+ each do |t|
108
+ break if time < t
109
+ yield(t)
110
+ end
111
+ else
112
+ inject([]) do |timings, t|
113
+ break timings if time < t
114
+ timings << t
115
+ end
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def parse_spec(spec)
122
+ args = spec.split(/\s+/)
123
+ raise ArgumentError, 'wrong number of spec fields: %d' % args.size unless args.size == 5
124
+ @minutes = parse_spec_field(args[0], 0..59, :accept_name => false)
125
+ @hours = parse_spec_field(args[1], 0..23, :accept_name => false)
126
+ @day_of_months = parse_spec_field(args[2], 1..31, :accept_name => false)
127
+ @day_of_months_given = args[2] != '*'
128
+ @months = parse_spec_field(args[3], 1..12, :names => MONTH_NAMES, :base => 1, :accept_name => true)
129
+ @day_of_weeks = parse_spec_field(args[4], 0..6, :names => DAY_OF_WEEK_NAMES, :base => 0, :accept_name => true, :allow_end => true)
130
+ @day_of_weeks_given = args[4] != '*'
131
+ end
132
+
133
+ def parse_symbolic_spec(spec)
134
+ case spec
135
+ when '@yearly', '@annually'
136
+ parse_spec('0 0 1 1 *')
137
+ when '@monthly'
138
+ parse_spec('0 0 1 * *')
139
+ when '@weekly'
140
+ parse_spec('0 0 * * 0')
141
+ when '@daily', '@midnight'
142
+ parse_spec('0 0 * * *')
143
+ when '@hourly'
144
+ parse_spec('0 * * * *')
145
+ when '@reboot'
146
+ raise NotImplementedError, '@reboot is not supported'
147
+ else
148
+ raise ArgumentError, 'unknown crontab spec: %s' % spec
149
+ end
150
+ end
151
+
152
+ def parse_spec_field(spec, range, options={})
153
+ options = { :accept_name => true, :allow_end => false }.merge(options)
154
+ case spec
155
+ when '*'
156
+ range.to_a
157
+ when /\A\d+\z/
158
+ [ parse_number(spec, range, options) ]
159
+ when /\A[a-z]{3}\z/i
160
+ [ parse_name(spec, range, options) ]
161
+ when /\A(\d+)-(\d+)\z/i
162
+ parse_range($1, $2, range, options.merge(:accept_name => false))
163
+ when /,/
164
+ parse_list(spec.split(/,/), range, options.merge(:accept_name => false))
165
+ when /\A(\*|\d+-\d+)\/(\d+)\z/
166
+ parse_step($1, $2.to_i, range, options.merge(:accept_name => false))
167
+ else
168
+ raise ArgumentError, 'wrong spec format: %s' % spec
169
+ end.tap do |result|
170
+ if options[:allow_end] && result.include?(range.first) && result.include?(range.last.succ)
171
+ result.delete(range.last.succ)
172
+ end
173
+ end.sort.uniq.freeze
174
+ end
175
+
176
+ def parse_number(spec, range, options)
177
+ v = spec.to_i
178
+ return v if range.include?(v) or options[:allow_end] && range.last.succ == v
179
+ raise ArgumentError, 'argument out of range: %s' % spec
180
+ end
181
+
182
+ def parse_name(spec, range, options)
183
+ raise ArgumentError, 'names not allowed in this field: %s' % spec unless options[:names] and options[:base]
184
+ raise ArgumentError, 'names not allowed in this context: %s' % spec unless options[:accept_name]
185
+ v = options[:names].index {|name| name.downcase == spec.downcase }
186
+ base = options[:base]
187
+ return v + base if v && range.include?(v + base)
188
+ raise ArgumentError, 'argument out of range: %s' % spec
189
+ end
190
+
191
+ def parse_range(from, to, range, options)
192
+ from = parse_number(from, range, options[:allow_end])
193
+ to = parse_number(to, range, options[:allow_end])
194
+ raise ArgumentError, 'start is after or equal to end: %s' % spec unless from < to
195
+ (from..to).to_a
196
+ end
197
+
198
+ def parse_list(specs, range, options)
199
+ specs.map{|spec| parse_spec_field(spec, range, options) }.flatten
200
+ end
201
+
202
+ def parse_step(first, step, range, options)
203
+ v = parse_spec_field(first, range, options)
204
+ v.first.step(v.size == 1 ? range.last : v.last, step).to_a
205
+ end
206
+
207
+ def matching_days(year, month)
208
+ days = number_of_days(year, month)
209
+ (1..days).select do |day|
210
+ wday = day_of_week(year, month, day)
211
+ if day_of_months_given?
212
+ if day_of_weeks_given?
213
+ @day_of_months.include?(day) or @day_of_weeks.include?(wday)
214
+ else
215
+ @day_of_months.include?(day)
216
+ end
217
+ else
218
+ if day_of_weeks_given?
219
+ @day_of_weeks.include?(wday)
220
+ else
221
+ true
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ def ensure_time(time_or_date)
228
+ time_or_date.is_a?(Date) ? Time.local(time_or_date.year, time_or_date.month, time_or_date.day) : time_or_date
229
+ end
230
+
231
+ def number_of_days(year, month)
232
+ days = DAYS_IN_MONTH[month]
233
+ days += 1 if month == 2 && (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
234
+ days
235
+ end
236
+
237
+ def day_of_week(year, month, day)
238
+ Date.new(year, month, day).wday
239
+ end
240
+ end
241
+
242
+ end
data/lib/cronman.rb ADDED
@@ -0,0 +1,34 @@
1
+ require 'cronman/schedule'
2
+ require 'cronman/entry'
3
+
4
+ # A class which represents crontab(5) content.
5
+ class Cronman
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 << Cronman::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,153 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+ require 'cronman/entry'
3
+ require 'cronman/schedule'
4
+
5
+ describe Cronman::Entry do
6
+ describe 'when instantiating' do
7
+ before :each do
8
+ @cron_definition = '0 0 * * *'
9
+ @schedule = Cronman::Schedule.new(@cron_definition) # @daily
10
+ @command = 'echo hello'
11
+ end
12
+
13
+ it 'should accpet Cronman::Schedule and command String' do
14
+ entry = Cronman::Entry.new(@schedule, @command, @cron_definition)
15
+ entry.uid.should == Process.uid
16
+ end
17
+
18
+ it 'should accpet Cronman::Schedule, command String and user String' do
19
+ uid = Process.uid
20
+ user = Etc.getpwuid(uid)
21
+ entry = Cronman::Entry.new(@schedule, @command, @cron_definition, user.name)
22
+ entry.uid.should == uid
23
+ end
24
+
25
+ it 'should accpet Cronman::Schedule, command String and user ID' do
26
+ uid = Process.uid
27
+ entry = Cronman::Entry.new(@schedule, @command, @cron_definition, uid)
28
+ entry.uid.should == uid
29
+ end
30
+
31
+ it 'should reject invalid arguments' do
32
+ lambda { Cronman::Entry.new(@schedule, nil) }.should raise_error(ArgumentError)
33
+ lambda { Cronman::Entry.new(@schedule, Object.new) }.should raise_error(ArgumentError)
34
+ lambda { Cronman::Entry.new(@schedule) }.should raise_error(ArgumentError)
35
+ lambda { Cronman::Entry.new(nil, @command) }.should raise_error(ArgumentError)
36
+ lambda { Cronman::Entry.new(Object.new, @command) }.should raise_error(ArgumentError)
37
+ lambda { Cronman::Entry.new(@command) }.should raise_error(ArgumentError)
38
+ lambda { Cronman::Entry.new }.should raise_error(ArgumentError)
39
+ lambda { Cronman::Entry.new(nil) }.should raise_error(ArgumentError)
40
+ end
41
+
42
+ describe 'by parsing' do
43
+ it 'should parse crontab entry line' do
44
+ entry = Cronman::Entry.parse('0 0 * * * echo hello')
45
+ entry.schedule.from(@schedule.start).should == @schedule and
46
+ entry.command.should == @command and
47
+ entry.uid.should == Process.uid
48
+
49
+ entry = Cronman::Entry.parse('0 0 * * * echo hello', :system => false)
50
+ entry.schedule.from(@schedule.start).should == @schedule and
51
+ entry.command.should == @command and
52
+ entry.uid.should == Process.uid
53
+ end
54
+
55
+ it 'should parse crontab entry line with symbolic schedule' do
56
+ entry = Cronman::Entry.parse('@daily echo hello')
57
+ entry.schedule.from(@schedule.start).should == @schedule and
58
+ entry.command.should == @command and
59
+ entry.uid.should == Process.uid
60
+
61
+ entry = Cronman::Entry.parse('0 0 * * * echo hello', :system => false)
62
+ entry.schedule.from(@schedule.start).should == @schedule and
63
+ entry.command.should == @command and
64
+ entry.uid.should == Process.uid
65
+ end
66
+
67
+ it 'should parse system crontab entry line' do
68
+ entry = Cronman::Entry.parse('0 0 * * * root echo hello', :system => true)
69
+ entry.schedule.from(@schedule.start).should == @schedule and
70
+ entry.command.should == @command and
71
+ entry.uid.should == Etc.getpwnam('root').uid
72
+ end
73
+
74
+ it 'should parse system crontab entry line with symbolic schedule' do
75
+ entry = Cronman::Entry.parse('@daily root echo hello', :system => true)
76
+ entry.schedule.from(@schedule.start).should == @schedule and
77
+ entry.command.should == @command and
78
+ entry.uid.should == Etc.getpwnam('root').uid
79
+ end
80
+
81
+ it 'should ignore leading whitespaces' do
82
+ entry = Cronman::Entry.parse(' @daily echo hello')
83
+ entry.schedule.from(@schedule.start).should == @schedule and
84
+ entry.command.should == @command
85
+
86
+ entry = Cronman::Entry.parse(' 0 0 * * * echo hello')
87
+ entry.schedule.from(@schedule.start).should == @schedule and
88
+ entry.command.should == @command
89
+ end
90
+ end
91
+ end
92
+
93
+ describe 'when accessing' do
94
+ before :each do
95
+ @schedule = Cronman::Schedule.new('@hourly')
96
+ @other_schedule = Cronman::Schedule.new('@monthly')
97
+ @command = 'ehco hello'
98
+ @other_command = 'echo bonjour'
99
+ @cron_definition = '0 0 * * *'
100
+ @other_cron_definition = '0,30 0 * * *'
101
+ @translation = ['midnight', 'every day']
102
+ @other_translation = ['0 and 30 minutes past', 'midnight of', 'every day']
103
+ @entry = Cronman::Entry.new(@schedule, @command, @cron_definition)
104
+ end
105
+
106
+ it 'should be read-accessible to schedule' do
107
+ @entry.schedule.should == @schedule and
108
+ lambda { @entry.schedule = @other_schedule }.should raise_error(NoMethodError)
109
+ @entry.schedule.should == @schedule
110
+ end
111
+
112
+ it 'should freeze schedule' do
113
+ @entry.schedule.should be_frozen
114
+ end
115
+
116
+ it 'should be read-accessible to command' do
117
+ @entry.command.should == @command and
118
+ lambda { @entry.command = @other_command }.should raise_error(NoMethodError)
119
+ @entry.command.should == @command
120
+ end
121
+
122
+ it 'should freeze command' do
123
+ @entry.command.should be_frozen
124
+ end
125
+
126
+ it 'should be read-accessible to uid' do
127
+ @entry.uid.should == Process.uid and
128
+ Process.uid.should_not == 0 and
129
+ lambda { @entry.uid = 0 }.should raise_error(NoMethodError)
130
+ @entry.uid.should == Process.uid
131
+ end
132
+
133
+ it 'should be freeze cron_definition' do
134
+ @entry.cron_definition.should be_frozen
135
+ end
136
+
137
+ it 'should be read-accessible to cron_definition' do
138
+ @entry.cron_definition == @cron_definition and
139
+ lambda { @entry.cron_definition = @other_cron_definition }.should raise_error(NoMethodError)
140
+ @entry.cron_definition == @cron_definition
141
+ end
142
+
143
+ it 'should be freeze translation' do
144
+ @entry.translation.should be_frozen
145
+ end
146
+
147
+ it 'should be read-accessible to translation' do
148
+ @entry.translation == @translation and
149
+ lambda { @entry.translation = @other_translation }.should raise_error(NoMethodError)
150
+ @entry.translation == @translation
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,356 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+ require 'cronman/schedule'
3
+
4
+ describe Cronman::Schedule do
5
+ describe 'when parsing spec' do
6
+ it 'should accept spec with asterisks' do
7
+ schedule = Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 { Cronman::Schedule.new }.should raise_error(ArgumentError)
76
+ lambda { Cronman::Schedule.new(nil) }.should raise_error(ArgumentError)
77
+ lambda { Cronman::Schedule.new('') }.should raise_error(ArgumentError)
78
+ lambda { Cronman::Schedule.new('x') }.should raise_error(ArgumentError)
79
+ lambda { Cronman::Schedule.new('* * * *') }.should raise_error(ArgumentError)
80
+ lambda { Cronman::Schedule.new('* * * * * *') }.should raise_error(ArgumentError)
81
+ end
82
+
83
+ it 'should reject spec with incorrect values' do
84
+ lambda { Cronman::Schedule.new('x 0 1 0 0') }.should raise_error(ArgumentError)
85
+ lambda { Cronman::Schedule.new('-1 0 1 1 0') }.should raise_error(ArgumentError)
86
+ lambda { Cronman::Schedule.new('60 0 1 1 0') }.should raise_error(ArgumentError)
87
+ lambda { Cronman::Schedule.new('0 24 1 1 0') }.should raise_error(ArgumentError)
88
+ lambda { Cronman::Schedule.new('0 0 32 1 0') }.should raise_error(ArgumentError)
89
+ lambda { Cronman::Schedule.new('0 0 1 13 0') }.should raise_error(ArgumentError)
90
+ lambda { Cronman::Schedule.new('0 0 1 1 8') }.should raise_error(ArgumentError)
91
+ lambda { Cronman::Schedule.new('0 0 1 Mon 8') }.should raise_error(ArgumentError)
92
+ lambda { Cronman::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 { Cronman::Schedule.new('0 0 1 Jan-Feb *') }.should raise_error(ArgumentError)
97
+ lambda { Cronman::Schedule.new('0 0 1 Jan,Feb *') }.should raise_error(ArgumentError)
98
+ lambda { Cronman::Schedule.new('0 0 1 * Sun-Fri') }.should raise_error(ArgumentError)
99
+ lambda { Cronman::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 { Cronman::Schedule.new('0/2 0 1 1 0') }.should raise_error(ArgumentError)
104
+ lambda { Cronman::Schedule.new('0 0/2 1 1 0') }.should raise_error(ArgumentError)
105
+ lambda { Cronman::Schedule.new('0 0 1/2 1 0') }.should raise_error(ArgumentError)
106
+ lambda { Cronman::Schedule.new('0 0 1 1/2 0') }.should raise_error(ArgumentError)
107
+ lambda { Cronman::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
+ Cronman::Schedule.new('@yearly', start).should == Cronman::Schedule.new('0 0 1 1 *', start) and
113
+ Cronman::Schedule.new('@annually', start).should == Cronman::Schedule.new('0 0 1 1 *', start) and
114
+ Cronman::Schedule.new('@monthly', start).should == Cronman::Schedule.new('0 0 1 * *', start) and
115
+ Cronman::Schedule.new('@weekly', start).should == Cronman::Schedule.new('0 0 * * 0', start) and
116
+ Cronman::Schedule.new('@daily', start).should == Cronman::Schedule.new('0 0 * * *', start) and
117
+ Cronman::Schedule.new('@midnight', start).should == Cronman::Schedule.new('0 0 * * *', start) and
118
+ Cronman::Schedule.new('@hourly', start).should == Cronman::Schedule.new('0 * * * *', start)
119
+ end
120
+
121
+ it 'should reject @reboot' do
122
+ lambda { Cronman::Schedule.new('@reboot') }.should raise_error(NotImplementedError)
123
+ end
124
+
125
+ it 'should reject unknown symbolic spec' do
126
+ lambda { Cronman::Schedule.new('@unknown') }.should raise_error(ArgumentError)
127
+ end
128
+
129
+ it 'should ignore leading/trailing whitespaces' do
130
+ lambda {
131
+ Cronman::Schedule.new(' @daily')
132
+ Cronman::Schedule.new('@daily ')
133
+ Cronman::Schedule.new(' * * * * *')
134
+ Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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
+ Cronman::Schedule.new('0 0 1 1 0', start).should == Cronman::Schedule.new('0 0 1 1 0', start) and
225
+ Cronman::Schedule.new('0 0 1 1 0', start).should == Cronman::Schedule.new('0 0 1 Jan 0', start) and
226
+ Cronman::Schedule.new('0 0 1 1 0', start).should == Cronman::Schedule.new('0 0 1 1 Sun', start) and
227
+
228
+ Cronman::Schedule.new('0 0 1 1 0', start).should_not == Cronman::Schedule.new('1 0 1 1 0', start) and
229
+ Cronman::Schedule.new('0 0 1 1 0', start).should_not == Cronman::Schedule.new('0 1 1 1 0', start) and
230
+ Cronman::Schedule.new('0 0 1 1 0', start).should_not == Cronman::Schedule.new('0 0 2 1 0', start) and
231
+ Cronman::Schedule.new('0 0 1 1 0', start).should_not == Cronman::Schedule.new('0 0 1 2 0', start) and
232
+ Cronman::Schedule.new('0 0 1 1 0', start).should_not == Cronman::Schedule.new('0 0 1 1 1', start) and
233
+
234
+ Cronman::Schedule.new('0 0 1-5 1 0', start).should == Cronman::Schedule.new('0 0 1,2,3,4,5 1 0', start) and
235
+ Cronman::Schedule.new('0 0 1 1 0-4', start).should == Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 = Cronman::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 'cronman'
3
+
4
+ describe Cronman do
5
+ describe 'parsing' do
6
+ it 'should recognize entries' do
7
+ crontab = Cronman.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 = Cronman.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,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cronman
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Renato Moya
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-06-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: cron2english
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.1.3
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 0.1.3
30
+ description:
31
+ email: imexto@gmail.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - lib/cronman.rb
37
+ - lib/cronman/entry.rb
38
+ - lib/cronman/schedule.rb
39
+ - spec/cronman_spec.rb
40
+ - spec/cronman/entry_spec.rb
41
+ - spec/cronman/schedule_spec.rb
42
+ - cronman.gemspec
43
+ - Rakefile
44
+ - README.md
45
+ homepage: http://github.com/renatomoya/cronman
46
+ licenses: []
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubyforge_project: n/a
65
+ rubygems_version: 1.8.23
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: A crontab(5) entry parser
69
+ test_files: []