zhong 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4eafb64d51589613dfcfeca62326319eb839962f
4
- data.tar.gz: 66b4fd921dd9ed1795ffa407afbcc5f33b867607
3
+ metadata.gz: 8f1dac5a4ab2bbf39455eb3ae3aa85b8db281f71
4
+ data.tar.gz: bad4a27f7f28e8d9e09bd89e722c8d14de24135f
5
5
  SHA512:
6
- metadata.gz: 267619b1f313f958d2e2e7071bb3fbdcb22efcd9018e64d1cdcf5f07d86d055e5e4e4e87f8b1144550956ded9370c10584212fec1c6c7890bd6ace880af2348b
7
- data.tar.gz: e4a9329e22811d4cda8f01882ed7e8755388b30723eb0a424f5b01ff05d526f512be60fae222d77b359326e16f60208cc8452b0ce837b9ad2ea492e9d17bf8a6
6
+ metadata.gz: 09b25ced5faf77601dcc4056f32441cdf9183a542e4fcdc87ce689806e38e1fc1189613ff9ca11f776c8967e1c4fba9bab83049022860c21ab4893255c64fa4b
7
+ data.tar.gz: 50d880d5819a8d103d99740a1cacd3a828277015d0def4d486fa777d20f082daa7c1e9d6a56f89469de22f3d9c26245b056165fad4f2339b7fcc45647471a4fd
data/.codeclimate.yml CHANGED
@@ -16,8 +16,8 @@ ratings:
16
16
  paths:
17
17
  - "**.js"
18
18
  - "**.jsx"
19
- - "**.module"
20
19
  - "**.rb"
21
20
  exclude_paths:
21
+ - "**/*helper*"
22
22
  - test/
23
23
  - "**/*{.,-}min.js"
data/.rubocop.yml CHANGED
@@ -74,7 +74,7 @@ Style/SpaceInsideBrackets:
74
74
  Style/AndOr:
75
75
  Enabled: false
76
76
 
77
- Style/TrailingComma:
77
+ Style/TrailingCommaInLiteral:
78
78
  Enabled: true
79
79
 
80
80
  Style/SpaceBeforeComma:
@@ -98,7 +98,7 @@ Style/SpaceAfterColon:
98
98
  Style/SpaceAfterComma:
99
99
  Enabled: true
100
100
 
101
- Style/SpaceAfterControlKeyword:
101
+ Style/SpaceAroundKeyword:
102
102
  Enabled: true
103
103
 
104
104
  Style/SpaceAfterNot:
@@ -163,7 +163,7 @@ Style/StringLiterals:
163
163
  EnforcedStyle: double_quotes
164
164
 
165
165
  Metrics/CyclomaticComplexity:
166
- Max: 8
166
+ Max: 10
167
167
 
168
168
  Metrics/LineLength:
169
169
  Max: 128
@@ -214,3 +214,6 @@ Metrics/ParameterLists:
214
214
 
215
215
  Metrics/PerceivedComplexity:
216
216
  Enabled: false
217
+
218
+ Style/Documentation:
219
+ Enabled: false
data/.travis.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.2.0
3
+ - 2.3.0
4
4
  services:
5
5
  - redis-server
data/CHANGELOG.md CHANGED
@@ -1,7 +1,17 @@
1
+ ## 0.1.6
2
+
3
+ - Add Zhong.any_running? for monitoring that any Zhong node has checked in rencently
4
+ - More code cleanup/refactoring.
5
+
1
6
  ## 0.1.5
2
7
 
3
8
  - Improve the API.
4
9
  - Add scheduler test.
10
+ - Add Zhong::Web to see job status, and enable/disable jobs. Activate it with:
11
+ Rails.application.routes.draw do
12
+ # ...
13
+ require "zhong/web"
14
+ mount Zhong::Web => "/zhong" # or wherever
5
15
 
6
16
  ## 0.1.4
7
17
 
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in zhong.gemspec
4
4
  gemspec
data/README.md CHANGED
@@ -45,20 +45,21 @@ Zhong.schedule do
45
45
  true
46
46
  end
47
47
 
48
+ on(:before_run) do |job, time|
49
+ puts "running #{job}"
50
+ true # can conditionally run a specific job
51
+ end
52
+
53
+ on(:after_run) do |job, time, ran|
54
+ puts "#{job} ran?: #{ran}"
55
+ end
56
+
48
57
  error_handler do |e, job|
49
- puts "damn, #{job} messed up: #{e}"
58
+ puts "dang, #{job} messed up: #{e}"
50
59
  end
51
60
  end
