cronscription 1.0.0

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/.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: