rufus-scheduler 2.0.23 → 3.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.
Files changed (53) hide show
  1. data/CHANGELOG.txt +12 -0
  2. data/CREDITS.txt +4 -0
  3. data/README.md +1064 -0
  4. data/Rakefile +1 -4
  5. data/TODO.txt +145 -55
  6. data/lib/rufus/{sc → scheduler}/cronline.rb +46 -17
  7. data/lib/rufus/{sc/version.rb → scheduler/job_array.rb} +56 -4
  8. data/lib/rufus/scheduler/jobs.rb +548 -0
  9. data/lib/rufus/scheduler/util.rb +318 -0
  10. data/lib/rufus/scheduler.rb +502 -26
  11. data/rufus-scheduler.gemspec +30 -4
  12. data/spec/cronline_spec.rb +29 -8
  13. data/spec/error_spec.rb +116 -0
  14. data/spec/job_array_spec.rb +39 -0
  15. data/spec/job_at_spec.rb +58 -0
  16. data/spec/job_cron_spec.rb +67 -0
  17. data/spec/job_every_spec.rb +71 -0
  18. data/spec/job_in_spec.rb +20 -0
  19. data/spec/job_interval_spec.rb +68 -0
  20. data/spec/job_repeat_spec.rb +308 -0
  21. data/spec/job_spec.rb +387 -115
  22. data/spec/lockfile_spec.rb +61 -0
  23. data/spec/parse_spec.rb +203 -0
  24. data/spec/schedule_at_spec.rb +129 -0
  25. data/spec/schedule_cron_spec.rb +66 -0
  26. data/spec/schedule_every_spec.rb +109 -0
  27. data/spec/schedule_in_spec.rb +80 -0
  28. data/spec/schedule_interval_spec.rb +128 -0
  29. data/spec/scheduler_spec.rb +831 -124
  30. data/spec/spec_helper.rb +65 -0
  31. data/spec/threads_spec.rb +75 -0
  32. metadata +64 -65
  33. data/README.rdoc +0 -661
  34. data/lib/rufus/otime.rb +0 -3
  35. data/lib/rufus/sc/jobqueues.rb +0 -160
  36. data/lib/rufus/sc/jobs.rb +0 -471
  37. data/lib/rufus/sc/rtime.rb +0 -363
  38. data/lib/rufus/sc/scheduler.rb +0 -636
  39. data/spec/at_in_spec.rb +0 -47
  40. data/spec/at_spec.rb +0 -125
  41. data/spec/blocking_spec.rb +0 -64
  42. data/spec/cron_spec.rb +0 -134
  43. data/spec/every_spec.rb +0 -304
  44. data/spec/exception_spec.rb +0 -113
  45. data/spec/in_spec.rb +0 -150
  46. data/spec/mutex_spec.rb +0 -159
  47. data/spec/rtime_spec.rb +0 -137
  48. data/spec/schedulable_spec.rb +0 -97
  49. data/spec/spec_base.rb +0 -87
  50. data/spec/stress_schedule_unschedule_spec.rb +0 -159
  51. data/spec/timeout_spec.rb +0 -148
  52. data/test/kjw.rb +0 -113
  53. data/test/t.rb +0 -20
@@ -22,41 +22,517 @@
22
22
  # Made in Japan.
23
23
  #++
24
24
 
25
+ require 'date' if RUBY_VERSION < '1.9.0'
26
+ require 'time'
27
+ require 'thread'
28
+ require 'tzinfo'
29
+ require 'fileutils'
25
30
 
26
- require 'rufus/sc/scheduler'
27
31
 
32
+ module Rufus
28
33
 
29
- module Rufus::Scheduler
34
+ class Scheduler
30
35
 
31
- # Starts and return a new instance of a PlainScheduler.
32
- #
33
- def self.new(opts={})
36
+ require 'rufus/scheduler/util'
37
+ require 'rufus/scheduler/jobs'
38
+ require 'rufus/scheduler/cronline'
39
+ require 'rufus/scheduler/job_array'
34
40
 
