cronman 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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: []