chore-core 1.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +15 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.md +260 -0
  4. data/Rakefile +32 -0
  5. data/bin/chore +34 -0
  6. data/chore-core.gemspec +46 -0
  7. data/lib/chore/cli.rb +232 -0
  8. data/lib/chore/configuration.rb +13 -0
  9. data/lib/chore/consumer.rb +52 -0
  10. data/lib/chore/duplicate_detector.rb +56 -0
  11. data/lib/chore/fetcher.rb +31 -0
  12. data/lib/chore/hooks.rb +25 -0
  13. data/lib/chore/job.rb +103 -0
  14. data/lib/chore/json_encoder.rb +18 -0
  15. data/lib/chore/manager.rb +47 -0
  16. data/lib/chore/publisher.rb +29 -0
  17. data/lib/chore/queues/filesystem/consumer.rb +128 -0
  18. data/lib/chore/queues/filesystem/filesystem_queue.rb +49 -0
  19. data/lib/chore/queues/filesystem/publisher.rb +45 -0
  20. data/lib/chore/queues/sqs/consumer.rb +121 -0
  21. data/lib/chore/queues/sqs/publisher.rb +55 -0
  22. data/lib/chore/queues/sqs.rb +38 -0
  23. data/lib/chore/railtie.rb +18 -0
  24. data/lib/chore/signal.rb +175 -0
  25. data/lib/chore/strategies/consumer/batcher.rb +76 -0
  26. data/lib/chore/strategies/consumer/single_consumer_strategy.rb +34 -0
  27. data/lib/chore/strategies/consumer/threaded_consumer_strategy.rb +81 -0
  28. data/lib/chore/strategies/worker/forked_worker_strategy.rb +221 -0
  29. data/lib/chore/strategies/worker/single_worker_strategy.rb +39 -0
  30. data/lib/chore/tasks/queues.task +11 -0
  31. data/lib/chore/unit_of_work.rb +17 -0
  32. data/lib/chore/util.rb +18 -0
  33. data/lib/chore/version.rb +9 -0
  34. data/lib/chore/worker.rb +117 -0
  35. data/lib/chore-core.rb +1 -0
  36. data/lib/chore.rb +218 -0
  37. data/spec/chore/cli_spec.rb +182 -0
  38. data/spec/chore/consumer_spec.rb +36 -0
  39. data/spec/chore/duplicate_detector_spec.rb +62 -0
  40. data/spec/chore/fetcher_spec.rb +38 -0
  41. data/spec/chore/hooks_spec.rb +44 -0
  42. data/spec/chore/job_spec.rb +80 -0
  43. data/spec/chore/json_encoder_spec.rb +11 -0
  44. data/spec/chore/manager_spec.rb +39 -0
  45. data/spec/chore/queues/filesystem/filesystem_consumer_spec.rb +71 -0
  46. data/spec/chore/queues/sqs/consumer_spec.rb +136 -0
  47. data/spec/chore/queues/sqs/publisher_spec.rb +74 -0
  48. data/spec/chore/queues/sqs_spec.rb +37 -0
  49. data/spec/chore/signal_spec.rb +244 -0
  50. data/spec/chore/strategies/consumer/batcher_spec.rb +93 -0
  51. data/spec/chore/strategies/consumer/single_consumer_strategy_spec.rb +23 -0
  52. data/spec/chore/strategies/consumer/threaded_consumer_strategy_spec.rb +105 -0
  53. data/spec/chore/strategies/worker/forked_worker_strategy_spec.rb +281 -0
  54. data/spec/chore/strategies/worker/single_worker_strategy_spec.rb +36 -0
  55. data/spec/chore/worker_spec.rb +134 -0
  56. data/spec/chore_spec.rb +108 -0
  57. data/spec/spec_helper.rb +58 -0
  58. data/spec/test_job.rb +7 -0
  59. metadata +194 -0
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ class TestConsumer < Chore::Consumer
4
+ def initialize(queue_name, opts={})
5
+ end
6
+
7
+ def consume
8
+ # just something that looks like an SQS message
9
+ msg = OpenStruct.new( :id => 1, :body => "test" )
10
+ yield msg if block_given?
11
+ end
12
+ end
13
+
14
+ class NoQueueConsumer < Chore::Consumer
15
+ def initialize(queue_name, opts={})
16
+ raise Chore::TerribleMistake
17
+ end
18
+
19
+ def consume
20
+ end
21
+ end
22
+
23
+ describe Chore::Strategy::ThreadedConsumerStrategy do
24
+ let(:fetcher) { double("fetcher") }
25
+ let(:manager) { double("manager") }
26
+ let(:consumer) { TestConsumer }
27
+ let(:strategy) { Chore::Strategy::ThreadedConsumerStrategy.new(fetcher) }
28
+
29
+ before(:each) do
30
+ fetcher.stub(:consumers) { [consumer] }
31
+ fetcher.stub(:manager) { manager }
32
+ Chore.configure do |c|
33
+ c.queues = ['test']
34
+ c.consumer = consumer
35
+ c.batch_size = batch_size
36
+ end
37
+ end
38
+
39
+ describe "unfilled batch" do
40
+ let(:batch_size) { 2 }
41
+
42
+ it "should queue but not assign the message" do
43
+ consumer.any_instance.should_receive(:consume).and_yield(1, 'test-queue', 60, "test", 0)
44
+ strategy.fetch
45
+ strategy.batcher.batch.size.should == 1
46
+
47
+ work = strategy.batcher.batch[0]
48
+ work.id.should == 1
49
+ work.queue_name.should == 'test-queue'
50
+ work.queue_timeout.should == 60
51
+ work.message.should == "test"
52
+ work.previous_attempts.should == 0
53
+ work.current_attempt.should == 1
54
+ end
55
+ end
56
+
57
+ describe "full batch" do
58
+ let(:batch_size) { 1 }
59
+
60
+ it "should assign the batch" do
61
+ manager.should_receive(:assign)
62
+ consumer.any_instance.should_receive(:consume).and_yield(1, 'test-queue', 60, "test", 0)
63
+ strategy.fetch
64
+ strategy.batcher.batch.size.should == 0
65
+ end
66
+ end
67
+
68
+ describe "2 threads per queue" do
69
+ let(:batch_size) { 2 }
70
+ let(:thread) { double('thread') }
71
+
72
+ before do
73
+ Chore.config.threads_per_queue = 2
74
+ thread.stub(:join)
75
+ end
76
+
77
+ it "should spawn two threads" do
78
+ # two for threads per queue and one for batcher#schedule
79
+ Thread.should_receive(:new).exactly(3).times { thread }
80
+ strategy.fetch
81
+ end
82
+ end
83
+
84
+ describe "non-existent queue" do
85
+ let(:bad_consumer) { NoQueueConsumer }
86
+ let(:fetcher) { double("fetcher") }
87
+ let(:strategy) { Chore::Strategy::ThreadedConsumerStrategy.new(fetcher) }
88
+ let(:batch_size) { 2 }
89
+
90
+ before do
91
+ fetcher.stub(:consumers) { [bad_consumer] }
92
+ Chore.configure do |c|
93
+ c.queues = ['test']
94
+ c.consumer = bad_consumer
95
+ c.batch_size = batch_size
96
+ c.threads_per_queue = 1
97
+ end
98
+ end
99
+
100
+ it "should shut down when a queue doesn't exist" do
101
+ manager.should_receive(:shutdown!)
102
+ strategy.fetch
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,281 @@
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
+
4
+ describe Chore::Strategy::ForkedWorkerStrategy do
5
+ let(:manager) { double('manager') }
6
+ let(:forker) do
7
+ strategy = Chore::Strategy::ForkedWorkerStrategy.new(manager)
8
+ strategy.stub(:exit!)
9
+ strategy
10
+ end
11
+ let(:job_timeout) { 60 }
12
+ let(:job) { Chore::UnitOfWork.new(SecureRandom.uuid, 'test', job_timeout, Chore::JsonEncoder.encode(TestJob.job_hash([1,2,"3"])), 0) }
13
+ let!(:worker) { Chore::Worker.new(job) }
14
+ let(:pid) { Random.rand(2048) }
15
+
16
+ after(:each) do
17
+ Process.stub(:kill => nil, :wait => pid)
18
+ forker.stop!
19
+ end
20
+
21
+ context "signal handling" do
22
+ it 'should trap signals from terminating children and reap them' do
23
+ Chore::Signal.should_receive(:trap).with('CHLD').and_yield
24
+ Chore::Strategy::ForkedWorkerStrategy.any_instance.should_receive(:reap_terminated_workers!)
25
+ forker
26
+ end
27
+ end
28
+
29
+ context '#assign' do
30
+ before(:each) do
31
+ forker.stub(:fork).and_yield.and_return(pid, pid + 1)
32
+ forker.stub(:after_fork)
33
+ end
34
+ after(:each) do
35
+ Chore.clear_hooks!
36
+ end
37
+
38
+ it 'should pop off the worker queue when assignd a job' do
39
+ Queue.any_instance.should_receive(:pop)
40
+ forker.assign(job)
41
+ end
42
+
43
+ it 'should assign a job to a new worker' do
44
+ Chore::Worker.should_receive(:new).with(job).and_return(worker)
45
+ worker.should_receive(:start)
46
+ forker.assign(job)
47
+ end
48
+
49
+ it 'should add an assigned worker to the worker list' do
50
+ forker.workers.should_receive(:[]=).with(pid,kind_of(Chore::Worker))
51
+ forker.assign(job)
52
+ end
53
+
54
+ it 'should fork a child for each new worker' do
55
+ forker.should_receive(:fork).and_yield.and_return(pid)
56
+ forker.assign(job)
57
+ end
58
+
59
+ it 'should remove the worker from the list when it has completed' do
60
+ forker.assign(job)
61
+
62
+ Process.should_receive(:wait).with(pid, Process::WNOHANG).and_return(pid)
63
+ forker.send(:reap_terminated_workers!)
64
+
65
+ forker.workers.should_not include(pid)
66
+ end
67
+
68
+ it 'should not remove the worker from the list if it has not yet completed' do
69
+ forker.assign(job)
70
+
71
+ Process.stub(:wait).and_return(nil)
72
+ forker.send(:reap_terminated_workers!)
73
+
74
+ forker.workers.should include(pid)
75
+ end
76
+
77
+ it 'should add the worker back to the queue when it has completed' do
78
+ 2.times { forker.assign(job) }
79
+
80
+ Queue.any_instance.should_receive(:<<).twice.with(:worker)
81
+
82
+ Process.stub(:wait).and_return(pid, pid + 1)
83
+ forker.send(:reap_terminated_workers!)
84
+ end
85
+
86
+ it 'should only release a worker once if reaped twice' do
87
+ forker.assign(job)
88
+ reaped = false
89
+
90
+ forker.should_receive(:release_worker).once
91
+
92
+ Process.should_receive(:wait).twice.with(pid, anything).and_return do
93
+ if !reaped
94
+ reaped = true
95
+ forker.send(:reap_terminated_workers!)
96
+ end
97
+
98
+ pid
99
+ end
100
+ forker.send(:reap_terminated_workers!)
101
+ end
102
+
103
+ it 'should continue to allow reaping after an exception occurs' do
104
+ 2.times { forker.assign(job) }
105
+
106
+ Process.should_receive(:wait).and_raise(Errno::ECHILD)
107
+ Process.should_receive(:wait).and_return(pid + 1)
108
+ forker.send(:reap_terminated_workers!)
109
+
110
+ forker.workers.should be_empty
111
+ end
112
+
113
+ [:before_fork, :after_fork, :within_fork, :before_fork_shutdown].each do |hook|
114
+ it "should run #{hook} hooks" do
115
+ hook_called = false
116
+ Chore.add_hook(hook) { hook_called = true }
117
+ forker.assign(job)
118
+ hook_called.should be_true
119
+ end
120
+ end
121
+
122
+ it 'should run around_fork hooks' do
123
+ hook_called = false
124
+ Chore.add_hook(:around_fork) {|&blk| hook_called = true; blk.call }
125
+ forker.assign(job)
126
+ hook_called.should be_true
127
+ end
128
+
129
+ it 'should run before_fork_shutdown hooks even if job errors' do
130
+ Chore::Worker.stub(:new).and_return(worker)
131
+ worker.stub(:start).and_raise(ArgumentError)
132
+
133
+ hook_called = false
134
+ Chore.add_hook(:before_fork_shutdown) { hook_called = true }
135
+
136
+ begin
137
+ forker.assign(job)
138
+ rescue ArgumentError => ex
139
+ end
140
+
141
+ hook_called.should be_true
142
+ end
143
+
144
+ it 'should exit the process without running at_exit handlers' do
145
+ forker.should_receive(:exit!).with(true)
146
+ forker.assign(job)
147
+ end
148
+
149
+ context 'long-lived work' do
150
+ let(:job_timeout) { 0.1 }
151
+
152
+ before(:each) do
153
+ Process.stub(:kill)
154
+ Chore::Worker.stub(:new).and_return(worker)
155
+ end
156
+
157
+ it 'should kill the process if it expires' do
158
+ Process.should_receive(:kill).with('KILL', pid)
159
+ forker.assign(job)
160
+ sleep 2
161
+ end
162
+
163
+ it 'should run the on_failure callback hook' do
164
+ forker.assign(job)
165
+ Chore.should_receive(:run_hooks_for).with(:on_failure, anything, instance_of(Chore::TimeoutError))
166
+ sleep 2
167
+ end
168
+ end
169
+
170
+ context 'short-lived work' do
171
+ let(:job_timeout) { 0.1 }
172
+
173
+ before(:each) do
174
+ Chore::Worker.stub(:new).and_return(worker)
175
+ end
176
+
177
+ it 'should not kill the process if does not expire' do
178
+ Process.should_not_receive(:kill)
179
+
180
+ forker.assign(job)
181
+ Process.stub(:wait).and_return(pid)
182
+ forker.send(:reap_terminated_workers!)
183
+ sleep 2
184
+ end
185
+ end
186
+ end
187
+
188
+ context '#before_fork' do
189
+ before(:each) do
190
+ Chore::Worker.stub(:new).and_return(worker)
191
+ end
192
+ after(:each) do
193
+ Chore.clear_hooks!
194
+ end
195
+
196
+ it 'should release the worker if an exception occurs' do
197
+ Chore.add_hook(:before_fork) { raise ArgumentError }
198
+ forker.should_receive(:release_worker)
199
+ forker.assign(job)
200
+ end
201
+ end
202
+
203
+ context '#around_fork' do
204
+ before(:each) do
205
+ Chore::Worker.stub(:new).and_return(worker)
206
+ end
207
+ after(:each) do
208
+ Chore.clear_hooks!
209
+ end
210
+
211
+ it 'should release the worker if an exception occurs' do
212
+ Chore.add_hook(:around_fork) {|worker, &block| raise ArgumentError}
213
+ forker.should_receive(:release_worker)
214
+ forker.assign(job)
215
+ end
216
+ end
217
+
218
+ context '#after_fork' do
219
+ let(:worker) { double('worker') }
220
+
221
+ it 'should clear signals' do
222
+ forker.should_receive(:clear_child_signals)
223
+ forker.send(:after_fork,worker)
224
+ end
225
+
226
+ it 'should trap signals' do
227
+ forker.should_receive(:trap_child_signals)
228
+ forker.send(:after_fork,worker)
229
+ end
230
+
231
+ it 'should set the procline' do
232
+ forker.should_receive(:procline)
233
+ forker.send(:after_fork,worker)
234
+ end
235
+ end
236
+
237
+ context '#stop!' do
238
+ before(:each) do
239
+ Process.stub(:kill)
240
+
241
+ forker.stub(:fork).and_yield.and_return(pid)
242
+ forker.stub(:after_fork)
243
+ forker.assign(job)
244
+ end
245
+
246
+ it 'should send a quit signal to each child' do
247
+ Process.should_receive(:kill).once.with('QUIT', pid)
248
+ Process.stub(:wait).and_return(pid, nil)
249
+ forker.stop!
250
+ end
251
+
252
+ it 'should reap each worker' do
253
+ Process.should_receive(:wait).and_return(pid)
254
+ forker.stop!
255
+ forker.workers.should be_empty
256
+ end
257
+
258
+ it 'should resend quit signal to children if workers are not reaped' do
259
+ Process.should_receive(:kill).twice.with('QUIT', pid)
260
+ Process.stub(:wait).and_return(nil, pid, nil)
261
+ forker.stop!
262
+ end
263
+
264
+ it 'should send kill signal to children if timeout is exceeded' do
265
+ Chore.config.stub(:shutdown_timeout).and_return(0.05)
266
+ Process.should_receive(:kill).once.with('QUIT', pid)
267
+ Process.stub(:wait).and_return(nil)
268
+ Process.should_receive(:kill).once.with('KILL', pid)
269
+ forker.stop!
270
+ end
271
+
272
+ it 'should not allow more work to be assigned' do
273
+ Process.stub(:wait).and_return(pid, nil)
274
+ forker.stop!
275
+
276
+ Chore::Worker.should_not_receive(:new)
277
+ forker.assign(job)
278
+ end
279
+ end
280
+ end
281
+
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chore::Strategy::SingleWorkerStrategy do
4
+ let(:manager) { mock('Manager') }
5
+ subject { described_class.new(manager) }
6
+
7
+ describe '#stop!' do
8
+ before(:each) do
9
+ subject.stub(:worker).and_return worker
10
+ end
11
+
12
+ context 'given there is no current worker' do
13
+ let(:worker) { nil }
14
+
15
+ it 'does nothing' do
16
+ allow_message_expectations_on_nil
17
+
18
+ worker.should_not_receive(:stop!)
19
+ subject.stop!
20
+ end
21
+ end
22
+
23
+ context 'given there is a current worker' do
24
+ let(:worker) { mock('Worker') }
25
+ before(:each) do
26
+ subject.stub(:worker).and_return worker
27
+ end
28
+
29
+ it 'stops the worker' do
30
+ worker.should_receive(:stop!)
31
+ subject.stop!
32
+ end
33
+ end
34
+ end
35
+
36
+ end
@@ -0,0 +1,134 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Chore::Worker do
4
+
5
+ class SimpleJob
6
+ include Chore::Job
7
+ queue_options :name => 'test', :publisher => FakePublisher, :max_attempts => 100
8
+
9
+ def perform(*args)
10
+ return args
11
+ end
12
+ end
13
+
14
+ let(:consumer) { double('consumer', :complete => nil) }
15
+ let(:job_args) { [1,2,'3'] }
16
+ let(:job) { SimpleJob.job_hash(job_args) }
17
+
18
+ it 'should use a default encoder' do
19
+ worker = Chore::Worker.new
20
+ worker.options[:encoder].should == Chore::JsonEncoder
21
+ end
22
+
23
+ it 'should process a single job' do
24
+ work = Chore::UnitOfWork.new('1', 'test', 60, Chore::JsonEncoder.encode(job), 0, consumer)
25
+ SimpleJob.should_receive(:perform).with(*job_args)
26
+ consumer.should_receive(:complete).with('1')
27
+ w = Chore::Worker.new(work)
28
+ w.start
29
+ end
30
+
31
+ it 'should process multiple jobs' do
32
+ work = []
33
+ 10.times do |i|
34
+ work << Chore::UnitOfWork.new(i, 'test', 60, Chore::JsonEncoder.encode(job), 0, consumer)
35
+ end
36
+ SimpleJob.should_receive(:perform).exactly(10).times
37
+ consumer.should_receive(:complete).exactly(10).times
38
+ Chore::Worker.start(work)
39
+ end
40
+
41
+ describe 'expired?' do
42
+ let(:now) { Time.now }
43
+ let(:queue_timeouts) { [10, 20, 30] }
44
+ let(:work) do
45
+ queue_timeouts.map do |queue_timeout|
46
+ Chore::UnitOfWork.new('1', 'test', queue_timeout, Chore::JsonEncoder.encode(job), 0, consumer)
47
+ end
48
+ end
49
+ let(:worker) do
50
+ Timecop.freeze(now) do
51
+ Chore::Worker.new(work)
52
+ end
53
+ end
54
+
55
+ it 'should not be expired when before total timeout' do
56
+ worker.should_not be_expired
57
+ end
58
+
59
+ it 'should not be expired when at total timeout' do
60
+ Timecop.freeze(now + 60) do
61
+ worker.should_not be_expired
62
+ end
63
+ end
64
+
65
+ it 'should be expired when past total timeout' do
66
+ Timecop.freeze(now + 61) do
67
+ worker.should be_expired
68
+ end
69
+ end
70
+ end
71
+
72
+ describe 'with errors' do
73
+ context 'on parse' do
74
+ let(:job) { "Not-A-Valid-Json-String" }
75
+
76
+ it 'should fail cleanly' do
77
+ work = Chore::UnitOfWork.new(2,'test',60,job,0,consumer)
78
+ consumer.should_not_receive(:complete)
79
+ Chore.should_receive(:run_hooks_for).with(:on_failure, job, anything())
80
+ Chore::Worker.start(work)
81
+ end
82
+
83
+ context 'more than the maximum allowed times' do
84
+ before(:each) do
85
+ Chore.config.stub(:max_attempts).and_return(10)
86
+ end
87
+
88
+ it 'should permanently fail' do
89
+ work = Chore::UnitOfWork.new(2,'test',60,job,9,consumer)
90
+ Chore.should_receive(:run_hooks_for).with(:on_permanent_failure, 'test', job, anything())
91
+ Chore::Worker.start(work)
92
+ end
93
+
94
+ it 'should mark the item as completed' do
95
+ work = Chore::UnitOfWork.new(2,'test',60,job,9,consumer)
96
+ consumer.should_receive(:complete).with(2)
97
+ Chore::Worker.start(work)
98
+ end
99
+ end
100
+ end
101
+
102
+ context 'on perform' do
103
+ let(:encoded_job) { Chore::JsonEncoder.encode(job) }
104
+ let(:parsed_job) { JSON.parse(encoded_job) }
105
+
106
+ before(:each) do
107
+ SimpleJob.stub(:perform).and_raise(ArgumentError)
108
+ SimpleJob.stub(:run_hooks_for).and_return(true)
109
+ end
110
+
111
+ it 'should fail cleanly' do
112
+ work = Chore::UnitOfWork.new(2,'test',60,encoded_job,0,consumer)
113
+ consumer.should_not_receive(:complete)
114
+ SimpleJob.should_receive(:run_hooks_for).with(:on_failure, parsed_job, anything())
115
+
116
+ Chore::Worker.start(work)
117
+ end
118
+
119
+ context 'more than the maximum allowed times' do
120
+ it 'should permanently fail' do
121
+ work = Chore::UnitOfWork.new(2,'test',60,encoded_job,999,consumer)
122
+ SimpleJob.should_receive(:run_hooks_for).with(:on_permanent_failure, 'test', parsed_job, anything())
123
+ Chore::Worker.start(work)
124
+ end
125
+
126
+ it 'should mark the item as completed' do
127
+ work = Chore::UnitOfWork.new(2,'test',60,encoded_job,999,consumer)
128
+ consumer.should_receive(:complete).with(2)
129
+ Chore::Worker.start(work)
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,108 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Chore do
4
+ before(:each) do
5
+ Chore.clear_hooks!
6
+ end
7
+ it 'should allow you to add a hook' do
8
+ blk = proc { true }
9
+ Chore.add_hook(:before_perform,&blk)
10
+ Chore.hooks_for(:before_perform).first.should == blk
11
+ end
12
+
13
+ it 'should call a hook if it exists' do
14
+ blk = proc { raise StandardError }
15
+ Chore.add_hook(:before_perform,&blk)
16
+ expect { Chore.run_hooks_for(:before_perform) }.to raise_error
17
+ end
18
+
19
+ it 'should not call a hook if it doesn\'t exist' do
20
+ blk = proc { raise StandardError }
21
+ expect { Chore.run_hooks_for(:before_perform) }.to_not raise_error
22
+ end
23
+
24
+ it 'should pass args to the block if present' do
25
+ blk = proc {|*args| true }
26
+ blk.should_receive(:call).with('1','2',3)
27
+ Chore.add_hook(:an_event,&blk)
28
+ Chore.run_hooks_for(:an_event, '1','2',3)
29
+ end
30
+
31
+ it 'should support multiple hooks for an event' do
32
+ blk = proc { true }
33
+ blk.should_receive(:call).twice
34
+ Chore.add_hook(:before_perform,&blk)
35
+ Chore.add_hook(:before_perform,&blk)
36
+
37
+ Chore.run_hooks_for(:before_perform)
38
+ end
39
+
40
+ it 'should support passing blocks' do
41
+ runner = proc { }
42
+
43
+ blk = proc { true }
44
+ blk.should_receive(:call) do |&arg1|
45
+ arg1.should_not be_nil
46
+ end
47
+ Chore.add_hook(:around_perform,&blk)
48
+
49
+ Chore.run_hooks_for(:around_perform, &runner)
50
+ end
51
+
52
+ it 'should call passed block' do
53
+ Chore.add_hook(:around_perform) do |&blk|
54
+ blk.call
55
+ end
56
+
57
+ run = false
58
+ Chore.run_hooks_for(:around_perform) { run = true }
59
+ run.should be_true
60
+ end
61
+
62
+ it 'should call passed blocks even if there are no hooks' do
63
+ run = false
64
+ Chore.run_hooks_for(:around_perform) { run = true }
65
+ run.should be_true
66
+ end
67
+
68
+ it 'should set configuration' do
69
+ Chore.configure {|c| c.test_config_option = 'howdy' }
70
+ Chore.config.test_config_option.should == 'howdy'
71
+ end
72
+
73
+ describe 'reopen_logs' do
74
+ let(:open_files) do
75
+ [
76
+ mock('file', :closed? => false, :reopen => nil, :sync= => nil, :path => '/a'),
77
+ mock('file2', :closed? => false, :reopen => nil, :sync= => nil, :path => '/b')
78
+ ]
79
+ end
80
+ let(:closed_files) do
81
+ [mock('file3', :closed? => true)]
82
+ end
83
+ let(:files) { open_files + closed_files }
84
+
85
+ before(:each) do
86
+ ObjectSpace.stub(:each_object).and_yield(open_files[0]).and_yield(open_files[1])
87
+ end
88
+
89
+ it 'should look up all instances of files' do
90
+ ObjectSpace.should_receive(:each_object).with(File)
91
+ Chore.reopen_logs
92
+ end
93
+
94
+ it 'should reopen files that are not closed' do
95
+ open_files.each do |file|
96
+ file.should_receive(:reopen).with(file.path, 'a+')
97
+ end
98
+ Chore.reopen_logs
99
+ end
100
+
101
+ it 'should sync files' do
102
+ open_files.each do |file|
103
+ file.should_receive(:sync=).with(true)
104
+ end
105
+ Chore.reopen_logs
106
+ end
107
+ end
108
+ end