35
- PlainScheduler.start_new(opts)
36
- end
41
+ VERSION = '3.0.0'
42
+
43
+ #
44
+ # This error is thrown when the :timeout attribute triggers
45
+ #
46
+ class TimeoutError < StandardError; end
47
+
48
+ #MIN_WORK_THREADS = 7
49
+ MAX_WORK_THREADS = 35
50
+
51
+ attr_accessor :frequency
52
+ attr_reader :started_at
53
+ attr_reader :thread
54
+ attr_reader :thread_key
55
+ attr_reader :mutexes
56
+
57
+ #attr_accessor :min_work_threads
58
+ attr_accessor :max_work_threads
59
+
60
+ attr_accessor :stderr
61
+
62
+ attr_reader :work_queue
63
+
64
+ def initialize(opts={})
65
+
66
+ @opts = opts
67
+
68
+ @started_at = nil
69
+ @paused = false
70
+
71
+ @jobs = JobArray.new
72
+
73
+ @frequency = Rufus::Scheduler.parse(opts[:frequency] || 0.300)
74
+ @mutexes = {}
75
+
76
+ @work_queue = Queue.new
37
77
 
38
- # A quick way to get a scheduler up an running
39
- #
40
- # require 'rubygems'
41
- # s = Rufus::Scheduler.start_new
42
- #
43
- # If EventMachine is present and running will create an EmScheduler, else
44
- # it will create a PlainScheduler instance.
45
- #
46
- def self.start_new(opts={})
47
-
48
- if defined?(EM) and EM.reactor_running?
49
- EmScheduler.start_new(opts)
50
- else
51
- PlainScheduler.start_new(opts)
78
+ #@min_work_threads = opts[:min_work_threads] || MIN_WORK_THREADS
79
+ @max_work_threads = opts[:max_work_threads] || MAX_WORK_THREADS
80
+
81
+ @stderr = $stderr
82
+
83
+ @thread_key = "rufus_scheduler_#{self.object_id}"
84
+
85
+ consider_lockfile || return
86
+
87
+ start
88
+ end
89
+
90
+ # Returns a singleton Rufus::Scheduler instance
91
+ #
92
+ def self.singleton(opts={})
93
+
94
+ @singleton ||= Rufus::Scheduler.new(opts)
95
+ end
96
+
97
+ # Alias for Rufus::Scheduler.singleton
98
+ #
99
+ def self.s(opts={}); singleton(opts); end
100
+
101
+ # Releasing the gem would probably require redirecting .start_new to
102
+ # .new and emit a simple deprecation message.
103
+ #
104
+ # For now, let's assume the people pointing at rufus-scheduler/master
105
+ # on GitHub know what they do...
106
+ #
107
+ def self.start_new
108
+
109
+ fail "this is rufus-scheduler 3.0, use .new instead of .start_new"
110
+ end
111
+
112
+ def shutdown(opt=nil)
113
+
114
+ @started_at = nil
115
+
116
+ jobs.each { |j| j.unschedule }
117
+
118
+ @work_queue.clear
119
+
120
+ if opt == :wait
121
+ join_all_work_threads
122
+ elsif opt == :kill
123
+ kill_all_work_threads
124
+ end
125
+
126
+ @lockfile.flock(File::LOCK_UN) if @lockfile
127
+ end
128
+
129
+ alias stop shutdown
130
+
131
+ def uptime
132
+
133
+ @started_at ? Time.now - @started_at : nil
134
+ end
135
+
136
+ def uptime_s
137
+
138
+ self.class.to_duration(uptime)
139
+ end
140
+
141
+ def join
142
+
143
+ @thread.join
144
+ end
145
+
146
+ def paused?
147
+
148
+ @paused
149
+ end
150
+
151
+ def pause
152
+
153
+ @paused = true
154
+ end
155
+
156
+ def resume
157
+
158
+ @paused = false
52
159
  end
53
- end
54
160
 
55
- # Returns true if the given string seems to be a cron string.
56
- #
57
- def self.is_cron_string(s)
161
+ #--
162
+ # scheduling methods
163
+ #++
164
+
165
+ def at(time, callable=nil, opts={}, &block)
166
+
167
+ do_schedule(:once, time, callable, opts, opts[:job], block)
168
+ end
169
+
170
+ def schedule_at(time, callable=nil, opts={}, &block)
171
+
172
+ do_schedule(:once, time, callable, opts, true, block)
173
+ end
174
+
175
+ def in(duration, callable=nil, opts={}, &block)
176
+
177
+ do_schedule(:once, duration, callable, opts, opts[:job], block)
178
+ end
179
+
180
+ def schedule_in(duration, callable=nil, opts={}, &block)
181
+
182
+ do_schedule(:once, duration, callable, opts, true, block)
183
+ end
184
+
185
+ def every(duration, callable=nil, opts={}, &block)
186
+
187
+ do_schedule(:every, duration, callable, opts, opts[:job], block)
188
+ end
189
+
190
+ def schedule_every(duration, callable=nil, opts={}, &block)
191
+
192
+ do_schedule(:every, duration, callable, opts, true, block)
193
+ end
194
+
195
+ def interval(duration, callable=nil, opts={}, &block)
196
+
197
+ do_schedule(:interval, duration, callable, opts, opts[:job], block)
198
+ end
199
+
200
+ def schedule_interval(duration, callable=nil, opts={}, &block)
201
+
202
+ do_schedule(:interval, duration, callable, opts, true, block)
203
+ end
204
+
205
+ def cron(cronline, callable=nil, opts={}, &block)
206
+
207
+ do_schedule(:cron, cronline, callable, opts, opts[:job], block)
208
+ end
209
+
210
+ def schedule_cron(cronline, callable=nil, opts={}, &block)
211
+
212
+ do_schedule(:cron, cronline, callable, opts, true, block)
213
+ end
214
+
215
+ def schedule(arg, callable=nil, opts={}, &block)
216
+
217
+ # TODO: eventually, spare one parse call
218
+
219
+ case Scheduler.parse(arg)
220
+ when CronLine then schedule_cron(arg, callable, opts, &block)
221
+ when Time then schedule_at(arg, callable, opts, &block)
222
+ else schedule_in(arg, callable, opts, &block)
223
+ end
224
+ end
225
+
226
+ def repeat(arg, callable=nil, opts={}, &block)
227
+
228
+ # TODO: eventually, spare one parse call
229
+
230
+ case Scheduler.parse(arg)
231
+ when CronLine then schedule_cron(arg, callable, opts, &block)
232
+ else schedule_every(arg, callable, opts, &block)
233
+ end
234
+ end
235
+
236
+ def unschedule(job_or_job_id)
237
+
238
+ job, job_id = fetch(job_or_job_id)
239
+
240
+ fail ArgumentError.new("no job found with id '#{job_id}'") unless job
241
+
242
+ job.unschedule if job
243
+ end
244
+
245
+ #--
246
+ # jobs methods
247
+ #++
248
+
249
+ # Returns all the scheduled jobs
250
+ # (even those right before re-schedule).
251
+ #
252
+ def jobs(opts={})
253
+
254
+ opts = { opts => true } if opts.is_a?(Symbol)
255
+
256
+ jobs = @jobs.to_a
257
+
258
+ if opts[:running]
259
+ jobs = jobs.select { |j| j.running? }
260
+ elsif ! opts[:all]
261
+ jobs = jobs.reject { |j| j.next_time.nil? || j.unscheduled_at }
262
+ end
263
+
264
+ tags = Array(opts[:tag] || opts[:tags]).collect { |t| t.to_s }
265
+ jobs = jobs.reject { |j| tags.find { |t| ! j.tags.include?(t) } }
266
+
267
+ jobs
268
+ end
58
269
 
59
- s.match(/.+ .+ .+ .+ .+/) # well...
270
+ def at_jobs(opts={})
271
+
272
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::AtJob) }
273
+ end
274
+
275
+ def in_jobs(opts={})
276
+
277
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::InJob) }
278
+ end
279
+
280
+ def every_jobs(opts={})
281
+
282
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::EveryJob) }
283
+ end
284
+
285
+ def interval_jobs(opts={})
286
+
287
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::IntervalJob) }
288
+ end
289
+
290
+ def cron_jobs(opts={})
291
+
292
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::CronJob) }
293
+ end
294
+
295
+ def job(job_id)
296
+
297
+ @jobs[job_id]
298
+ end
299
+
300
+ # Returns true if this job is currently scheduled.
301
+ #
302
+ # Takes extra care to answer true if the job is a repeat job
303
+ # currently firing.
304
+ #
305
+ def scheduled?(job_or_job_id)
306
+
307
+ job, job_id = fetch(job_or_job_id)
308
+
309
+ !! (job && job.next_time != nil)
310
+ end
311
+
312
+ # Lists all the threads associated with this scheduler.
313
+ #
314
+ def threads
315
+
316
+ Thread.list.select { |t| t[thread_key] }
317
+ end
318
+
319
+ # Lists all the work threads (the ones actually running the scheduled
320
+ # block code)
321
+ #
322
+ # Accepts a query option, which can be set to:
323
+ # * :all (default), returns all the threads that are work threads
324
+ # or are currently running a job
325
+ # * :active, returns all threads that are currenly running a job
326
+ # * :vacant, returns the threads that are not running a job
327
+ #
328
+ # If, thanks to :blocking => true, a job is scheduled to monopolize the
329
+ # main scheduler thread, that thread will get returned when :active or
330
+ # :all.
331
+ #
332
+ def work_threads(query=:all)
333
+
334
+ ts =
335
+ threads.select { |t|
336
+ t[:rufus_scheduler_job] || t[:rufus_scheduler_work_thread]
337
+ }
338
+
339
+ case query
340
+ when :active then ts.select { |t| t[:rufus_scheduler_job] }
341
+ when :vacant then ts.reject { |t| t[:rufus_scheduler_job] }
342
+ else ts
343
+ end
344
+ end
345
+
346
+ def running_jobs(opts={})
347
+
348
+ jobs(opts.merge(:running => true))
349
+ end
350
+
351
+ def on_error(job, err)
352
+
353
+ pre = err.object_id.to_s
354
+
355
+ stderr.puts("{ #{pre} rufus-scheduler intercepted an error:")
356
+ stderr.puts(" #{pre} job:")
357
+ stderr.puts(" #{pre} #{job.class} #{job.original.inspect} #{job.opts.inspect}")
358
+ stderr.puts(" #{pre} error:")
359
+ stderr.puts(" #{pre} #{err.object_id}")
360
+ stderr.puts(" #{pre} #{err.class}")
361
+ stderr.puts(" #{pre} #{err}")
362
+ err.backtrace.each do |l|
363
+ stderr.puts(" #{pre} #{l}")
364
+ end
365
+ stderr.puts("} #{pre} .")
366
+
367
+ rescue => e
368
+
369
+ stderr.puts("failure in #on_error itself:")
370
+ stderr.puts(e.inspect)
371
+ stderr.puts(e.backtrace)
372
+
373
+ ensure
374
+
375
+ stderr.flush
376
+ end
377
+
378
+ protected
379
+
380
+ # Returns [ job, job_id ]
381
+ #
382
+ def fetch(job_or_job_id)
383
+
384
+ if job_or_job_id.respond_to?(:job_id)
385
+ [ job_or_job_id, job_or_job_id.job_id ]
386
+ else
387
+ [ job(job_or_job_id), job_or_job_id ]
388
+ end
389
+ end
390
+
391
+ def consider_lockfile
392
+
393
+ @lockfile = nil
394
+
395
+ return true unless f = @opts[:lockfile]
396
+
397
+ raise ArgumentError.new(
398
+ ":lockfile argument must be a string, not a #{f.class}"
399
+ ) unless f.is_a?(String)
400
+
401
+ FileUtils.mkdir_p(File.dirname(f))
402
+
403
+ f = File.new(f, File::RDWR | File::CREAT)
404
+ locked = f.flock(File::LOCK_NB | File::LOCK_EX)
405
+
406
+ return false unless locked
407
+
408
+ now = Time.now
409
+
410
+ f.print("pid: #{$$}, ")
411
+ f.print("scheduler.object_id: #{self.object_id}, ")
412
+ f.print("time: #{now}, ")
413
+ f.print("timestamp: #{now.to_f}")
414
+ f.flush
415
+
416
+ @lockfile = f
417
+
418
+ true
419
+ end
420
+
421
+ def terminate_all_jobs
422
+
423
+ jobs.each { |j| j.unschedule }
424
+
425
+ sleep 0.01 while running_jobs.size > 0
426
+ end
427
+
428
+ def join_all_work_threads
429
+
430
+ work_threads.size.times { @work_queue << :sayonara }
431
+
432
+ work_threads.each { |t| t.join }
433
+
434
+ @work_queue.clear
435
+ end
436
+
437
+ def kill_all_work_threads
438
+
439
+ work_threads.each { |t| t.kill }
440
+ end
441
+
442
+ #def free_all_work_threads
443
+ #
444
+ # work_threads.each { |t| t.raise(KillSignal) }
445
+ #end
446
+
447
+ def start
448
+
449
+ @started_at = Time.now
450
+
451
+ @thread =
452
+ Thread.new do
453
+
454
+ while @started_at do
455
+
456
+ unschedule_jobs
457
+ trigger_jobs unless @paused
458
+ timeout_jobs
459
+
460
+ sleep(@frequency)
461
+ end
462
+ end
463
+
464
+ @thread[@thread_key] = true
465
+ @thread[:rufus_scheduler] = self
466
+ @thread[:name] = @opts[:thread_name] || "#{@thread_key}_scheduler"
467
+ end
468
+
469
+ def unschedule_jobs
470
+
471
+ @jobs.delete_unscheduled
472
+ end
473
+
474
+ def trigger_jobs
475
+
476
+ now = Time.now
477
+
478
+ @jobs.each(now) do |job|
479
+
480
+ job.trigger(now)
481
+ end
482
+ end
483
+
484
+ def timeout_jobs
485
+
486
+ work_threads(:active).each do |t|
487
+
488
+ job = t[:rufus_scheduler_job]
489
+ to = t[:rufus_scheduler_timeout]
490
+
491
+ next unless job && to
492
+ # thread might just have become inactive (job -> nil)
493
+
494
+ ts = t[:rufus_scheduler_time]
495
+ to = to.is_a?(Time) ? to : ts + to
496
+
497
+ next if to > Time.now
498
+
499
+ t.raise(Rufus::Scheduler::TimeoutError)
500
+ end
501
+ end
502
+
503
+ def do_schedule(job_type, t, callable, opts, return_job_instance, block)
504
+
505
+ raise RuntimeError.new(
506
+ 'cannot schedule, scheduler is down or shutting down'
507
+ ) if @started_at == nil
508
+
509
+ callable, opts = nil, callable if callable.is_a?(Hash)
510
+ return_job_instance ||= opts[:job]
511
+
512
+ job_class =
513
+ case job_type
514
+ when :once
515
+ tt = Rufus::Scheduler.parse(t)
516
+ tt.is_a?(Time) ? AtJob : InJob
517
+ when :every
518
+ EveryJob
519
+ when :interval
520
+ IntervalJob
521
+ when :cron
522
+ CronJob
523
+ end
524
+
525
+ job = job_class.new(self, t, opts, block || callable)
526
+
527
+ raise ArgumentError.new(
528
+ "job frequency (#{job.frequency}) is higher than " +
529
+ "scheduler frequency (#{@frequency})"
530
+ ) if job.respond_to?(:frequency) && job.frequency < @frequency
531
+
532
+ @jobs.push(job)
533
+
534
+ return_job_instance ? job : job.job_id
535
+ end
60
536
  end
