whenever 0.9.2 → 0.10.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +17 -7
  4. data/CHANGELOG.md +63 -0
  5. data/Gemfile +0 -5
  6. data/LICENSE +1 -1
  7. data/README.md +101 -18
  8. data/Rakefile +3 -10
  9. data/bin/whenever +3 -0
  10. data/bin/wheneverize +8 -5
  11. data/gemfiles/activesupport4.1.gemfile +5 -0
  12. data/gemfiles/activesupport4.2.gemfile +5 -0
  13. data/lib/whenever/capistrano/v2/recipes.rb +4 -3
  14. data/lib/whenever/capistrano/v3/tasks/whenever.rake +20 -7
  15. data/lib/whenever/capistrano.rb +1 -1
  16. data/lib/whenever/command_line.rb +49 -28
  17. data/lib/whenever/cron.rb +43 -21
  18. data/lib/whenever/job.rb +4 -3
  19. data/lib/whenever/job_list.rb +54 -24
  20. data/lib/whenever/numeric.rb +13 -0
  21. data/lib/whenever/numeric_seconds.rb +48 -0
  22. data/lib/whenever/os.rb +7 -0
  23. data/lib/whenever/setup.rb +5 -1
  24. data/lib/whenever/version.rb +1 -1
  25. data/lib/whenever.rb +15 -14
  26. data/test/functional/command_line_test.rb +379 -255
  27. data/test/functional/output_at_test.rb +227 -249
  28. data/test/functional/output_default_defined_jobs_test.rb +239 -288
  29. data/test/functional/output_defined_job_test.rb +65 -91
  30. data/test/functional/output_env_test.rb +22 -26
  31. data/test/functional/output_jobs_for_roles_test.rb +46 -66
  32. data/test/functional/output_jobs_with_mailto_test.rb +168 -0
  33. data/test/functional/output_redirection_test.rb +231 -309
  34. data/test/test_case.rb +32 -0
  35. data/test/test_helper.rb +44 -15
  36. data/test/unit/capistrano_support_test.rb +126 -148
  37. data/test/unit/cron_test.rb +327 -210
  38. data/test/unit/executable_test.rb +142 -0
  39. data/test/unit/job_test.rb +111 -121
  40. data/whenever.gemspec +5 -4
  41. metadata +37 -28
  42. data/lib/whenever/tasks/whenever.rake +0 -43
data/lib/whenever/cron.rb CHANGED
@@ -3,16 +3,20 @@ require 'chronic'
3
3
  module Whenever
4
4
  module Output
5
5
  class Cron
6
+ DAYS = %w(sun mon tue wed thu fri sat)
7
+ MONTHS = %w(jan feb mar apr may jun jul aug sep oct nov dec)
6
8
  KEYWORDS = [:reboot, :yearly, :annually, :monthly, :weekly, :daily, :midnight, :hourly]
