cronscription 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ coverage/*
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |rt|
5
+ rt.fail_on_error = false
6
+ end
7
+
8
+ task :default => :spec
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "cronscription/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "cronscription"
7
+ s.version = Cronscription::VERSION
8
+ s.authors = ["Ben Feng"]
9
+ s.email = ["bfeng@enova.com"]
10
+ s.homepage = "https://github.com/enova/cronscription"
11
+ s.summary = %q{Cron parsing}
12
+ s.description = %q{Cron parsing}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_development_dependency "rake"
20
+ s.add_development_dependency "rspec"
21
+ s.add_development_dependency "simplecov"
22
+ end
@@ -0,0 +1,18 @@
1
+ require 'cronscription/tab'
2
+ require 'cronscription/version'
3
+
4
+
5
+ module Cronscription
6
+ class << self
7
+ # Convenient construction methods
8
+ def from_s(str)
9
+ Tab.new(str.lines.to_a)
10
+ end
11
+
12
+ def from_filepath(path)
13
+ File.open(path) do |f|
14
+ Tab.new(f.readlines)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,91 @@
1
+ module Cronscription
2
+ class Entry
3
+ ORDERED_KEYS = [:min, :hour, :day, :month, :wday]
4
+ FULL_RANGE = {
5
+ :min => (0..59).to_a,
6
+ :hour => (0..23).to_a,
7
+ :day => (1..31).to_a,
8
+ :month => (1..12).to_a,
9
+ :wday => (0..6).to_a,
10
+ }
11
+
12
+ attr_reader :times, :command
13
+
14
+ def initialize(line)
15
+ @line = line
16
+ @times = {}
17
+
18
+ raw = {}
19
+ raw[:min], raw[:hour], raw[:day], raw[:month], raw[:wday], @command = line.split(nil, 6)
20
+ @command.gsub!(/#.*/, '')
21
+
22
+ raw.each do |key, val|
23
+ @times[key] = parse_column(val, FULL_RANGE[key])
24
+ end
25
+ end
26
+
27
+ def ==(other)
28
+ @line == other.instance_variable_get(:@line)
29
+ end
30
+
31
+ def to_s
32
+ @line
33
+ end
34
+
35
+ def self.parsable?(str)
36
+ !!(str =~ /([*\d,-]+\s+){5}.*/)
37
+ end
38
+
39
+ def parse_column(column, default=[])
40
+ case column
41
+ when /\*\/(\d+)/ then default.select { |val| val % $1.to_i == 0 }
42
+ when /\*/ then default
43
+ when /,/ then column.split(',').map{|c| parse_column(c)}.flatten.uniq
44
+ when /-/ then Range.new(*column.split('-').map{|c| c.to_i}).to_a
45
+ else [column.to_i]
46
+ end
47
+ end
48
+
49
+ def match_command?(regex)
50
+ regex === @command
51
+ end
52
+
53
+ def times_to_execute(start, finish)
54
+ ret = []
55
+
56
+ incr_min = 60
57
+ incr_hour = incr_min*60
58
+ incr_day = incr_hour*24
59
+ incr = incr_min
60
+
61
+ current = nearest_minute(start)
62
+ while current <= finish
63
+ if ORDERED_KEYS.map{|k| @times[k].include?(current.send k)}.all?
64
+ ret << current
65
+ # If only I could goto into the middle of the loop, this wouldn't run every time.
66
+ # Optimizations to reduce execution time. No need to run minutely if there is only one minute.
67
+ if @times[:min].size == 1
68
+ if @times[:hour].size == 1
69
+ incr = incr_day
70
+ else
71
+ incr = incr_hour
72
+ end
73
+ end
74
+ end
75
+ current += incr
76
+ end
77
+
78
+ ret
79
+ end
80
+
81
+ private
82
+ def nearest_minute(time)
83
+ if time.sec == 0
84
+ time
85
+ else
86
+ # Always round up
87
+ time + (60 - time.sec)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,27 @@
1
+ require 'cronscription/entry'
2
+
3
+
4
+ module Cronscription
5
+ class Tab
6
+ def initialize(cron_lines)
7
+ # Eliminate all lines starting with '#' since they are full comments
8
+ @entries = cron_lines.select{|l| Entry.parsable?(l)}.map{|e| Entry.new(e)}
9
+ end
10
+
11
+ def ==(other)
12
+ @entries == other.instance_variable_get(:@entries)
13
+ end
14
+
15
+ def find(regex)
16
+ @entries.select{|e| e.match_command?(regex)}
17
+ end
18
+
19
+ def sorted_merge(*arrs)
20
+ arrs.flatten.uniq.sort
21
+ end
22
+
23
+ def times_to_execute(regex, start, finish)
24
+ sorted_merge(find(regex).map{|e| e.times_to_execute(start, finish)})
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module Cronscription
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,178 @@
1
+ require 'spec_helper'
2
+ require 'cronscription/entry'
3
+
4
+
5
+ describe Cronscription::Entry do
6
+ before :all do
7
+ # <minute> <hour> <day> <month> <day of week> <tags and command>
8
+ @line = '1 2 3 4 5 comm'
9
+ @entry = Cronscription::Entry.new(@line)
10
+ end
11
+
12
+ it 'should be equal when created from same values' do
13
+ Cronscription::Entry.new(@line).should == @entry
14
+ end
15
+
16
+ describe 'from_s' do
17
+ it 'should map columns with correct fields' do
18
+ @entry.times.should == {
19
+ :min => [1],
20
+ :hour => [2],
21
+ :day => [3],
22
+ :month => [4],
23
+ :wday => [5],
24
+ }
25
+ @entry.command.should == 'comm'
26
+ end
27
+
28
+ it 'should convert to original line on to_s' do
29
+ line = ' 1 2 * 4 5 fun today! # some comment'
30
+ entry = Cronscription::Entry.new(line)
31
+ entry.to_s.should == line
32
+ end
33
+
34
+ it 'should use FULL_RANGE for default values' do
35
+ entry = Cronscription::Entry.new('* * * * * comm')
36
+ entry.times.should == Cronscription::Entry::FULL_RANGE
37
+ end
38
+
39
+ it 'should understand compex command structure' do
40
+ command = 'comm/rad --gen=ro -s tail'
41
+ entry = Cronscription::Entry.new("* * * * * #{command}")
42
+ entry.command.should == command
43
+ end
44
+ end
45
+
46
+ describe 'parsable?' do
47
+ it 'should be true for basic lines' do
48
+ Cronscription::Entry.parsable?('* * * * * comm').should be_true
49
+ Cronscription::Entry.parsable?('1 2 3 4 5 comm').should be_true
50
+ end
51
+
52
+ it 'should be true for compound time directives' do
53
+ Cronscription::Entry.parsable?('1-2 * * * * comm').should be_true
54
+ Cronscription::Entry.parsable?(' * 3,4 * * * comm').should be_true
55
+ Cronscription::Entry.parsable?(' * * 3,4-5 * * comm').should be_true
56
+ Cronscription::Entry.parsable?(' * * * 6-7,0,8-9 * comm').should be_true
57
+ end
58
+
59
+ it 'should be true for complex commands' do
60
+ Cronscription::Entry.parsable?('* * * * * comm/rad --gen=ro -s tail').should be_true
61
+ end
62
+
63
+ it 'should be false for bad time declaration' do
64
+ Cronscription::Entry.parsable?('* b * * * comm').should be_false
65
+ Cronscription::Entry.parsable?('* * * * . comm').should be_false
66
+ end
67
+
68
+ it 'should be false for dumb lines' do
69
+ Cronscription::Entry.parsable?(' # This is a comment').should be_false
70
+ Cronscription::Entry.parsable?('rawr!').should be_false
71
+ end
72
+ end
73
+
74
+ describe 'parse_column' do
75
+ it 'should return fixed value as list of one' do
76
+ @entry.parse_column('1').should == [1]
77
+ end
78
+
79
+ it 'should return comma-separated as list of values' do
80
+ @entry.parse_column('5,2,8').should == [5, 2, 8]
81
+ end
82
+
83
+ it 'should return range as list of everything within the range' do
84
+ @entry.parse_column('4-6').should == [4, 5, 6]
85
+ end
86
+
87
+ it 'should return combination of range and comma-separated' do
88
+ @entry.parse_column('9,2-5,7').should == [9, 2, 3, 4, 5, 7]
89
+ end
90
+
91
+ it 'should use default value on asterisk' do
92
+ default = [1, 5, 9, 2, 6]
93
+ @entry.parse_column('*', default).should == default
94
+ end
95
+
96
+ it 'should find every n minutes from the default on */n' do
97
+ default = [*0..100]
98
+ @entry.parse_column('*/31', default).should == [0, 31, 62, 93]
99
+ end
100
+
101
+ it 'should return unique entries only' do
102
+ @entry.parse_column('1,1,1').should == [1]
103
+ end
104
+ end
105
+
106
+ describe 'match_command?' do
107
+ it 'should match command by regex' do
108
+ entry = Cronscription::Entry.new('1 2 3 4 5 command one')
109
+ entry.match_command?(/m*and\s*on/).should be_true
110
+ end
111
+
112
+ it 'should not match command within comments' do
113
+ entry = Cronscription::Entry.new('1 2 3 4 5 command #herp')
114
+ entry.match_command?(/herp/).should be_false
115
+ end
116
+
117
+ it 'should not match command within time directives' do
118
+ entry = Cronscription::Entry.new('1 2 3 4 5 command #herp')
119
+ entry.match_command?(/\d/).should be_false
120
+ end
121
+ end
122
+
123
+ describe 'times_to_execute' do
124
+ it 'should return times based on minutes' do
125
+ entry = Cronscription::Entry.new("21-40 * * * * comm")
126
+ start = Time.local(2011, 1, 1, 0, 0)
127
+ finish = Time.local(2011, 1, 1, 0, 30)
128
+
129
+ times = entry.times_to_execute(start, finish)
130
+ times.should == (21..30).map{|m| Time.local(2011, 1, 1, 0, m)}
131
+ end
132
+
133
+ it 'should return times based on hours' do
134
+ entry = Cronscription::Entry.new("0 1-8 * * * comm")
135
+ start = Time.local(2011, 1, 1, 0, 0)
136
+ finish = Time.local(2011, 1, 1, 6, 0)
137
+
138
+ times = entry.times_to_execute(start, finish)
139
+ times.should == (1..6).map{|h| Time.local(2011, 1, 1, h, 0)}
140
+ end
141
+
142
+ it 'should return times based on days' do
143
+ entry = Cronscription::Entry.new("0 0 8-20 * * comm")
144
+ start = Time.local(2011, 1, 5, 0, 0)
145
+ finish = Time.local(2011, 1, 15, 0, 0)
146
+
147
+ times = entry.times_to_execute(start, finish)
148
+ times.should == (8..15).map{|d| Time.local(2011, 1, d, 0, 0)}
149
+ end
150
+
151
+ it 'should return times based on months' do
152
+ entry = Cronscription::Entry.new("0 0 1 1-5 * comm")
153
+ start = Time.local(2011, 4, 1, 0, 0)
154
+ finish = Time.local(2011, 12, 1, 0, 0)
155
+
156
+ times = entry.times_to_execute(start, finish)
157
+ times.should == (4..5).map{|m| Time.local(2011, m, 1, 0, 0)}
158
+ end
159
+
160
+ it 'should round start time up to the next minute' do
161
+ entry = Cronscription::Entry.new("* * * * * comm")
162
+ start = Time.local(2011, 1, 1, 0, 0, 45)
163
+ finish = Time.local(2011, 1, 1, 0, 1, 45)
164
+
165
+ times = entry.times_to_execute(start, finish)
166
+ times.should == [Time.local(2011, 1, 1, 0, 1)]
167
+ end
168
+
169
+ it 'should round start time up by bumping up necessary other fields' do
170
+ entry = Cronscription::Entry.new("* * * * * comm")
171
+ start = Time.local(2011, 12, 31, 23, 59, 45)
172
+ finish = Time.local(2012, 1, 1, 0, 0, 45)
173
+
174
+ times = entry.times_to_execute(start, finish)
175
+ times.should == [Time.local(2012, 1, 1, 0, 0)]
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,103 @@
1
+ require 'spec_helper'
2
+ require 'cronscription'
3
+
4
+ require 'tempfile'
5
+
6
+
7
+ describe Cronscription::Tab do
8
+ before :all do
9
+ @cronstr = <<-END
10
+ # <minute> <hour> <day> <month> <day of week> <tags and command>
11
+ 0 * * * * cron.hourly
12
+ 0 0 * * * cron.daily
13
+ 0 0 * * 0 cron.weekly
14
+ 0 0 1 * * cron.monthly
15
+ 1 2 3 4 5 0 # test trailing comment
16
+ END
17
+ @cron_lines = @cronstr.lines.to_a
18
+ @tab = Cronscription::Tab.new(@cron_lines)
19
+ end
20
+
21
+ it 'should be equal when created from same values' do
22
+ Cronscription::Tab.new(@cron_lines).should == @tab
23
+ end
24
+
25
+ describe 'find' do
26
+ it 'should find the daily entry' do
27
+ entries = @tab.find(/daily/).map{|e| e.to_s}
28
+ entries.should == [@cron_lines[2]]
29
+ end
30
+
31
+ it 'should find all cron.* entries' do
32
+ entries = @tab.find(/cron\..*/).map{|e| e.to_s}
33
+ entries.should == @cron_lines[1..4]
34
+ end
35
+
36
+ it 'should ignore complete line comments' do
37
+ entries = @tab.find(/.*/).map{|e| e.to_s}
38
+ entries.should == @cron_lines[1..-1]
39
+ end
40
+
41
+ it 'should ignore trailing comments' do
42
+ entries = @tab.find(/test trailing comment/).map{|e| e.to_s}
43
+ entries.should == []
44
+ end
45
+
46
+ it 'should ignore time directives' do
47
+ entries = @tab.find(/0/).map{|e| e.to_s}
48
+ entries.should == [@cron_lines[-1]]
49
+ end
50
+
51
+ it 'should attempt to use possible lines when encountering mangled garbage' do
52
+ cronstr = <<-END
53
+ # The history of all hitherto
54
+ existing society is the
55
+ * * * * * comm
56
+ history of class struggles.
57
+ END
58
+ cron_lines = cronstr.lines.to_a
59
+ tab = Cronscription::Tab.new(cron_lines)
60
+ entries = tab.find(/.*/).map{|e| e.to_s}
61
+ entries.should == [cron_lines[2]]
62
+ end
63
+ end
64
+
65
+ describe 'sorted_merge' do
66
+ before :all do
67
+ @tab = Cronscription::Tab.new([])
68
+ end
69
+
70
+ it 'should merge in order' do
71
+ @tab.sorted_merge([1, 4], [2, 7, 9]).should == [1, 2, 4, 7, 9]
72
+ end
73
+
74
+ it 'should merge while eliminating duplicates' do
75
+ @tab.sorted_merge([2, 2, 2, 6, 7], [7, 8]).should == [2, 6, 7, 8]
76
+ end
77
+ end
78
+
79
+ describe 'times_to_execute' do
80
+ it 'should return merged times' do
81
+ hour1 = 3
82
+ min1 = 48
83
+
84
+ hour2 = 6
85
+ min2 = 21
86
+ cronfile = <<-END
87
+ #{min1} #{hour1} * * * common
88
+ #{min2} #{hour2} * * * common
89
+ END
90
+ tab = Cronscription::Tab.new(cronfile.lines.to_a)
91
+
92
+ start = Time.local(2011, 1, 1, 0, 0)
93
+ finish = Time.local(2011, 1, 3, 0, 0)
94
+ tab.times_to_execute(/common/, start, finish).should == [
95
+ Time.local(2011, 1, 1, hour1, min1),
96
+ Time.local(2011, 1, 1, hour2, min2),
97
+ Time.local(2011, 1, 2, hour1, min1),
98
+ Time.local(2011, 1, 2, hour2, min2),
99
+ ]
100
+ end
101
+ end
102
+ end
103
+
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+ require 'cronscription'
3
+
4
+ require 'tempfile'
5
+
6
+
7
+ describe Cronscription do
8
+ before(:all) do
9
+ @cronstr = <<-END
10
+ 59 * 10 * * entry1
11
+ * 12 2 * 5 entry2
12
+ END
13
+ @tab = Cronscription::Tab.new(@cronstr.lines.to_a)
14
+ end
15
+
16
+ describe 'convenient constructors' do
17
+ it 'should create from string' do
18
+ Cronscription.from_s(@cronstr).should == @tab
19
+ end
20
+
21
+ it 'should create from filepath' do
22
+ path = nil
23
+ Tempfile.open('cronscription') do |f|
24
+ f.write(@cronstr)
25
+ path = f.path
26
+ end
27
+
28
+ Cronscription.from_filepath(path).should == @tab
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,2 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cronscription
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ben Feng
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: simplecov
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: Cron parsing
63
+ email:
64
+ - bfeng@enova.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - Rakefile
72
+ - cronscription.gemspec
73
+ - lib/cronscription.rb
74
+ - lib/cronscription/entry.rb
75
+ - lib/cronscription/tab.rb
76
+ - lib/cronscription/version.rb
77
+ - spec/cronscription/entry_spec.rb
78
+ - spec/cronscription/tab_spec.rb
79
+ - spec/cronscription_spec.rb
80
+ - spec/spec_helper.rb
81
+ homepage: https://github.com/enova/cronscription
82
+ licenses: []
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 1.8.23
102
+ signing_key:
103
+ specification_version: 3
104
+ summary: Cron parsing
105
+ test_files:
106
+ - spec/cronscription/entry_spec.rb
107
+ - spec/cronscription/tab_spec.rb
108
+ - spec/cronscription_spec.rb
109
+ - spec/spec_helper.rb
110
+ has_rdoc: