chore-core 1.5.2

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 (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