7
- REGEX = /^(@(#{KEYWORDS.join '|'})|.+\s+.+\s+.+\s+.+\s+.+.?)$/
9
+ REGEX = /^(@(#{KEYWORDS.join '|'})|((\*?[\d\/,\-]*)\s){3}(\*?([\d\/,\-]|(#{MONTHS.join '|'}))*\s)(\*?([\d\/,\-]|(#{DAYS.join '|'}))*))$/i
8
10
 
9
11
  attr_accessor :time, :task
10
12
 
11
- def initialize(time = nil, task = nil, at = nil)
13
+ def initialize(time = nil, task = nil, at = nil, options = {})
14
+ chronic_options = options[:chronic_options] || {}
15
+
12
16
  @at_given = at
13
17
  @time = time
14
18
  @task = task
15
- @at = at.is_a?(String) ? (Chronic.parse(at) || 0) : (at || 0)
19
+ @at = at.is_a?(String) ? (Chronic.parse(at, chronic_options) || 0) : (at || 0)
16
20
  end
17
21
 
18
22
  def self.enumerate(item, detect_cron = true)
@@ -30,10 +34,10 @@ module Whenever
30
34
  items
31
35
  end
32
36
 
33
- def self.output(times, job)
37
+ def self.output(times, job, options = {})
34
38
  enumerate(times).each do |time|
35
39
  enumerate(job.at, false).each do |at|
36
- yield new(time, job.output, at).output
40
+ yield new(time, job.output, at, options).output
37
41
  end
38
42
  end
39
43
  end
@@ -43,8 +47,9 @@ module Whenever
43
47
  end
44
48
 
45
49
  def time_in_cron_syntax
50
+ @time = @time.to_i if @time.is_a?(Numeric) # Compatibility with `1.day` format using ruby 2.3 and activesupport
46
51
  case @time
47
- when REGEX then @time # raw cron sytax given
52
+ when REGEX then @time # raw cron syntax given
48
53
  when Symbol then parse_symbol
49
54
  when String then parse_as_string
50
55
  else parse_time
@@ -53,18 +58,18 @@ module Whenever
53
58
 
54
59
  protected
55
60
  def day_given?
56
- months = %w(jan feb mar apr may jun jul aug sep oct nov dec)
57
- @at_given.is_a?(String) && months.any? { |m| @at_given.downcase.index(m) }
61
+ @at_given.is_a?(String) && (MONTHS.any? { |m| @at_given.downcase.index(m) } || @at_given[/\d\/\d/])
58
62
  end
59
63
 
60
64
  def parse_symbol
61
65
  shortcut = case @time
62
66
  when *KEYWORDS then "@#{@time}" # :reboot => '@reboot'
63
- when :year then 12.months
64
- when :day then 1.day
65
- when :month then 1.month
66
- when :week then 1.week
67
- when :hour then 1.hour
67
+ when :year then Whenever.seconds(1, :year)
68
+ when :day then Whenever.seconds(1, :day)
69
+ when :month then Whenever.seconds(1, :month)
70
+ when :week then Whenever.seconds(1, :week)
71
+ when :hour then Whenever.seconds(1, :hour)
72
+ when :minute then Whenever.seconds(1, :minute)
68
73
  end
69
74
 
70
75
  if shortcut.is_a?(Numeric)
@@ -84,22 +89,24 @@ module Whenever
84
89
  def parse_time
85
90
  timing = Array.new(5, '*')
86
91
  case @time
87
- when 0.seconds...1.minute
92
+ when Whenever.seconds(0, :seconds)...Whenever.seconds(1, :minute)
88
93
  raise ArgumentError, "Time must be in minutes or higher"
89
- when 1.minute...1.hour
94
+ when Whenever.seconds(1, :minute)...Whenever.seconds(1, :hour)
90
95
  minute_frequency = @time / 60
91
96
  timing[0] = comma_separated_timing(minute_frequency, 59, @at || 0)
92
- when 1.hour...1.day
97
+ when Whenever.seconds(1, :hour)...Whenever.seconds(1, :day)
93
98
  hour_frequency = (@time / 60 / 60).round
94
99
  timing[0] = @at.is_a?(Time) ? @at.min : @at
95
100
  timing[1] = comma_separated_timing(hour_frequency, 23)
96
- when 1.day...1.month
101
+ raise ArgumentError, "Minute must be between 0-59, #{timing[0]} given" unless (0..59).include?(timing[0])
102
+ when Whenever.seconds(1, :day)...Whenever.seconds(1, :month)
97
103
  day_frequency = (@time / 24 / 60 / 60).round
98
104
  timing[0] = @at.is_a?(Time) ? @at.min : 0
99
105
  timing[1] = @at.is_a?(Time) ? @at.hour : @at
100
106
  timing[2] = comma_separated_timing(day_frequency, 31, 1)
101
- when 1.month..12.months
102
- month_frequency = (@time / 30 / 24 / 60 / 60).round
107
+ raise ArgumentError, "Hour must be between 0-23, #{timing[1]} given" unless (0..23).include?(timing[1])
108
+ when Whenever.seconds(1, :month)...Whenever.seconds(1, :year)
109
+ month_frequency = (@time / 30 / 24 / 60 / 60).round
103
110
  timing[0] = @at.is_a?(Time) ? @at.min : 0
104
111
  timing[1] = @at.is_a?(Time) ? @at.hour : 0
105
112
  timing[2] = if @at.is_a?(Time)
@@ -108,6 +115,21 @@ module Whenever
108
115
  @at.zero? ? 1 : @at
109
116
  end
110
117
  timing[3] = comma_separated_timing(month_frequency, 12, 1)
118
+ raise ArgumentError, "Day must be between 1-31, #{timing[2]} given" unless (1..31).include?(timing[2])
119
+ when Whenever.seconds(1, :year)
120
+ timing[0] = @at.is_a?(Time) ? @at.min : 0
121
+ timing[1] = @at.is_a?(Time) ? @at.hour : 0
122
+ timing[2] = if @at.is_a?(Time)
123
+ day_given? ? @at.day : 1
124
+ else
125
+ 1
126
+ end
127
+ timing[3] = if @at.is_a?(Time)
128
+ day_given? ? @at.month : 1
129
+ else
130
+ @at.zero? ? 1 : @at
131
+ end
132
+ raise ArgumentError, "Month must be between 1-12, #{timing[3]} given" unless (1..12).include?(timing[3])
111
133
  else
112
134
  return parse_as_string
113
135
  end
@@ -125,7 +147,7 @@ module Whenever
125
147
  return (timing << '1-5') * " " if string.downcase.index('weekday')
126
148
  return (timing << '6,0') * " " if string.downcase.index('weekend')
127
149
 
128
- %w(sun mon tue wed thu fri sat).each_with_index do |day, i|
150
+ DAYS.each_with_index do |day, i|
129
151
  return (timing << i) * " " if string.downcase.index(day)
130
152
  end
131
153
 
@@ -133,7 +155,7 @@ module Whenever
133
155
  end
134
156
 
135
157
  def comma_separated_timing(frequency, max, start = 0)
136
- return start if frequency.blank? || frequency.zero?
158
+ return start if frequency.nil? || frequency == "" || frequency.zero?
137
159
  return '*' if frequency == 1
138
160
  return frequency if frequency > (max * 0.5).ceil
139
161
 
data/lib/whenever/job.rb CHANGED
@@ -2,14 +2,15 @@ require 'shellwords'
2
2
 
3
3
  module Whenever
4
4
  class Job
5
- attr_reader :at, :roles
5
+ attr_reader :at, :roles, :mailto
6
6
 
7
7
  def initialize(options = {})
8
8
  @options = options
9
9
  @at = options.delete(:at)
10
10
  @template = options.delete(:template)
11
+ @mailto = options.fetch(:mailto, :default_mailto)
11
12
  @job_template = options.delete(:job_template) || ":job"
12
- @roles = Array.wrap(options.delete(:roles))
13
+ @roles = Array(options.delete(:roles))
13
14
  @options[:output] = options.has_key?(:output) ? Whenever::Output::Redirection.new(options[:output]).to_s : ''
14
15
  @options[:environment_variable] ||= "RAILS_ENV"
15
16
  @options[:environment] ||= :production
@@ -40,7 +41,7 @@ module Whenever
40
41
  else
41
42
  option
42
43
  end
43
- end.squish
44
+ end.gsub(/\s+/m, " ").strip
44
45
  end
45
46
 
46
47
  def escape_single_quotes(str)
@@ -30,10 +30,17 @@ module Whenever
30
30
  return if @pre_set_variables[variable]
31
31
 
32
32
  instance_variable_set("@#{variable}".to_sym, value)
33
- self.class.send(:attr_reader, variable.to_sym)
34
33
  @set_variables[variable] = value
35
34
  end
36
35
 
36
+ def method_missing(name, *args, &block)
37
+ @set_variables.has_key?(name) ? @set_variables[name] : super
38
+ end
39
+
40
+ def self.respond_to?(name, include_private = false)
41
+ @set_variables.has_key?(name) || super
42
+ end
43
+
37
44
  def env(variable, value)
38
45
  @env[variable.to_s] = value
39
46
  end
@@ -45,18 +52,21 @@ module Whenever
45
52
  end
46
53
 
47
54
  def job_type(name, template)
48
- class_eval do
55
+ singleton_class.class_eval do
49
56
  define_method(name) do |task, *args|
50
57
  options = { :task => task, :template => template }
51
58
  options.merge!(args[0]) if args[0].is_a? Hash
52
59
 
60
+ options[:mailto] ||= @options.fetch(:mailto, :default_mailto)
61
+
53
62
  # :cron_log was an old option for output redirection, it remains for backwards compatibility
54
63
  options[:output] = (options[:cron_log] || @cron_log) if defined?(@cron_log) || options.has_key?(:cron_log)
55
64
  # :output is the newer, more flexible option.
56
65
  options[:output] = @output if defined?(@output) && !options.has_key?(:output)
57
66
 
58
- @jobs[@current_time_scope] ||= []
59
- @jobs[@current_time_scope] << Whenever::Job.new(@options.merge(@set_variables).merge(options))
67
+ @jobs[options.fetch(:mailto)] ||= {}
68
+ @jobs[options.fetch(:mailto)][@current_time_scope] ||= []
69
+ @jobs[options.fetch(:mailto)][@current_time_scope] << Whenever::Job.new(@options.merge(@set_variables).merge(options))
60
70
  end
61
71
  end
62
72
  end
@@ -73,13 +83,13 @@ module Whenever
73
83
  # Only works for setting values as strings.
74
84
  #
75
85
  def pre_set(variable_string = nil)
76
- return if variable_string.blank?
86
+ return if variable_string.nil? || variable_string == ""
77
87
 
78
88
  pairs = variable_string.split('&')
79
89
  pairs.each do |pair|
80
90
  next unless pair.index('=')
81
91
  variable, value = *pair.split('=')
82
- unless variable.blank? || value.blank?
92
+ unless variable.nil? || variable == "" || value.nil? || value == ""
83
93
  variable = variable.strip.to_sym
84
94
  set(variable, value.strip)
85
95
  @pre_set_variables[variable] = value
@@ -92,7 +102,7 @@ module Whenever
92
102
 
93
103
  output = []
94
104
  @env.each do |key, val|
95
- output << "#{key}=#{val.blank? ? '""' : val}\n"
105
+ output << "#{key}=#{val.nil? || val == "" ? '""' : val}\n"
96
106
  end
97
107
  output << "\n"
98
108
 
@@ -125,31 +135,51 @@ module Whenever
125
135
  entries.map { |entry| entry.join(' ') }
126
136
  end
127
137
 
138
+ def cron_jobs_of_time(time, jobs)
139
+ shortcut_jobs, regular_jobs = [], []
140
+
141
+ jobs.each do |job|
142
+ next unless roles.empty? || roles.any? do |r|
143
+ job.has_role?(r)
144
+ end
145
+ Whenever::Output::Cron.output(time, job, :chronic_options => @chronic_options) do |cron|
146
+ cron << "\n\n"
147
+
148
+ if cron[0,1] == "@"
149
+ shortcut_jobs << cron
150
+ else
151
+ regular_jobs << cron
152
+ end
153
+ end
154
+ end
155
+
156
+ shortcut_jobs.join + combine(regular_jobs).join
157
+ end
158
+
128
159
  def cron_jobs
129
160
  return if @jobs.empty?
130
161
 
131
- shortcut_jobs = []
132
- regular_jobs = []
162
+ output = []
133
163
 
134
- output_all = roles.empty?
135
- @jobs.each do |time, jobs|
136
- jobs.each do |job|
137
- next unless output_all || roles.any? do |r|
138
- job.has_role?(r)
139
- end
140
- Whenever::Output::Cron.output(time, job) do |cron|
141
- cron << "\n\n"
164
+ # jobs with default mailto's must be output before the ones with non-default mailto's.
165
+ @jobs.delete(:default_mailto) { Hash.new }.each do |time, jobs|
166
+ output << cron_jobs_of_time(time, jobs)
167
+ end
142
168
 
143
- if cron.starts_with?("@")
144
- shortcut_jobs << cron
145
- else
146
- regular_jobs << cron
147
- end
148
- end
169
+ @jobs.each do |mailto, time_and_jobs|
170
+ output_jobs = []
171
+
172
+ time_and_jobs.each do |time, jobs|
173
+ output_jobs << cron_jobs_of_time(time, jobs)
149
174
  end
175
+
176
+ output_jobs.reject! { |output_job| output_job.empty? }
177
+
178
+ output << "MAILTO=#{mailto}\n\n" unless output_jobs.empty?
179
+ output << output_jobs
150
180
  end
151
181
 
152
- shortcut_jobs.join + combine(regular_jobs).join
182
+ output.join
153
183
  end
154
184
  end
155
185
  end
@@ -0,0 +1,13 @@
1
+ class Numeric
2
+ def respond_to?(method, include_private = false)
3
+ super || Whenever::NumericSeconds.public_method_defined?(method)
4
+ end
5
+
6
+ def method_missing(method, *args, &block)
7
+ if Whenever::NumericSeconds.public_method_defined?(method)
8
+ Whenever::NumericSeconds.new(self).send(method)
9
+ else
10
+ super
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ module Whenever
2
+ class NumericSeconds
3
+ attr_reader :number
4
+
5
+ def self.seconds(number, units)
6
+ new(number).send(units)
7
+ end
8
+
9
+ def initialize(number)
10
+ @number = number.to_i
11
+ end
12
+
13
+ def seconds
14
+ number
15
+ end
16
+ alias :second :seconds
17
+
18
+ def minutes
19
+ number * 60
20
+ end
21
+ alias :minute :minutes
22
+
23
+ def hours
24
+ number * 3_600
25
+ end
26
+ alias :hour :hours
27
+
28
+ def days
29
+ number * 86_400
30
+ end
31
+ alias :day :days
32
+
33
+ def weeks
34
+ number * 604_800
35
+ end
36
+ alias :week :weeks
37
+
38
+ def months
39
+ number * 2_592_000
40
+ end
41
+ alias :month :months
42
+
43
+ def years
44
+ number * 31_557_600
45
+ end
46
+ alias :year :years
47
+ end
48
+ end
@@ -0,0 +1,7 @@
1
+ module Whenever
2
+ module OS
3
+ def self.solaris?
4
+ (/solaris/ =~ RUBY_PLATFORM)
5
+ end
6
+ end
7
+ end
@@ -5,6 +5,10 @@ set :environment, "production"
5
5
  # Path defaults to the directory `whenever` was run from
6
6
  set :path, Whenever.path
7
7
 
8
+ # Custom Chronic configuration for time parsing, empty by default
9
+ # Full list of options at: https://github.com/mojombo/chronic/blob/master/lib/chronic/parser.rb
10
+ set :chronic_options, {}
11
+
8
12
  # All jobs are wrapped in this template.
9
13
  # http://blog.scoutapp.com/articles/2010/09/07/rvm-and-cron-in-production
10
14
  set :job_template, "/bin/bash -l -c ':job'"
@@ -23,4 +27,4 @@ set :bundle_command, Whenever.bundler? ? "bundle exec" : ""
23
27
  job_type :command, ":task :output"
24
28
  job_type :rake, "cd :path && :environment_variable=:environment :bundle_command rake :task --silent :output"
25
29
  job_type :script, "cd :path && :environment_variable=:environment :bundle_command script/:task :output"
26
- job_type :runner, "cd :path && :runner_command -e :environment ':task' :output"
30
+ job_type :runner, "cd :path && :bundle_command :runner_command -e :environment ':task' :output"
@@ -1,3 +1,3 @@
1
1
  module Whenever
2
- VERSION = '0.9.2'
2
+ VERSION = '0.10.0'
3
3
  end
data/lib/whenever.rb CHANGED
@@ -1,33 +1,34 @@
1
- require 'thread'
2
- require 'active_support/all'
1
+ require 'whenever/numeric'
2
+ require 'whenever/numeric_seconds'
3
+ require 'whenever/job_list'
4
+ require 'whenever/job'
5
+ require 'whenever/command_line'
6
+ require 'whenever/cron'
7
+ require 'whenever/output_redirection'
8
+ require 'whenever/os'
3
9
 
4
10
  module Whenever
5
- autoload :JobList, 'whenever/job_list'
6
- autoload :Job, 'whenever/job'
7
- autoload :CommandLine, 'whenever/command_line'
8
-
9
- module Output
10
- autoload :Cron, 'whenever/cron'
11
- autoload :Redirection, 'whenever/output_redirection'
12
- end
13
-
14
11
  def self.cron(options)
15
12
  Whenever::JobList.new(options).generate_cron_output
16
13
  end
17
14
 
15
+ def self.seconds(number, units)
16
+ Whenever::NumericSeconds.seconds(number, units)
17
+ end
18
+
18
19
  def self.path
19
20
  Dir.pwd
20
21
  end
21
22
 
22
23
  def self.bin_rails?
23
- File.exists?(File.join(path, 'bin', 'rails'))
24
+ File.exist?(File.join(path, 'bin', 'rails'))
24
25
  end
25
26
 
26
27
  def self.script_rails?
27
- File.exists?(File.join(path, 'script', 'rails'))
28
+ File.exist?(File.join(path, 'script', 'rails'))
28
29
  end
29
30
 
30
31
  def self.bundler?
31
- File.exists?(File.join(path, 'Gemfile'))
32
+ File.exist?(File.join(path, 'Gemfile'))
32
33
  end
33
34
  end