61
537
  end
62
538
 
@@ -4,7 +4,7 @@ Gem::Specification.new do |s|
4
4
  s.name = 'rufus-scheduler'
5
5
 
6
6
  s.version = File.read(
7
- File.expand_path('../lib/rufus/sc/version.rb', __FILE__)
7
+ File.expand_path('../lib/rufus/scheduler.rb', __FILE__)
8
8
  ).match(/ VERSION *= *['"]([^'"]+)/)[1]
9
9
 
10
10
  s.platform = Gem::Platform::RUBY
@@ -12,8 +12,8 @@ Gem::Specification.new do |s|
12
12
  s.email = [ 'jmettraux@gmail.com' ]
13
13
  s.homepage = 'http://github.com/jmettraux/rufus-scheduler'
14
14
  s.rubyforge_project = 'rufus'
15
- s.summary = 'job scheduler for Ruby (at, cron, in and every jobs)'
16
15
  s.license = 'MIT'
16
+ s.summary = 'job scheduler for Ruby (at, cron, in and every jobs)'
17
17
 
18
18
  s.description = %{
19
19
  job scheduler for Ruby (at, cron, in and every jobs).
@@ -26,11 +26,37 @@ job scheduler for Ruby (at, cron, in and every jobs).
26
26
  '*.gemspec', '*.txt', '*.rdoc', '*.md'
27
27
  ]
28
28
 
29
- s.add_runtime_dependency 'tzinfo', '>= 0.3.23'
29
+ s.add_runtime_dependency 'tzinfo'
30
30
 
31
31
  s.add_development_dependency 'rake'
32
- s.add_development_dependency 'rspec', '>= 2.7.0'
32
+ s.add_development_dependency 'rspec', '>= 2.13.0'
33
33
 
34
34
  s.require_path = 'lib'
35
+
36
+ s.post_install_message =
37
+ %{
38
+ ***
39
+
40
+ Thanks for installing rufus-scheduler #{s.version}
41
+
42
+ It might not be 100% compatible with rufus-scheduler 2.x.
43
+
44
+ If you encounter issues with this new rufus-scheduler, especially
45
+ if your app worked fine with previous versions of it, you can
46
+
47
+ A) Forget it and peg your Gemfile to rufus-scheduler 2.0.24
48
+
49
+ and / or
50
+
51
+ B) Take some time to carefully report the issue at
52
+ https://github.com/jmettraux/rufus-scheduler/issue
53
+
54
+ For general help about rufus-scheduler, ask via:
55
+ http://stackoverflow.com/questions/ask?tags=rufus-scheduler+ruby
56
+
57
+ Cheers.
58
+
59
+ ***
60
+ }
35
61
  end
36
62
 
@@ -5,13 +5,14 @@
5
5
  # Sat Mar 21 12:55:27 JST 2009
6
6
  #
7
7
 
8
- require 'spec_base'
8
+ require 'tzinfo'
9
+ require 'spec_helper'
9
10
 
10
11
 
11
- describe Rufus::CronLine do
12
+ describe Rufus::Scheduler::CronLine do
12
13
 
13
14
  def cl(cronline_string)
14
- Rufus::CronLine.new(cronline_string)
15
+ Rufus::Scheduler::CronLine.new(cronline_string)
15
16
  end
16
17
 
17
18
  def match(line, time)
@@ -111,7 +112,7 @@ describe Rufus::CronLine do
111
112
  it 'does not support ranges for monthdays (sun#1-sun#2)' do
112
113
 
113
114
  lambda {
114
- Rufus::CronLine.new('* * * * sun#1-sun#2')
115
+ Rufus::Scheduler::CronLine.new('* * * * sun#1-sun#2')
115
116
  }.should raise_error(ArgumentError)
116
117
  end
117
118
 
@@ -165,7 +166,7 @@ describe Rufus::CronLine do
165
166
  describe '#next_time' do
166
167
 
167
168
  def nt(cronline, now)
168
- Rufus::CronLine.new(cronline).next_time(now)
169
+ Rufus::Scheduler::CronLine.new(cronline).next_time(now)
169
170
  end