52
61
  ```
53
62
 
54
- ## TODO
55
- - better logging
56
- - error handling
57
- - tests
58
- - examples
59
- - callbacks
60
- - generic handler
61
-
62
63
  ## History
63
64
 
64
65
  View the [changelog](https://github.com/nickelser/zhong/blob/master/CHANGELOG.md).
data/Rakefile CHANGED
@@ -1,8 +1,9 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
3
 
4
- task default: :test
5
- Rake::TestTask.new do |t|
4
+ Rake::TestTask.new(:test) do |t|
6
5
  t.libs << "test"
7
- t.pattern = "test/**/*_test.rb"
6
+ t.pattern = "test/**/test_*.rb"
8
7
  end
8
+
9
+ task default: :test
data/lib/zhong/at.rb CHANGED
@@ -24,7 +24,7 @@ module Zhong
24
24
  parse_at(at, grace)
25
25
  end
26
26
  rescue ArgumentError
27
- fail FailedToParse, at
27
+ raise FailedToParse, at
28
28
  end
29
29
 
30
30
  def self.deserialize(at)
@@ -37,7 +37,7 @@ module Zhong
37
37
  @wday = wday
38
38
  @grace = grace
39
39
 
40
- fail ArgumentError unless valid?
40
+ raise ArgumentError unless valid?
41
41
  end
42
42
 
43
43
  def next_at(time = Time.now)
@@ -46,11 +46,11 @@ module Zhong
46
46
  grace_cutoff = time.change(sec: 0) - @grace
47
47
 
48
48
  if at_time < grace_cutoff
49
- if @wday.nil?
50
- at_time += @hour.nil? ? 1.hour : 1.day
51
- else
52
- at_time += 1.week
53
- end
49
+ at_time + if @wday.nil?
50
+ @hour.nil? ? 1.hour : 1.day
51
+ else
52
+ 1.week
53
+ end
54
54
  else
55
55
  at_time
56
56
  end
@@ -58,10 +58,7 @@ module Zhong
58
58
 
59
59
  def to_s
60
60
  str = "#{formatted_time(@hour)}:#{formatted_time(@minute)}"
61
-
62
- if @wday
63
- str += " on #{WDAYS.invert[@wday].capitalize}"
64
- end
61
+ str += " on #{WDAYS.invert[@wday].capitalize}" if @wday
65
62
 
66
63
  str
67
64
  end
@@ -74,57 +71,59 @@ module Zhong
74
71
  MessagePack.pack(as_json)
75
72
  end
76
73
 
77
- protected
78
-
79
- def formatted_time(t)
80
- if t.nil?
81
- "**"
74
+ def self.parse_serialized(at)
75
+ if at.is_a?(Array)
76
+ MultiAt.new(at.map { |a| parse_serialized(a) })
82
77
  else
83
- t.to_s.rjust(2, "0")
78
+ new(minute: at["m"], hour: at["h"], wday: at["w"], grace: at["g"])
84
79
  end
85
80
  end
86
-
87
- def ==(o)
88
- o.class == self.class && o.state == state
89
- end
90
-
91
- def state
92
- [@minute, @hour, @wday]
93
- end
94
-
95
- private
81
+ private_class_method :parse_serialized
96
82
 
97
83
  def self.parse_at(at, grace)
98
84
  case at
99
85
  when /\A([[:alpha:]]+)\s+(.*)\z/
100
86
  wday = WDAYS[$1.downcase]
101
87
 
102
- if wday
103
- parsed_time = parse_at($2, grace)
104
- parsed_time.wday = wday
105
- parsed_time
106
- else
107
- fail FailedToParse, at
108
- end
88
+ raise FailedToParse, at unless wday
89
+
90
+ parsed_time = parse_at($2, grace)
91
+ parsed_time.wday = wday
92
+ parsed_time
109
93
  when /\A(\d{1,2}):(\d\d)\z/
110
94
  new(minute: $2.to_i, hour: $1.to_i, grace: grace)
111
95
  when /\A\*{1,2}:(\d\d)\z/
112
96
  new(minute: $1.to_i, grace: grace)
113
97
  when /\A(\d{1,2}):\*{1,2}\z/
114
98
  new(hour: $1.to_i, grace: grace)
99
+ when /\A\*{1,2}:\*{1,2}\z/
100
+ new(grace: grace)
115
101
  else
116
- fail FailedToParse, at
102
+ raise FailedToParse, at
117
103
  end
118
104
  end
105
+ private_class_method :parse_at
119
106
 
120
- def self.parse_serialized(at)
121
- if at.is_a?(Array)
122
- MultiAt.new(at.map { |a| parse_serialized(a) })
107
+ protected
108
+
109
+ def formatted_time(t)
110
+ if t.nil?
111
+ "**"
123
112
  else
124
- new(minute: at["m"], hour: at["h"], wday: at["w"], grace: at["g"])
113
+ t.to_s.rjust(2, "0")
125
114
  end
126
115
  end
127
116
 
117
+ def ==(other)
118
+ other.class == self.class && other.state == state
119
+ end
120
+
121
+ def state
122
+ [@minute, @hour, @wday]
123
+ end
124
+
125
+ private
126
+
128
127
  def at_time_hour_minute_adjusted(time)
129
128
  if @minute && @hour
130
129
  time.change(hour: @hour, min: @minute)
@@ -155,8 +154,8 @@ module Zhong
155
154
  @ats = ats
156
155
  end
157
156
 
158
- def ==(o)
159
- o.class == self.class && @ats == o.ats
157
+ def ==(other)
158
+ other.class == self.class && @ats == other.ats
160
159
  end
161
160
 
162
161
  def next_at(time = Time.now)
data/lib/zhong/every.rb CHANGED
@@ -15,20 +15,17 @@ module Zhong
15
15
  def initialize(period)
16
16
  @period = period
17
17
 
18
- fail "`every` must be >= 1 second" unless valid?
18
+ raise "`every` must be >= 1 second" unless valid?
19
19
  end
20
20
 
21
21
  def to_s
22
22
  EVERY_KEYWORDS.to_a.reverse.each do |friendly, period|
23
- if @period % period == 0
24
- rem = @period / period
25
-
26
- if rem == 1
27
- return "#{rem} #{friendly}"
28
- else
29
- return "#{rem} #{friendly}s"
30
- end
31
- end
23
+ next unless @period % period == 0
24
+
25
+ rem = @period / period
26
+
27
+ return "#{rem} #{friendly}" if rem == 1
28
+ return "#{rem} #{friendly}s"
32
29
  end
33
30
 
34
31
  "#{@period.to_i} second#{@period.to_i == 1 ? '' : 's'}"
@@ -49,11 +46,11 @@ module Zhong
49
46
  when String, Symbol
50
47
  key = every.downcase.to_sym
51
48
 
52
- fail FailedToParse, every unless EVERY_KEYWORDS.key?(key)
49
+ raise FailedToParse, every unless EVERY_KEYWORDS.key?(key)
53
50
 
54
51
  new(EVERY_KEYWORDS[key])
55
52
  else
56
- fail FailedToParse, every
53
+ raise FailedToParse, every
57
54
  end
58
55
  end
59
56
  end
data/lib/zhong/job.rb CHANGED
@@ -11,7 +11,7 @@ module Zhong
11
11
  @at = config[:at] ? At.parse(config[:at], grace: config.fetch(:grace, 15.minutes)) : nil
12
12
  @every = config[:every] ? Every.parse(config[:every]) : nil
13
13
 
14
- fail "must specific either `at` or `every` for job: #{self}" unless @at || @every
14
+ raise "must specific either `at` or `every` for job: #{self}" unless @at || @every
15
15
 
16
16
  @block = block
17
17
 
@@ -39,6 +39,7 @@ module Zhong
39
39
 
40
40
  locked = false
41
41
  errored = false
42
+ ran = false
42
43
 
43
44
  begin
44
45
  redis_lock.lock do
@@ -61,6 +62,7 @@ module Zhong
61
62
  if @block
62
63
  begin
63
64
  @block.call
65
+ ran = true
64
66
  rescue => boom
65
67
  logger.error "#{self} failed: #{boom}"
66
68
  error_handler.call(boom, self) if error_handler
@@ -77,6 +79,8 @@ module Zhong
77
79
  @running = false
78
80
 
79
81
  logger.info "unable to acquire exclusive run lock: #{self}" if !locked && !errored
82
+
83
+ ran
80
84
  end
81
85
 
82
86
  def running?
@@ -97,7 +101,7 @@ module Zhong
97
101
  end
98
102
 
99
103
  def disabled?
100
- !!@redis.get(disabled_key)
104
+ !@redis.get(disabled_key).nil?
101
105
  end
102
106
 
103
107
  def to_s
@@ -19,10 +19,19 @@ module Zhong
19
19
  @tz = @config[:tz]
20
20
  @category = nil
21
21
  @error_handler = nil
22
+ @running = false
23
+ end
24
+
25
+ def clear
26
+ raise "unable to clear while running; run Zhong.stop first" if @running
27
+
28
+ @jobs = {}
29
+ @callbacks = {}
30
+ @category = nil
22
31
  end
23
32
 
24
33
  def category(name)
25
- fail "cannot nest categories: #{name} would be nested in #{@category} (#{caller.first})" if @category
34
+ raise "cannot nest categories: #{name} would be nested in #{@category} (#{caller.first})" if @category
26
35
 
27
36
  @category = name.to_s
28
37
 
@@ -32,7 +41,7 @@ module Zhong
32
41
  end
33
42
 
34
43
  def every(period, name, opts = {}, &block)
35
- fail "must specify a period for #{name} (#{caller.first})" unless period
44
+ raise "must specify a period for #{name} (#{caller.first})" unless period
36
45
 
37
46
  job = Job.new(name, opts.merge(@config).merge(every: period, category: @category), &block)
38
47
 
@@ -50,7 +59,7 @@ module Zhong
50
59
  end
51
60
 
52
61
  def on(event, &block)
53
- fail "unknown callback #{event}" unless [:before_tick, :after_tick, :before_run, :after_run].include?(event.to_sym)
62
+ raise "unknown callback #{event}" unless [:before_tick, :after_tick, :before_run, :after_run].include?(event.to_sym)
54
63
  (@callbacks[event.to_sym] ||= []) << block
55
64
  end
56
65
 
@@ -61,7 +70,11 @@ module Zhong
61
70
 
62
71
  trap_signals
63
72
 
73
+ raise "already running" if @running
74
+
64
75
  loop do
76
+ @running = true
77
+
65
78
  if fire_callbacks(:before_tick)
66
79
  now = redis_time
67
80
 
@@ -83,14 +96,26 @@ module Zhong
83
96
  break if @stop
84
97
  end
85
98
 
99
+ @running = false
100
+
86
101
  Thread.new { @logger.info "stopped" }.join
87
102
  end
88
103
 
89
104
  def stop
90
- Thread.new { @logger.error "stopping" } # thread necessary due to trap context
105
+ Thread.new { @logger.error "stopping" } if @running # thread necessary due to trap context
91
106
  @stop = true
92
107
  end
93
108
 
109
+ def find_by_name(job_name)
110
+ @jobs[Digest::SHA256.hexdigest(job_name)]
111
+ end
112
+
113
+ def redis_time
114
+ s, ms = @redis.time # returns [seconds since epoch, microseconds]
115
+ now = Time.at(s + ms / (10**6))
116
+ @tz ? now.in_time_zone(@tz) : now
117
+ end
118
+
94
119
  private
95
120
 
96
121
  TRAPPED_SIGNALS = %w(QUIT INT TERM).freeze
@@ -107,9 +132,9 @@ module Zhong
107
132
  def run_job(job, time = redis_time)
108
133
  return unless fire_callbacks(:before_run, job, time)
109
134
 
110
- job.run(time, error_handler)
135
+ ran = job.run(time, error_handler)
111
136
 
112
- fire_callbacks(:after_run, job, time)
137
+ fire_callbacks(:after_run, job, time, ran)
113
138
  end
114
139
 
115
140
  def heartbeat(time)
@@ -130,11 +155,5 @@ module Zhong
130
155
  GC.start
131
156
  sleep(1.0 - Time.now.subsec + 0.0001)
132
157
  end
133
-
134
- def redis_time
135
- s, ms = @redis.time # returns [seconds since epoch, microseconds]
136
- now = Time.at(s + ms / (10**6))
137
- @tz ? now.in_time_zone(@tz) : now
138
- end
139
158
  end
140
159
  end
data/lib/zhong/util.rb ADDED
@@ -0,0 +1,13 @@
1
+ module Zhong
2
+ module Util
3
+ def safe_mget(keys)
4
+ if keys.empty?
5
+ {}
6
+ else
7
+ Zhong.redis.mapped_mget(*keys)
8
+ end
9
+ end
10
+
11
+ module_function :safe_mget
12
+ end
13
+ end
data/lib/zhong/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Zhong
2
- VERSION = "0.1.5"
2
+ VERSION = "0.1.6".freeze
3
3
  end
data/lib/zhong/web.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require "tilt/erubis"
3
2
  require "erb"
4
3
  require "sinatra/base"
5
4
 
@@ -29,21 +28,21 @@ module Zhong
29
28
 
30
29
  helpers WebHelpers
31
30
 
32
- get '/' do
31
+ get "/" do
33
32
  index
34
33
 
35
34
  erb :index
36
35
  end
37
36
 
38
- post '/' do
39
- if params['disable']
40
- if job = Zhong.jobs[params['disable']]
41
- job.disable
42
- end
43
- elsif params['enable']
44
- if job = Zhong.jobs[params['enable']]
45
- job.enable
46
- end
37
+ post "/" do
38
+ if params["disable"]
39
+ job = Zhong.jobs[params["disable"]]
40
+
41
+ job.disable if job
42
+ elsif params["enable"]
43
+ job = Zhong.jobs[params["enable"]]
44
+
45
+ job.enable if job
47
46
  end
48
47
 
49
48
  index
@@ -55,30 +54,18 @@ module Zhong
55
54
  @jobs = Zhong.jobs.values
56
55
  @last_runs = zhong_mget(@jobs, "last_ran")
57
56
  @disabled = zhong_mget(@jobs, "disabled")
58
- @hosts = safe_mget(Zhong.redis.scan_each(match: "zhong:heartbeat:*").to_a).map do |k, v|
59
- host, pid = k.split("zhong:heartbeat:", 2)[1].split("#", 2)
60
- {host: host, pid: pid, last_seen: Time.at(v.to_i)}
61
- end
57
+ @hosts = Zhong.all_heartbeats
62
58
  end
63
59
 
64
60
  def zhong_mget(jobs, key)
65
61
  keys = jobs.map(&:to_s)
66
- ret = safe_mget(keys.map { |j| "zhong:#{key}:#{j}" })
62
+ ret = Zhong::Util.safe_mget(keys.map { |j| "zhong:#{key}:#{j}" })
67
63
  Hash[keys.map { |j| [j, ret["zhong:#{key}:#{j}"]] }]
68
64
  end
69
-
70
- def safe_mget(keys)
71
- if keys.length > 0
72
- Zhong.redis.mapped_mget(*keys)
73
- else
74
- {}
75
- end
76
- end
77
65
  end
78
66
  end
79
67
 
80
- if defined?(::ActionDispatch::Request::Session) &&
81
- !::ActionDispatch::Request::Session.respond_to?(:each)
68
+ if defined?(::ActionDispatch::Request::Session) && !::ActionDispatch::Request::Session.respond_to?(:each)
82
69
  # mperham/sidekiq#2460
83
70
  # Rack apps can't reuse the Rails session store without
84
71
  # this monkeypatch
@@ -7,7 +7,7 @@ module Zhong
7
7
  # capture method from sinatra-contrib library.
8
8
  def capture(&block)
9
9
  block.call
10
- eval('', block.binding)
10
+ eval("", block.binding)
11
11
  end
12
12
 
13
13
  def root_path
@@ -15,12 +15,12 @@ module Zhong
15
15
  end
16
16
 
17
17
  def current_path
18
- @current_path ||= request.path_info.gsub(/^\//,'')
18
+ @current_path ||= request.path_info.gsub(%r(^\/),"")
19
19
  end
20
20
 
21
21
  def relative_time(time)
22
22
  if time
23
- %{<time datetime="#{time.getutc.iso8601}">#{time}</time>}
23
+ %(<time datetime="#{time.getutc.iso8601}">#{time}</time>)
24
24
  else
25
25
  "never"
26
26
  end
@@ -41,14 +41,12 @@ module Zhong
41
41
  end
42
42
 
43
43
  def to_display(arg)
44
+ arg.inspect
45
+ rescue
44
46
  begin
45
- arg.inspect
46
- rescue
47
- begin
48
- arg.to_s
49
- rescue => ex
50
- "Cannot display argument: [#{ex.class.name}] #{ex.message}"
51
- end
47
+ arg.to_s
48
+ rescue => ex
49
+ "Cannot display argument: [#{ex.class.name}] #{ex.message}"
52
50
  end
53
51
  end
54
52
 
@@ -59,8 +57,8 @@ module Zhong
59
57
  return number
60
58
  end
61
59
 
62
- options = {delimiter: ',', separator: '.'}
63
- parts = number.to_s.to_str.split('.')
60
+ options = {delimiter: ",", separator: "."}
61
+ parts = number.to_s.to_str.split(".")
64
62
  parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
65
63
  parts.join(options[:separator])
66
64
  end
@@ -68,13 +66,13 @@ module Zhong
68
66
  def h(text)
69
67
  ::Rack::Utils.escape_html(text)
70
68
  rescue ArgumentError => e
71
- raise unless e.message.eql?('invalid byte sequence in UTF-8')
72
- text.encode!('UTF-16', 'UTF-8', invalid: :replace, replace: '').encode!('UTF-8', 'UTF-16')
69
+ raise unless e.message.eql?("invalid byte sequence in UTF-8")
70
+ text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16")
73
71
  retry
74
72
  end
75
73
 
76
74
  def environment_title_prefix
77
- environment = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
75
+ environment = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
78
76
 
79
77
  "[#{environment.upcase}] " unless environment == "production"
80
78
  end
data/test/helper.rb ADDED
@@ -0,0 +1,33 @@
1
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2
+
3
+ if ENV["CODECLIMATE_REPO_TOKEN"]
4
+ require "codeclimate-test-reporter"
5
+ ::SimpleCov.add_filter "helper"
6
+ CodeClimate::TestReporter.start
7
+ end
8
+
9
+ require "zhong"
10
+ require "minitest/autorun"
11
+ require "rack/test"
12
+
13
+ ENV["RACK_ENV"] = ENV["RAILS_ENV"] = "test"
14
+
15
+ def assert_contains(expected_substring, string, *args)
16
+ assert string.include?(expected_substring), *args
17
+ end
18
+
19
+ Zhong.logger = begin
20
+ l = Logger.new(STDOUT)
21
+ l.level = Logger::ERROR
22
+ l
23
+ end
24
+
25
+ Zhong.redis = Redis.new(url: ENV["REDIS_URL"] || "redis://localhost/13")
26
+
27
+ def test_default_config
28
+ @default_config ||= {
29
+ redis: Zhong.redis,
30
+ logger: Zhong.logger,
31
+ long_running_timeout: 10.seconds
32
+ }
33
+ end
@@ -1,4 +1,4 @@
1
- require "test_helper"
1
+ require_relative "helper"
2
2
 
3
3
  # Many At tests lifted from Clockwork (thanks Clockwork!)
4
4
  class TestAt < Minitest::Test
@@ -139,6 +139,32 @@ class TestAt < Minitest::Test
139
139
  assert_equal time_in_day(8, 20, 3), at.next_at(time_in_day(12, 31, 2))
140
140
  end
141
141
 
142
+ def test_to_s
143
+ at = Zhong::At.parse("thr 12:59")
144
+
145
+ assert_equal "12:59 on Thr", at.to_s
146
+
147
+ at = Zhong::At.parse(["8:20", "tues 12:30"])
148
+
149
+ assert_equal "08:20, 12:30 on Tues", at.to_s
150
+ end
151
+
152
+ def test_as_json
153
+ at = Zhong::At.parse("tues 23:01")
154
+
155
+ assert_equal({m: 1, h: 23, w: 2, g: 0.seconds}, at.as_json)
156
+
157
+ at = Zhong::At.parse(["8:**", "sun 12:30"])
158
+
159
+ assert_equal([{m: nil, h: 8, w: nil, g: 0.seconds}, {m: 30, h: 12, w: 0, g: 0.seconds}], at.as_json)
160
+ end
161
+
162
+ def test_serialize
163
+ at = Zhong::At.parse(["8:20", "tues 12:30"])
164
+
165
+ assert_equal Zhong::At.deserialize(at.serialize), at
166
+ end
167
+
142
168
  def test_invalid_time_32
143
169
  assert_raises Zhong::At::FailedToParse do
144
170
  Zhong::At.parse("32:00")
@@ -1,4 +1,4 @@
1
- require "test_helper"
1
+ require_relative "helper"
2
2
 
3
3
  class TestEvery < Minitest::Test
4
4
  def time_in_day(hour, minute, day = 0, sec = 0)
data/test/test_job.rb ADDED
@@ -0,0 +1,74 @@
1
+ require_relative "helper"
2
+
3
+ class TestJob < Minitest::Test
4
+ def test_initialize
5
+ job = Zhong::Job.new("test_initialize", {at: "12:00"}.merge(test_default_config))
6
+
7
+ assert job
8
+ assert !job.running?
9
+ assert_equal "test_initialize", job.to_s
10
+ end
11
+
12
+ def test_should_run
13
+ sleep 1
14
+
15
+ job = Zhong::Job.new("test_should_run", {every: 1.second}.merge(test_default_config))
16
+
17
+ assert_equal true, job.run?
18
+ end
19
+
20
+ def test_run
21
+ success_counter = Queue.new
22
+ job = Zhong::Job.new("test_run", {every: 1.second}.merge(test_default_config)) { success_counter << 1 }
23
+ now = Time.now
24
+
25
+ assert_equal 0, success_counter.size
26
+ assert_equal true, job.run?(now)
27
+ job.run(now)
28
+ assert_equal false, job.run?(now)
29
+ assert_equal 1, success_counter.size
30
+ end
31
+
32
+ def test_run_at
33
+ success_counter = Queue.new
34
+ job = Zhong::Job.new("test_run_at", {every: 1.second, at: ["**:**"]}.merge(test_default_config)) { success_counter << 1 }
35
+ now = Time.now
36
+
37
+ assert_equal 0, success_counter.size
38
+ assert_equal true, job.run?(now)
39
+ job.run(now)
40
+ assert_equal false, job.run?(now)
41
+ assert_equal 1, success_counter.size
42
+ end
43
+
44
+ def test_run_at_change
45
+ success_counter = Queue.new
46
+ job = Zhong::Job.new("test_run_at_change", {every: 1.second, at: ["**:**"]}.merge(test_default_config)) { success_counter << 1 }
47
+ now = Time.now
48
+
49
+ assert_equal 0, success_counter.size
50
+ assert_equal true, job.run?(now)
51
+ job.run(now)
52
+ assert_equal false, job.run?(now)
53
+ assert_equal 1, success_counter.size
54
+
55
+ job = Zhong::Job.new("test_run_at_change", {every: 1.second, at: ["**:**", "**:**"]}.merge(test_default_config)) { success_counter << 1 }
56
+ assert_equal true, job.run?(now)
57
+ job.run(now)
58
+ assert_equal false, job.run?(now)
59
+ assert_equal 2, success_counter.size
60
+ end
61
+
62
+ def test_disable
63
+ success_counter = Queue.new
64
+ job = Zhong::Job.new("test_disable", {every: 1.second}.merge(test_default_config)) { success_counter << 1 }
65
+ now = Time.now
66
+
67
+ job.disable
68
+
69
+ assert_equal true, job.run?(now)
70
+ job.run(now)
71
+ assert_equal true, job.run?(now)
72
+ assert_equal 0, success_counter.size
73
+ end
74
+ end
@@ -0,0 +1,23 @@
1
+ require_relative "helper"
2
+
3
+ class TestLibrary < Minitest::Test
4
+ def test_that_it_has_a_version_number
5
+ refute_nil ::Zhong::VERSION
6
+ end
7
+
8
+ def teardown
9
+ Zhong.stop
10
+ sleep 1
11
+ end
12
+
13
+ def test_heartbeats
14
+ Zhong.schedule { nil }
15
+ t = Thread.new { Zhong.start }
16
+ sleep(1)
17
+ assert_equal true, Zhong.any_running?
18
+ assert_in_delta Zhong.redis_time.to_f, Time.now.to_f, 0.1
19
+ assert_in_delta Zhong.redis_time.to_f, Zhong.latest_heartbeat.to_f, 1
20
+ Zhong.stop
21
+ t.join
22
+ end
23
+ end
@@ -0,0 +1,80 @@
1
+ require_relative "helper"
2
+
3
+ class TestScheduler < Minitest::Test
4
+ def setup
5
+ Zhong.clear
6
+ end
7
+
8
+ def test_scheduler
9
+ test_one_counter = 0
10
+ test_two_counter = 0
11
+
12
+ Zhong.schedule do
13
+ every(10.seconds, "test_one") { test_one_counter += 1 }
14
+ every(3.seconds, "test_two") { test_two_counter += 1 }
15
+ end
16
+
17
+ t = Thread.new { Zhong.start }
18
+ sleep(7)
19
+ Zhong.stop
20
+ t.join
21
+
22
+ assert_equal 1, test_one_counter
23
+ assert_equal 3, test_two_counter
24
+ end
25
+
26
+ def test_scheduler_categories
27
+ Zhong.schedule do
28
+ category "cat1" do
29
+ every(10.seconds, "cat_test_one") { nil }
30
+ end
31
+ end
32
+
33
+ assert_equal 1, Zhong.jobs.size
34
+ assert_equal "cat1.cat_test_one", Zhong.jobs.values.first.to_s
35
+ end
36
+
37
+ def test_scheduler_nested_category
38
+ assert_raises RuntimeError do
39
+ Zhong.schedule do
40
+ category "cat1" do
41
+ category "cat2"
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def test_scheduler_callbacks
48
+ test_before_tick = 0
49
+ test_after_tick = 0
50
+ test_errors = 0
51
+ test_before_run = 0
52
+ test_after_run = 0
53
+
54
+ Zhong.schedule do
55
+ on(:before_tick) { test_before_tick += 1; true }
56
+ on(:after_tick) { test_after_tick += 1 }
57
+ on(:before_run) { test_before_run += 1; true }
58
+ on(:after_run) { |_, _, ran| test_after_run += 1 if ran }
59
+ error_handler { test_errors += 1 }
60
+
61
+ every(1.second, "test_every_second") { nil }
62
+ every(1.second, "break_every_second") { raise "boom" }
63
+ end
64
+
65
+ t = Thread.new { Zhong.start }
66
+ sleep(3)
67
+ Zhong.stop
68
+ t.join
69
+
70
+ assert_operator 2, :<=, test_before_tick
71
+ assert_operator 4, :>=, test_before_tick
72
+ assert_operator 2, :<=, test_after_tick
73
+ assert_operator 4, :>=, test_after_tick
74
+ assert_operator 6, :<=, test_before_run
75
+ assert_operator 8, :>=, test_before_run
76
+ assert_operator 2, :<=, test_after_run
77
+ assert_operator 4, :>=, test_after_run
78
+ assert_operator 2, :<=, test_errors
79
+ end
80
+ end
data/test/test_web.rb ADDED
@@ -0,0 +1,86 @@
1
+ require_relative "helper"
2
+ require "zhong/web"
3
+ require "tilt/erubis"
4
+
5
+ class TestWeb < Minitest::Test
6
+ include Rack::Test::Methods
7
+
8
+ def app
9
+ Zhong::Web
10
+ end
11
+
12
+ def setup
13
+ Zhong.clear
14
+ end
15
+
16
+ def test_index
17
+ get "/"
18
+ assert last_response.ok?
19
+ assert_contains "<title>[TEST] Zhong</title>", last_response.body
20
+ assert_contains Zhong::VERSION, last_response.body
21
+ end
22
+
23
+ def test_index_job
24
+ Zhong.schedule do
25
+ every(10.minutes, "test_web_job") { nil }
26
+ end
27
+
28
+ get "/"
29
+ assert last_response.ok?
30
+ assert_contains "test_web_job", last_response.body
31
+ assert_contains "every 10 minutes", last_response.body
32
+ end
33
+
34
+ def test_disable_job
35
+ Zhong.schedule do
36
+ every(30.seconds, "test_disable_web_job") { nil }
37
+ end
38
+
39
+ job = Zhong.scheduler.find_by_name("test_disable_web_job")
40
+
41
+ job.enable
42
+
43
+ assert_equal false, job.disabled?
44
+
45
+ post "/", "disable" => job.id
46
+ assert last_response.ok?
47
+ assert_contains "test_disable_web_job", last_response.body
48
+ assert_contains "every 30 seconds", last_response.body
49
+ assert_contains 'name="enable"', last_response.body
50
+ assert_equal true, job.disabled?
51
+ end
52
+
53
+ def test_enable_job
54
+ Zhong.schedule do
55
+ every(12.hours, "test_enable_web_job") { nil }
56
+ end
57
+
58
+ job = Zhong.scheduler.find_by_name("test_enable_web_job")
59
+
60
+ job.disable
61
+
62
+ assert_equal true, job.disabled?
63
+
64
+ post "/", "enable" => job.id
65
+ assert last_response.ok?
66
+ assert_contains "test_enable_web_job", last_response.body
67
+ assert_contains "every 12 hours", last_response.body
68
+ assert_contains 'name="disable"', last_response.body
69
+ assert_equal false, job.disabled?
70
+ end
71
+
72
+ def test_heartbeat
73
+ hostname = `hostname`.strip
74
+ pid = Process.pid
75
+
76
+ t = Thread.new { Zhong.start }
77
+ sleep(1)
78
+ Zhong.stop
79
+ t.join
80
+
81
+ get "/"
82
+ assert last_response.ok?
83
+ assert_contains hostname, last_response.body
84
+ assert_contains "PID #{pid}", last_response.body
85
+ end
86
+ end
data/zhong.gemspec CHANGED
@@ -32,4 +32,8 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency "rubocop", "~> 0.30.0"
33
33
  spec.add_development_dependency "minitest", "~> 5.5.0"
34
34
  spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4.7"
35
+ spec.add_development_dependency "sinatra", "~> 1.4", ">= 1.4.6"
36
+ spec.add_development_dependency "rack-test", "~> 0.6"
37
+ spec.add_development_dependency "tilt"
38
+ spec.add_development_dependency "erubis"
35
39
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zhong
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Elser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-28 00:00:00.000000000 Z
11
+ date: 2016-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: suo
@@ -136,6 +136,68 @@ dependencies:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
138
  version: 0.4.7
139
+ - !ruby/object:Gem::Dependency
140
+ name: sinatra
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '1.4'
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: 1.4.6
149
+ type: :development
150
+ prerelease: false
151
+ version_requirements: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - "~>"
154
+ - !ruby/object:Gem::Version
155
+ version: '1.4'
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: 1.4.6
159
+ - !ruby/object:Gem::Dependency
160
+ name: rack-test
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '0.6'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '0.6'
173
+ - !ruby/object:Gem::Dependency
174
+ name: tilt
175
+ requirement: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ type: :development
181
+ prerelease: false
182
+ version_requirements: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ - !ruby/object:Gem::Dependency
188
+ name: erubis
189
+ requirement: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ type: :development
195
+ prerelease: false
196
+ version_requirements: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
139
201
  description: Reliable, distributed cron.
140
202
  email:
141
203
  - nick.elser@gmail.com
@@ -165,15 +227,17 @@ files:
165
227
  - lib/zhong/every.rb
166
228
  - lib/zhong/job.rb
167
229
  - lib/zhong/scheduler.rb
230
+ - lib/zhong/util.rb
168
231
  - lib/zhong/version.rb
169
232
  - lib/zhong/web.rb
170
233
  - lib/zhong/web_helpers.rb
171
- - test/at_test.rb
172
- - test/every_test.rb
173
- - test/job_test.rb
174
- - test/library_test.rb
175
- - test/scheduler_test.rb
176
- - test/test_helper.rb
234
+ - test/helper.rb
235
+ - test/test_at.rb
236
+ - test/test_every.rb
237
+ - test/test_job.rb
238
+ - test/test_library.rb
239
+ - test/test_scheduler.rb
240
+ - test/test_web.rb
177
241
  - web/assets/javascript/application.js
178
242
  - web/assets/javascript/vendor.min.js
179
243
  - web/views/index.erb
@@ -203,10 +267,11 @@ signing_key:
203
267
  specification_version: 4
204
268
  summary: Reliable, distributed cron.
205
269
  test_files:
206
- - test/at_test.rb
207
- - test/every_test.rb
208
- - test/job_test.rb
209
- - test/library_test.rb
210
- - test/scheduler_test.rb
211
- - test/test_helper.rb
270
+ - test/helper.rb
271
+ - test/test_at.rb
272
+ - test/test_every.rb
273
+ - test/test_job.rb
274
+ - test/test_library.rb
275
+ - test/test_scheduler.rb
276
+ - test/test_web.rb
212
277
  has_rdoc:
data/test/job_test.rb DELETED
@@ -1,60 +0,0 @@
1
- require "test_helper"
2
-
3
- class TestJob < Minitest::Test
4
- def default_config
5
- @default_config ||= {
6
- redis: Redis.new,
7
- logger: logger,
8
- long_running_timeout: 5.minutes
9
- }
10
- end
11
-
12
- def logger
13
- @logger ||= begin
14
- l = Logger.new(STDOUT)
15
- l.level = Logger::UNKNOWN
16
- l
17
- end
18
- end
19
-
20
- def test_initialize
21
- job = Zhong::Job.new("test_initialize", {at: "12:00"}.merge(default_config))
22
-
23
- assert job
24
- assert !job.running?
25
- assert_equal "test_initialize", job.to_s
26
- end
27
-
28
- def test_should_run
29
- sleep 1
30
-
31
- job = Zhong::Job.new("test_should_run", {every: 1.second}.merge(default_config))
32
-
33
- assert_equal true, job.run?
34
- end
35
-
36
- def test_run
37
- success_counter = Queue.new
38
- job = Zhong::Job.new("test_run", {every: 1.second}.merge(default_config)) { success_counter << 1 }
39
- now = Time.now
40
-
41
- assert_equal 0, success_counter.size
42
- assert_equal true, job.run?(now)
43
- job.run(now)
44
- assert_equal false, job.run?(now)
45
- assert_equal 1, success_counter.size
46
- end
47
-
48
- def test_disable
49
- success_counter = Queue.new
50
- job = Zhong::Job.new("test_disable", {every: 1.second}.merge(default_config)) { success_counter << 1 }
51
- now = Time.now
52
-
53
- job.disable
54
-
55
- assert_equal true, job.run?(now)
56
- job.run(now)
57
- assert_equal true, job.run?(now)
58
- assert_equal 0, success_counter.size
59
- end
60
- end
data/test/library_test.rb DELETED
@@ -1,7 +0,0 @@
1
- require "test_helper"
2
-
3
- class TestLibrary < Minitest::Test
4
- def test_that_it_has_a_version_number
5
- refute_nil ::Zhong::VERSION
6
- end
7
- end
@@ -1,30 +0,0 @@
1
- require "test_helper"
2
-
3
- class TestScheduler < Minitest::Test
4
- def logger
5
- @logger ||= begin
6
- l = Logger.new(STDOUT)
7
- l.level = Logger::UNKNOWN
8
- l
9
- end
10
- end
11
-
12
- def test_scheduler
13
- test_one_counter = 0
14
- test_two_counter = 0
15
-
16
- Zhong.logger = logger
17
-
18
- Zhong.schedule do
19
- every(10.seconds, "test_one") { test_one_counter += 1 }
20
- every(3.seconds, "test_two") { test_two_counter += 1 }
21
- end
22
-
23
- t = Thread.new { Zhong.start }
24
- sleep(7)
25
- Zhong.stop
26
- t.join
27
- assert_equal 1, test_one_counter
28
- assert_equal 3, test_two_counter
29
- end
30
- end
data/test/test_helper.rb DELETED
@@ -1,11 +0,0 @@
1
- $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2
-
3
- if ENV["CODECLIMATE_REPO_TOKEN"]
4
- require "codeclimate-test-reporter"
5
- CodeClimate::TestReporter.start
6
- end
7
-
8
- require "zhong"
9
- require "minitest/autorun"
10
-
11
- ENV["ZHONG_TEST"] = "true"