rufus-scheduler 2.0.24 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/CHANGELOG.txt +76 -0
  2. data/CREDITS.txt +23 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +1439 -0
  5. data/Rakefile +1 -5
  6. data/TODO.txt +149 -55
  7. data/lib/rufus/{sc → scheduler}/cronline.rb +167 -53
  8. data/lib/rufus/scheduler/job_array.rb +92 -0
  9. data/lib/rufus/scheduler/jobs.rb +633 -0
  10. data/lib/rufus/scheduler/locks.rb +95 -0
  11. data/lib/rufus/scheduler/util.rb +306 -0
  12. data/lib/rufus/scheduler/zones.rb +174 -0
  13. data/lib/rufus/scheduler/zotime.rb +154 -0
  14. data/lib/rufus/scheduler.rb +608 -27
  15. data/rufus-scheduler.gemspec +6 -4
  16. data/spec/basics_spec.rb +54 -0
  17. data/spec/cronline_spec.rb +479 -152
  18. data/spec/error_spec.rb +139 -0
  19. data/spec/job_array_spec.rb +39 -0
  20. data/spec/job_at_spec.rb +58 -0
  21. data/spec/job_cron_spec.rb +128 -0
  22. data/spec/job_every_spec.rb +104 -0
  23. data/spec/job_in_spec.rb +20 -0
  24. data/spec/job_interval_spec.rb +68 -0
  25. data/spec/job_repeat_spec.rb +357 -0
  26. data/spec/job_spec.rb +498 -109
  27. data/spec/lock_custom_spec.rb +47 -0
  28. data/spec/lock_flock_spec.rb +47 -0
  29. data/spec/lock_lockfile_spec.rb +61 -0
  30. data/spec/lock_spec.rb +59 -0
  31. data/spec/parse_spec.rb +263 -0
  32. data/spec/schedule_at_spec.rb +158 -0
  33. data/spec/schedule_cron_spec.rb +66 -0
  34. data/spec/schedule_every_spec.rb +109 -0
  35. data/spec/schedule_in_spec.rb +80 -0
  36. data/spec/schedule_interval_spec.rb +128 -0
  37. data/spec/scheduler_spec.rb +928 -124
  38. data/spec/spec_helper.rb +126 -0
  39. data/spec/threads_spec.rb +96 -0
  40. data/spec/zotime_spec.rb +396 -0
  41. metadata +56 -33
  42. data/README.rdoc +0 -661
  43. data/lib/rufus/otime.rb +0 -3
  44. data/lib/rufus/sc/jobqueues.rb +0 -160
  45. data/lib/rufus/sc/jobs.rb +0 -471
  46. data/lib/rufus/sc/rtime.rb +0 -363
  47. data/lib/rufus/sc/scheduler.rb +0 -636
  48. data/lib/rufus/sc/version.rb +0 -32
  49. data/spec/at_in_spec.rb +0 -47
  50. data/spec/at_spec.rb +0 -125
  51. data/spec/blocking_spec.rb +0 -64
  52. data/spec/cron_spec.rb +0 -134
  53. data/spec/every_spec.rb +0 -304
  54. data/spec/exception_spec.rb +0 -113
  55. data/spec/in_spec.rb +0 -150
  56. data/spec/mutex_spec.rb +0 -159
  57. data/spec/rtime_spec.rb +0 -137
  58. data/spec/schedulable_spec.rb +0 -97
  59. data/spec/spec_base.rb +0 -87
  60. data/spec/stress_schedule_unschedule_spec.rb +0 -159
  61. data/spec/timeout_spec.rb +0 -148
  62. data/test/kjw.rb +0 -113
  63. data/test/t.rb +0 -20