170
171
 
171
172
  it 'computes the next occurence correctly' do
@@ -185,6 +186,7 @@ describe Rufus::CronLine do
185
186
  # that slowness is gone)
186
187
 
187
188
  nt('0 0 * * thu', now).should == now + 604800
189
+ nt('00 0 * * thu', now).should == now + 604800
188
190
 
189
191
  nt('0 0 * * *', now).should == now + 24 * 3600
190
192
  nt('0 24 * * *', now).should == now + 24 * 3600
@@ -294,12 +296,18 @@ describe Rufus::CronLine do
294
296
  nt('* * L * *', lo(1972, 2, 1)).should == lo(1972, 2, 29)
295
297
  nt('* * L * *', lo(1970, 4, 1)).should == lo(1970, 4, 30)
296
298
  end
299
+
300
+ it 'returns a time with subseconds chopped off' do
301
+
302
+ nt('* * * * *', Time.now).usec.should == 0
303
+ nt('* * * * *', Time.now).iso8601(10).match(/\.0+[^\d]/).should_not == nil
304
+ end
297
305
  end
298
306
 
299
307
  describe '#previous_time' do
300
308
 
301
309
  def pt(cronline, now)
302
- Rufus::CronLine.new(cronline).previous_time(now)
310
+ Rufus::Scheduler::CronLine.new(cronline).previous_time(now)
303
311
  end
304
312
 
305
313
  it 'returns the previous time the cron should have triggered' do
@@ -386,11 +394,11 @@ describe Rufus::CronLine do
386
394
 
387
395
  it 'returns the appropriate "sun#2"-like string' do
388
396
 
389
- class Rufus::CronLine
397
+ class Rufus::Scheduler::CronLine
390
398
  public :monthdays
391
399
  end
392
400
 
393
- cl = Rufus::CronLine.new('* * * * *')
401
+ cl = Rufus::Scheduler::CronLine.new('* * * * *')
394
402
 
395
403
  cl.monthdays(local(1970, 1, 1)).should == %w[ thu#1 thu#-5 ]
396
404
  cl.monthdays(local(1970, 1, 7)).should == %w[ wed#1 wed#-4 ]
@@ -399,5 +407,18 @@ describe Rufus::CronLine do
399
407
  cl.monthdays(local(2011, 3, 11)).should == %w[ fri#2 fri#-3 ]
400
408
  end
401
409
  end
410
+
411
+ describe '#frequency' do
412
+
413
+ it 'returns the shortest delta between two occurrences' do
414
+
415
+ Rufus::Scheduler::CronLine.new('* * * * *').frequency.should == 60
416
+ Rufus::Scheduler::CronLine.new('* * * * * *').frequency.should == 1
417
+
418
+ Rufus::Scheduler::CronLine.new('5 23 * * *').frequency.should == 24 * 3600
419
+ Rufus::Scheduler::CronLine.new('5 * * * *').frequency.should == 3600
420
+ Rufus::Scheduler::CronLine.new('10,20,30 * * * *').frequency.should == 600
421
+ end
422
+ end
402
423
  end
403
424