@@ -1,636 +0,0 @@
1
- #--
2
- # Copyright (c) 2006-2013, John Mettraux, jmettraux@gmail.com
3
- #
4
- # Permission is hereby granted, free of charge, to any person obtaining a copy
5
- # of this software and associated documentation files (the "Software"), to deal
6
- # in the Software without restriction, including without limitation the rights
7
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- # copies of the Software, and to permit persons to whom the Software is
9
- # furnished to do so, subject to the following conditions:
10
- #
11
- # The above copyright notice and this permission notice shall be included in
12
- # all copies or substantial portions of the Software.
13
- #
14
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
- # THE SOFTWARE.
21
- #
22
- # Made in Japan.
23
- #++
24
-
25
-
26
- require 'rufus/sc/version'
27
- require 'rufus/sc/rtime'
28
- require 'rufus/sc/cronline'
29
- require 'rufus/sc/jobs'
30
- require 'rufus/sc/jobqueues'
31
-
32
-
33
- module Rufus::Scheduler
34
-
35
- #
36
- # It's OK to pass an object responding to :trigger when scheduling a job
37
- # (instead of passing a block).
38
- #
39
- # This is simply a helper module. The rufus-scheduler will check if scheduled
40
- # object quack (respond to :trigger anyway).
41
- #
42
- module Schedulable
43
- def call(job)
44
- trigger(job.params)
45
- end
46
- def trigger(params)
47
- raise NotImplementedError.new('implementation is missing')
48
- end
49
- end
50
-
51
- #
52
- # For backward compatibility
53
- #
54
- module ::Rufus::Schedulable
55
- extend ::Rufus::Scheduler::Schedulable
56
- end
57
-
58
- # Legacy from the previous version of Rufus-Scheduler.
59
- #
60
- # Consider all methods here as 'deprecated'.
61
- #
62
- module LegacyMethods
63
-
64
- def find_jobs(tag=nil)
65
- tag ? find_by_tag(tag) : all_jobs.values
66
- end
67
- def at_job_count
68
- @jobs.select(:at).size +
69
- @jobs.select(:in).size
70
- end
71
- def every_job_count
72
- @jobs.select(:every).size
73
- end
74
- def cron_job_count
75
- @cron_jobs.size
76
- end
77
- def pending_job_count
78
- @jobs.size
79
- end
80
- def precision
81
- @frequency
82
- end
83
- end
84
-
85
- #
86
- # The core of a rufus-scheduler. See implementations like
87
- # Rufus::Scheduler::PlainScheduler and Rufus::Scheduler::EmScheduler for
88
- # directly usable stuff.
89
- #
90
- class SchedulerCore
91
-
92
- include LegacyMethods
93
-
94
- # classical options hash
95
- #
96
- attr_reader :options
97
-
98
- # Instantiates a Rufus::Scheduler.
99
- #
100
- def initialize(opts={})
101
-
102
- @options = opts
103
-
104
- @jobs = get_queue(:at, opts)
105
- @cron_jobs = get_queue(:cron, opts)
106
-
107
- @frequency = @options[:frequency] || 0.330
108
-
109
- @mutexes = {}
110
- end
111
-
112
- # Instantiates and starts a new Rufus::Scheduler.
113
- #
114
- def self.start_new(opts={})
115
-
116
- s = self.new(opts)
117
- s.start
118
- s
119
- end
120
-
121
- #--
122
- # SCHEDULE METHODS
123
- #++
124
-
125
- # Schedules a job in a given amount of time.
126
- #
127
- # scheduler.in '20m' do
128
- # puts "order ristretto"
129
- # end
130
- #
131
- # will order an espresso (well sort of) in 20 minutes.
132
- #
133
- def in(t, s=nil, opts={}, &block)
134
-
135
- add_job(InJob.new(self, t, combine_opts(s, opts), &block))
136
- end
137
- alias :schedule_in :in
138
-
139
- # Schedules a job at a given point in time.
140
- #
141
- # scheduler.at 'Thu Mar 26 19:30:00 2009' do
142
- # puts 'order pizza'
143
- # end
144
- #
145
- # pizza is for Thursday at 2000 (if the shop brochure is right).
146
- #
147
- def at(t, s=nil, opts={}, &block)
148
-
149
- add_job(AtJob.new(self, t, combine_opts(s, opts), &block))
150
- end
151
- alias :schedule_at :at
152
-
153
- # Schedules a recurring job every t.
154
- #
155
- # scheduler.every '5m1w' do
156
- # puts 'check blood pressure'
157
- # end
158
- #
159
- # checking blood pressure every 5 months and 1 week.
160
- #
161
- def every(t, s=nil, opts={}, &block)
162
-
163
- add_job(EveryJob.new(self, t, combine_opts(s, opts), &block))
164
- end
165
- alias :schedule_every :every
166
-
167
- # Schedules a job given a cron string.
168
- #
169
- # scheduler.cron '0 22 * * 1-5' do
170
- # # every day of the week at 00:22
171
- # puts 'activate security system'
172
- # end
173
- #
174
- def cron(cronstring, s=nil, opts={}, &block)
175
-
176
- add_cron_job(CronJob.new(self, cronstring, combine_opts(s, opts), &block))
177
- end
178
- alias :schedule :cron
179
-
180
- # Unschedules a job (cron or at/every/in job).
181
- #
182
- # Returns the job that got unscheduled.
183
- #
184
- def unschedule(job_or_id)
185
-
186
- job_id = job_or_id.respond_to?(:job_id) ? job_or_id.job_id : job_or_id
187
-
188
- @jobs.unschedule(job_id) || @cron_jobs.unschedule(job_id)
189
- end
190
-
191
- # Given a tag, unschedules all the jobs that bear that tag.
192
- #
193
- def unschedule_by_tag(tag)
194
-
195
- jobs = find_by_tag(tag)
196
- jobs.each { |job| unschedule(job.job_id) }
197
-
198
- jobs
199
- end
200
-
201
- # Pauses a given job. If the argument is an id (String) and the
202
- # corresponding job cannot be found, an ArgumentError will get raised.
203
- #
204
- def pause(job_or_id)
205
-
206
- find(job_or_id).pause
207
- end
208
-
209
- # Resumes a given job. If the argument is an id (String) and the
210
- # corresponding job cannot be found, an ArgumentError will get raised.
211
- #
212
- def resume(job_or_id)
213
-
214
- find(job_or_id).resume
215
- end
216
-
217
- #--
218
- # MISC
219
- #++
220
-
221
- # Determines if there is #log_exception, #handle_exception or #on_exception
222
- # method. If yes, hands the exception to it, else defaults to outputting
223
- # details to $stderr.
224
- #
225
- def do_handle_exception(job, exception)
226
-
227
- begin
228
-
229
- [ :log_exception, :handle_exception, :on_exception ].each do |m|
230
-
231
- next unless self.respond_to?(m)
232
-
233
- if method(m).arity == 1
234
- self.send(m, exception)
235
- else
236
- self.send(m, job, exception)
237
- end
238
-
239
- return
240
- # exception was handled successfully
241
- end
242
-
243
- rescue Exception => e
244
-
245
- $stderr.puts '*' * 80
246
- $stderr.puts 'the exception handling method itself had an issue:'
247
- $stderr.puts e
248
- $stderr.puts *e.backtrace
249
- $stderr.puts '*' * 80
250
- end
251
-
252
- $stderr.puts '=' * 80
253
- $stderr.puts 'scheduler caught exception:'
254
- $stderr.puts exception
255
- $stderr.puts *exception.backtrace
256
- $stderr.puts '=' * 80
257
- end
258
-
259
- #--
260
- # JOB LOOKUP
261
- #++
262
-
263
- # Returns a map job_id => job for at/in/every jobs
264
- #
265
- def jobs
266
-
267
- @jobs.to_h
268
- end
269
-
270
- # Returns a map job_id => job for cron jobs
271
- #
272
- def cron_jobs
273
-
274
- @cron_jobs.to_h
275
- end
276
-
277
- # Returns a map job_id => job of all the jobs currently in the scheduler
278
- #
279
- def all_jobs
280
-
281
- jobs.merge(cron_jobs)
282
- end
283
-
284
- # Returns a list of jobs with the given tag
285
- #
286
- def find_by_tag(tag)
287
-
288
- all_jobs.values.select { |j| j.tags.include?(tag) }
289
- end
290
-
291
- # Mostly used to find a job given its id. If the argument is a job, will
292
- # simply return it.
293
- #
294
- # If the argument is an id, and no job with that id is found, it will
295
- # raise an ArgumentError.
296
- #
297
- def find(job_or_id)
298
-
299
- return job_or_id if job_or_id.respond_to?(:job_id)
300
-
301
- job = all_jobs[job_or_id]
302
-
303
- raise ArgumentError.new(
304
- "couldn't find job #{job_or_id.inspect}"
305
- ) unless job
306
-
307
- job
308
- end
309
-
310
- # Returns the current list of trigger threads (threads) dedicated to
311
- # the execution of jobs.
312
- #
313
- def trigger_threads
314
-
315
- Thread.list.select { |t|
316
- t["rufus_scheduler__trigger_thread__#{self.object_id}"]
317
- }
318
- end
319
-
320
- # Returns the list of the currently running jobs (jobs that just got
321
- # triggered and are executing).
322
- #
323
- def running_jobs
324
-
325
- Thread.list.collect { |t|
326
- t["rufus_scheduler__trigger_thread__#{self.object_id}"]
327
- }.compact
328
- end
329
-
330
- # This is a blocking call, it will return when all the jobs have been
331
- # unscheduled, waiting for any running one to finish before unscheduling
332
- # it.
333
- #
334
- def terminate_all_jobs
335
-
336
- all_jobs.each do |job_id, job|
337
- job.unschedule
338
- end
339
-
340
- while running_jobs.size > 0
341
- sleep 0.01
342
- end
343
- end
344
-
345
- protected
346
-
347
- # Returns a job queue instance.
348
- #
349
- # (made it into a method for easy override)
350
- #
351
- def get_queue(type, opts)
352
-
353
- q = if type == :cron
354
- opts[:cron_job_queue] || Rufus::Scheduler::CronJobQueue.new
355
- else
356
- opts[:job_queue] || Rufus::Scheduler::JobQueue.new
357
- end
358
-
359
- q.scheduler = self if q.respond_to?(:scheduler=)
360
-
361
- q
362
- end
363
-
364
- def combine_opts(schedulable, opts)
365
-
366
- if schedulable.respond_to?(:trigger) || schedulable.respond_to?(:call)
367
-
368
- opts[:schedulable] = schedulable
369
-
370
- elsif schedulable != nil
371
-
372
- opts = schedulable.merge(opts)
373
- end
374
-
375
- opts
376
- end
377
-
378
- # The method that does the "wake up and trigger any job that should get
379
- # triggered.
380
- #
381
- def step
382
-
383
- @cron_jobs.trigger_matching_jobs
384
- @jobs.trigger_matching_jobs
385
- end
386
-
387
- def add_job(job)
388
-
389
- complain_if_blocking_and_timeout(job)
390
-
391
- return nil if job.params[:discard_past] && Time.now.to_f >= job.at
392
-
393
- @jobs << job
394
-
395
- job
396
- end
397
-
398
- def add_cron_job(job)
399
-
400
- complain_if_blocking_and_timeout(job)
401
-
402
- @cron_jobs << job
403
-
404
- job
405
- end
406
-
407
- # Raises an error if the job has the params :blocking and :timeout set
408
- #
409
- def complain_if_blocking_and_timeout(job)
410
-
411
- raise(
412
- ArgumentError.new('cannot set a :timeout on a :blocking job')
413
- ) if job.params[:blocking] and job.params[:timeout]
414
- end
415
-
416
- # The default, plain, implementation. If 'blocking' is true, will simply
417
- # call the block and return when the block is done.
418
- # Else, it will call the block in a dedicated thread.
419
- #
420
- # TODO : clarify, the blocking here blocks the whole scheduler, while
421
- # EmScheduler blocking triggers for the next tick. Not the same thing ...
422
- #
423
- def trigger_job(params, &block)
424
-
425
- if params[:blocking]
426
- block.call
427
- elsif m = params[:mutex]
428
- Thread.new { synchronize_with_mutex(m, &block) }
429
- else
430
- Thread.new { block.call }
431
- end
432
- end
433
-
434
- def synchronize_with_mutex(mutex, &block)
435
- case mutex
436
- when Mutex
437
- mutex.synchronize { block.call }
438
- when Array
439
- mutex.reduce(block) do |memo, m|
440
- m = (@mutexes[m.to_s] ||= Mutex.new) unless m.is_a?(Mutex)
441
- lambda { m.synchronize { memo.call } }
442
- end.call
443
- else
444
- (@mutexes[mutex.to_s] ||= Mutex.new).synchronize { block.call }
445
- end
446
- end
447
- end
448
-
449
- #--
450
- # SCHEDULER 'IMPLEMENTATIONS'
451
- #++
452
-
453
- #
454
- # A classical implementation, uses a sleep/step loop in a thread (like the
455
- # original rufus-scheduler).
456
- #
457
- class PlainScheduler < SchedulerCore
458
-
459
- def start
460
-
461
- @thread = Thread.new do
462
- loop do
463
- sleep(@frequency)
464
- step
465
- end
466
- end
467
-
468
- @thread[:name] =
469
- @options[:thread_name] ||
470
- "#{self.class} - #{Rufus::Scheduler::VERSION}"
471
- end
472
-
473
- # Stops this scheduler.
474
- #
475
- # == :terminate => true
476
- #
477
- # If the option :terminate is set to true,
478
- # the method will return once all the jobs have been unscheduled and
479
- # are done with their current run if any.
480
- #
481
- # (note that if a job is
482
- # currently running, this method will wait for it to terminate, it
483
- # will not interrupt the job run).
484
- #
485
- def stop(opts={})
486
-
487
- @thread.exit
488
-
489
- terminate_all_jobs if opts[:terminate]
490
- end
491
-
492
- def join
493
-
494
- @thread.join
495
- end
496
- end
497
-
498
- # TODO : investigate idea
499
- #
500
- #class BlockingScheduler < PlainScheduler
501
- # # use a Queue and a worker thread for the 'blocking' jobs
502
- #end
503
-
504
- #
505
- # A rufus-scheduler that steps only when the ruby process receives the
506
- # 10 / USR1 signal.
507
- #
508
- class SignalScheduler < SchedulerCore
509
-
510
- def initialize(opts={})
511
-
512
- super(opts)
513
-
514
- trap(@options[:signal] || 10) do
515
- step
516
- end
517
- end
518
-
519
-
520
- # Stops this scheduler.
521
- #
522
- # == :terminate => true
523
- #
524
- # If the option :terminate is set to true,
525
- # the method will return once all the jobs have been unscheduled and
526
- # are done with their current run if any.
527
- #
528
- # (note that if a job is
529
- # currently running, this method will wait for it to terminate, it
530
- # will not interrupt the job run).
531
- #
532
- def stop(opts={})
533
-
534
- trap(@options[:signal] || 10)
535
-
536
- terminate_all_jobs if opts[:terminate]
537
- end
538
- end
539
-
540
- #
541
- # A rufus-scheduler that uses an EventMachine periodic timer instead of a
542
- # loop.
543
- #
544
- class EmScheduler < SchedulerCore
545
-
546
- def initialize(opts={})
547
-
548
- raise LoadError.new(
549
- 'EventMachine missing, "require \'eventmachine\'" might help'
550
- ) unless defined?(EM)
551
-
552
- super(opts)
553
- end
554
-
555
- def start
556
-
557
- @em_thread = nil
558
-
559
- unless EM.reactor_running?
560
- @em_thread = Thread.new { EM.run }
561
- while (not EM.reactor_running?)
562
- Thread.pass
563
- end
564
- end
565
-
566
- #unless EM.reactor_running?
567
- # t = Thread.current
568
- # @em_thread = Thread.new { EM.run { t.wakeup } }
569
- # Thread.stop # EM will wake us up when it's ready
570
- #end
571
-
572
- @timer = EM::PeriodicTimer.new(@frequency) { step }
573
- end
574
-
575
- # Stops the scheduler.
576
- #
577
- # == :stop_em => true
578
- #
579
- # If the :stop_em option is passed and set to true, it will stop the
580
- # EventMachine (but only if it started the EM by itself !).
581
- #
582
- # == :terminate => true
583
- #
584
- # If the option :terminate is set to true,
585
- # the method will return once all the jobs have been unscheduled and
586
- # are done with their current run if any.
587
- #
588
- # (note that if a job is
589
- # currently running, this method will wait for it to terminate, it
590
- # will not interrupt the job run).
591
- #
592
- def stop(opts={})
593
-
594
- @timer.cancel
595
-
596
- terminate_all_jobs if opts[:terminate]
597
-
598
- EM.stop if opts[:stop_em] and @em_thread
599
- end
600
-
601
- # Joins this scheduler. Will actually join it only if it started the
602
- # underlying EventMachine.
603
- #
604
- def join
605
-
606
- @em_thread.join if @em_thread
607
- end
608
-
609
- protected
610
-
611
- # If 'blocking' is set to true, the block will get called at the
612
- # 'next_tick'. Else the block will get called via 'defer' (own thread).
613
- #
614
- def trigger_job(params, &block)
615
-
616
- # :next_tick monopolizes the EM
617
- # :defer executes its block in another thread
618
- # (if I read the doc carefully...)
619
-
620
- if params[:blocking]
621
- EM.next_tick { block.call }
622
- elsif m = params[:mutex]
623
- EM.defer { synchronize_with_mutex(m, &block) }
624
- else
625
- EM.defer { block.call }
626
- end
627
- end
628
- end
629
-
630
- #
631
- # This error is thrown when the :timeout attribute triggers
632
- #
633
- class TimeOutError < RuntimeError
634
- end
635
- end
636
-
@@ -1,32 +0,0 @@
1
- #--
2
- # Copyright (c) 2006-2013, John Mettraux, jmettraux@gmail.com
3
- #
4
- # Permission is hereby granted, free of charge, to any person obtaining a copy
5
- # of this software and associated documentation files (the "Software"), to deal
6
- # in the Software without restriction, including without limitation the rights
7
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- # copies of the Software, and to permit persons to whom the Software is
9
- # furnished to do so, subject to the following conditions:
10
- #
11
- # The above copyright notice and this permission notice shall be included in
12
- # all copies or substantial portions of the Software.
13
- #
14
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
- # THE SOFTWARE.
21
- #
22
- # Made in Japan.
23
- #++
24
-
25
-
26
- module Rufus
27
- module Scheduler
28
-
29
- VERSION = '2.0.24'
30
- end
31
- end
32
-
data/spec/at_in_spec.rb DELETED
@@ -1,47 +0,0 @@
1
-
2
- #
3
- # Specifying rufus-scheduler
4
- #
5
- # Sun Mar 22 16:47:28 JST 2009
6
- #
7
-
8
- require 'spec_base'
9
-
10
-
11
- describe SCHEDULER_CLASS do
12
-
13
- before(:each) do
14
- @s = start_scheduler
15
- end
16
- after(:each) do
17
- stop_scheduler(@s)
18
- end
19
-
20
-
21
- it 'overrides jobs with the same id' do
22
-
23
- hits = []
24
-
25
- job0 = @s.in '1s', :job_id => 'nada' do
26
- hits << 0
27
- end
28
-
29
- wait_next_tick
30
-
31
- job1 = @s.in '1s', :job_id => 'nada' do
32
- hits << 1
33
- end
34
-
35
- wait_next_tick
36
- @s.jobs.size.should == 1
37
-
38
- hits.should == []
39
-
40
- sleep 1.5
41
-
42
- hits.should == [ 1 ]
43
-
44
- @s.jobs.size.should == 0
45
- end
46
- end
47
-