sneakers 0.1.1.pre → 1.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.
@@ -0,0 +1,350 @@
1
+ require 'spec_helper'
2
+ require 'sneakers'
3
+ require 'timeout'
4
+ require 'sneakers/handlers/oneshot'
5
+ require 'sneakers/handlers/maxretry'
6
+ require 'json'
7
+
8
+
9
+ # Specific tests of the Handler implementations you can use to deal with job
10
+ # results. These tests only make sense with a worker that requires acking.
11
+
12
+ class HandlerTestWorker
13
+ include Sneakers::Worker
14
+ from_queue 'defaults',
15
+ :ack => true
16
+
17
+ def work(msg)
18
+ if msg.is_a?(StandardError)
19
+ raise msg
20
+ elsif msg.is_a?(String)
21
+ hash = maybe_json(msg)
22
+ if hash.is_a?(Hash)
23
+ hash['response'].to_sym
24
+ else
25
+ hash
26
+ end
27
+ else
28
+ msg
29
+ end
30
+ end
31
+
32
+ def maybe_json(string)
33
+ JSON.parse(string)
34
+ rescue
35
+ string
36
+ end
37
+ end
38
+
39
+ class TestPool
40
+ def process(*args,&block)
41
+ block.call
42
+ end
43
+ end
44
+
45
+
46
+ describe 'Handlers' do
47
+ let(:channel) { Object.new }
48
+ let(:queue) { Object.new }
49
+ let(:worker) { HandlerTestWorker.new(@queue, TestPool.new) }
50
+
51
+ before(:each) do
52
+ Sneakers.configure(:daemonize => true, :log => 'sneakers.log')
53
+ Sneakers::Worker.configure_logger(Logger.new('/dev/null'))
54
+ Sneakers::Worker.configure_metrics
55
+ end
56
+
57
+ describe 'Oneshot' do
58
+ before(:each) do
59
+ @opts = Object.new
60
+ @handler = Sneakers::Handlers::Oneshot.new(channel, queue, @opts)
61
+
62
+ @header = Object.new
63
+ stub(@header).delivery_tag { 37 }
64
+ end
65
+
66
+ describe '#do_work' do
67
+ it 'should work and handle acks' do
68
+ mock(channel).acknowledge(37, false)
69
+
70
+ worker.do_work(@header, nil, :ack, @handler)
71
+ end
72
+
73
+ it 'should work and handle rejects' do
74
+ mock(channel).reject(37, false)
75
+
76
+ worker.do_work(@header, nil, :reject, @handler)
77
+ end
78
+
79
+ it 'should work and handle requeues' do
80
+ mock(channel).reject(37, true)
81
+
82
+ worker.do_work(@header, nil, :requeue, @handler)
83
+ end
84
+
85
+ it 'should work and handle user-land timeouts' do
86
+ mock(channel).reject(37, false)
87
+
88
+ worker.do_work(@header, nil, :timeout, @handler)
89
+ end
90
+
91
+ it 'should work and handle user-land error' do
92
+ mock(channel).reject(37, false)
93
+
94
+ worker.do_work(@header, nil, StandardError.new('boom!'), @handler)
95
+ end
96
+
97
+ it 'should work and handle noops' do
98
+ worker.do_work(@header, nil, :wait, @handler)
99
+ end
100
+ end
101
+
102
+ end
103
+
104
+ describe 'Maxretry' do
105
+ let(:max_retries) { nil }
106
+
107
+ before(:each) do
108
+ @opts = {
109
+ :exchange => 'sneakers',
110
+ :durable => 'true',
111
+ }.tap do |opts|
112
+ opts[:retry_max_times] = max_retries unless max_retries.nil?
113
+ end
114
+
115
+ mock(queue).name { 'downloads' }
116
+
117
+ @retry_exchange = Object.new
118
+ @error_exchange = Object.new
119
+ @requeue_exchange = Object.new
120
+
121
+ @retry_queue = Object.new
122
+ @error_queue = Object.new
123
+
124
+ mock(channel).exchange('downloads-retry',
125
+ :type => 'topic',
126
+ :durable => 'true').once { @retry_exchange }
127
+ mock(channel).exchange('downloads-error',
128
+ :type => 'topic',
129
+ :durable => 'true').once { @error_exchange }
130
+ mock(channel).exchange('downloads-retry-requeue',
131
+ :type => 'topic',
132
+ :durable => 'true').once { @requeue_exchange }
133
+
134
+ mock(channel).queue('downloads-retry',
135
+ :durable => 'true',
136
+ :arguments => {
137
+ :'x-dead-letter-exchange' => 'downloads-retry-requeue',
138
+ :'x-message-ttl' => 60000
139
+ }
140
+ ).once { @retry_queue }
141
+ mock(@retry_queue).bind(@retry_exchange, :routing_key => '#')
142
+
143
+ mock(channel).queue('downloads-error',
144
+ :durable => 'true').once { @error_queue }
145
+ mock(@error_queue).bind(@error_exchange, :routing_key => '#')
146
+
147
+ mock(queue).bind(@requeue_exchange, :routing_key => '#')
148
+
149
+ @handler = Sneakers::Handlers::Maxretry.new(channel, queue, @opts)
150
+
151
+ @header = Object.new
152
+ stub(@header).delivery_tag { 37 }
153
+
154
+ @props = {}
155
+ @props_with_x_death = {
156
+ :headers => {
157
+ "x-death" => [
158
+ {
159
+ "reason" => "expired",
160
+ "queue" => "downloads-retry",
161
+ "time" => Time.now,
162
+ "exchange" => "RawMail-retry",
163
+ "routing-keys" => ["RawMail"]
164
+ },
165
+ {
166
+ "reason" => "rejected",
167
+ "queue" => "downloads",
168
+ "time" => Time.now,
169
+ "exchange" => "",
170
+ "routing-keys" => ["RawMail"]
171
+ }
172
+ ]
173
+ },
174
+ :delivery_mode => 1}
175
+ end
176
+
177
+ # it 'allows overriding the retry exchange name'
178
+ # it 'allows overriding the error exchange name'
179
+ # it 'allows overriding the retry timeout'
180
+
181
+ describe '#do_work' do
182
+ before do
183
+ @now = Time.now
184
+ end
185
+
186
+ # Used to stub out the publish method args. Sadly RR doesn't support
187
+ # this, only proxying existing methods.
188
+ module MockPublish
189
+ attr_reader :data, :opts, :called
190
+
191
+ def publish(data, opts)
192
+ @data = data
193
+ @opts = opts
194
+ @called = true
195
+ end
196
+ end
197
+
198
+ it 'should work and handle acks' do
199
+ mock(channel).acknowledge(37, false)
200
+
201
+ worker.do_work(@header, @props, :ack, @handler)
202
+ end
203
+
204
+ describe 'rejects' do
205
+ describe 'more retries ahead' do
206
+ it 'should work and handle rejects' do
207
+ mock(channel).reject(37, false)
208
+
209
+ worker.do_work(@header, @props_with_x_death, :reject, @handler)
210
+ end
211
+ end
212
+
213
+ describe 'no more retries' do
214
+ let(:max_retries) { 1 }
215
+
216
+ it 'sends the rejection to the error queue' do
217
+ mock(@header).routing_key { '#' }
218
+ mock(channel).acknowledge(37, false)
219
+
220
+ @error_exchange.extend MockPublish
221
+ worker.do_work(@header, @props_with_x_death, :reject, @handler)
222
+ @error_exchange.called.must_equal(true)
223
+ @error_exchange.opts.must_equal({ :routing_key => '#' })
224
+ data = JSON.parse(@error_exchange.data)
225
+ data['error'].must_equal('reject')
226
+ data['num_attempts'].must_equal(2)
227
+ data['payload'].must_equal(Base64.encode64(:reject.to_s))
228
+ Time.parse(data['failed_at']).wont_be_nil
229
+ end
230
+
231
+ end
232
+ end
233
+
234
+ describe 'requeues' do
235
+ it 'should work and handle requeues' do
236
+ mock(channel).reject(37, true)
237
+
238
+ worker.do_work(@header, @props_with_x_death, :requeue, @handler)
239
+ end
240
+
241
+ describe 'no more retries left' do
242
+ let(:max_retries) { 1 }
243
+
244
+ it 'continues to reject with requeue' do
245
+ mock(channel).reject(37, true)
246
+
247
+ worker.do_work(@header, @props_with_x_death, :requeue, @handler)
248
+ end
249
+ end
250
+
251
+ end
252
+
253
+ describe 'timeouts' do
254
+ describe 'more retries ahead' do
255
+ it 'should reject the message' do
256
+ mock(channel).reject(37, false)
257
+
258
+ worker.do_work(@header, @props_with_x_death, :timeout, @handler)
259
+ end
260
+ end
261
+
262
+ describe 'no more retries left' do
263
+ let(:max_retries) { 1 }
264
+
265
+ it 'sends the rejection to the error queue' do
266
+ mock(@header).routing_key { '#' }
267
+ mock(channel).acknowledge(37, false)
268
+ @error_exchange.extend MockPublish
269
+
270
+ worker.do_work(@header, @props_with_x_death, :timeout, @handler)
271
+ @error_exchange.called.must_equal(true)
272
+ @error_exchange.opts.must_equal({ :routing_key => '#' })
273
+ data = JSON.parse(@error_exchange.data)
274
+ data['error'].must_equal('timeout')
275
+ data['num_attempts'].must_equal(2)
276
+ data['payload'].must_equal(Base64.encode64(:timeout.to_s))
277
+ Time.parse(data['failed_at']).wont_be_nil
278
+ end
279
+ end
280
+ end
281
+
282
+ describe 'exceptions' do
283
+ describe 'more retries ahead' do
284
+ it 'should reject the message' do
285
+ mock(channel).reject(37, false)
286
+
287
+ worker.do_work(@header, @props_with_x_death, StandardError.new('boom!'), @handler)
288
+ end
289
+ end
290
+
291
+ describe 'no more retries left' do
292
+ let(:max_retries) { 1 }
293
+
294
+ it 'sends the rejection to the error queue' do
295
+ mock(@header).routing_key { '#' }
296
+ mock(channel).acknowledge(37, false)
297
+ @error_exchange.extend MockPublish
298
+
299
+ worker.do_work(@header, @props_with_x_death, StandardError.new('boom!'), @handler)
300
+ @error_exchange.called.must_equal(true)
301
+ @error_exchange.opts.must_equal({ :routing_key => '#' })
302
+ data = JSON.parse(@error_exchange.data)
303
+ data['error'].must_equal('boom!')
304
+ data['error_class'].must_equal(StandardError.to_s)
305
+ data['backtrace'].wont_be_nil
306
+ data['num_attempts'].must_equal(2)
307
+ data['payload'].must_equal(Base64.encode64('boom!'))
308
+ Time.parse(data['failed_at']).wont_be_nil
309
+ end
310
+ end
311
+ end
312
+
313
+ it 'should work and handle user-land error' do
314
+ mock(channel).reject(37, false)
315
+
316
+ worker.do_work(@header, @props, StandardError.new('boom!'), @handler)
317
+ end
318
+
319
+ it 'should work and handle noops' do
320
+ worker.do_work(@header, @props, :wait, @handler)
321
+ end
322
+
323
+ # Since we encode in json, we want to make sure if the actual payload is
324
+ # json, then it's something you can get back out.
325
+ describe 'JSON payloads' do
326
+ let(:max_retries) { 1 }
327
+
328
+ it 'properly encodes the json payload' do
329
+ mock(@header).routing_key { '#' }
330
+ mock(channel).acknowledge(37, false)
331
+ @error_exchange.extend MockPublish
332
+
333
+ payload = {
334
+ data: 'hello',
335
+ response: :timeout
336
+ }
337
+ worker.do_work(@header, @props_with_x_death, payload.to_json, @handler)
338
+ @error_exchange.called.must_equal(true)
339
+ @error_exchange.opts.must_equal({ :routing_key => '#' })
340
+ data = JSON.parse(@error_exchange.data)
341
+ data['error'].must_equal('timeout')
342
+ data['num_attempts'].must_equal(2)
343
+ data['payload'].must_equal(Base64.encode64(payload.to_json))
344
+ end
345
+
346
+ end
347
+
348
+ end
349
+ end
350
+ end
@@ -96,7 +96,7 @@ class WithParamsWorker
96
96
  :ack => true,
97
97
  :timeout_job_after => 0.5
98
98
 
99
- def work_with_params(msg, header, props)
99
+ def work_with_params(msg, delivery_info, metadata)
100
100
  msg
101
101
  end
102
102
  end
@@ -108,13 +108,6 @@ class TestPool
108
108
  end
109
109
  end
110
110
 
111
- class TestHandler
112
- def acknowledge(tag); end
113
- def reject(tag); end
114
- def error(tag, err); end
115
- def timeout(tag); end
116
- end
117
-
118
111
  def with_test_queuefactory(ctx, ack=true, msg=nil, nowork=false)
119
112
  qf = Object.new
120
113
  q = Object.new
@@ -139,7 +132,6 @@ describe Sneakers::Worker do
139
132
  stub(@queue).exchange { @exchange }
140
133
 
141
134
  Sneakers.configure(:daemonize => true, :log => 'sneakers.log')
142
- Sneakers::Worker.configure_logger(Logger.new('/dev/null'))
143
135
  Sneakers::Worker.configure_metrics
144
136
  end
145
137
 
@@ -167,14 +159,14 @@ describe Sneakers::Worker do
167
159
 
168
160
  it "should build a queue with correct configuration given defaults" do
169
161
  @defaults_q.name.must_equal('defaults')
170
- @defaults_q.opts.must_equal(
162
+ @defaults_q.opts.to_hash.must_equal(
171
163
  {:runner_config_file=>nil, :metrics=>nil, :daemonize=>true, :start_worker_delay=>0.2, :workers=>4, :log=>"sneakers.log", :pid_path=>"sneakers.pid", :timeout_job_after=>5, :prefetch=>10, :threads=>10, :durable=>true, :ack=>true, :amqp=>"amqp://guest:guest@localhost:5672", :vhost=>"/", :exchange=>"sneakers", :exchange_type=>:direct, :hooks=>{}, :handler=>Sneakers::Handlers::Oneshot, :heartbeat => 2}
172
164
  )
173
165
  end
174
166
 
175
167
  it "should build a queue with given configuration" do
176
168
  @dummy_q.name.must_equal('downloads')
177
- @dummy_q.opts.must_equal(
169
+ @dummy_q.opts.to_hash.must_equal(
178
170
  {:runner_config_file=>nil, :metrics=>nil, :daemonize=>true, :start_worker_delay=>0.2, :workers=>4, :log=>"sneakers.log", :pid_path=>"sneakers.pid", :timeout_job_after=>1, :prefetch=>40, :threads=>50, :durable=>false, :ack=>false, :amqp=>"amqp://guest:guest@localhost:5672", :vhost=>"/", :exchange=>"dummy", :exchange_type=>:direct, :hooks=>{}, :handler=>Sneakers::Handlers::Oneshot, :heartbeat =>5}
179
171
  )
180
172
  end
@@ -222,9 +214,19 @@ describe Sneakers::Worker do
222
214
  w = AcksWorker.new(@queue, TestPool.new)
223
215
  mock(w).work("msg").once{ raise "foo" }
224
216
  handler = Object.new
225
- mock(handler).error("tag", anything)
226
217
  header = Object.new
227
- stub(header).delivery_tag { "tag" }
218
+ mock(handler).error(header, nil, "msg", anything)
219
+ mock(w.logger).error(/unexpected error \[Exception error="foo" error_class=RuntimeError backtrace=.*/)
220
+ w.do_work(header, nil, "msg", handler)
221
+ end
222
+
223
+ it "should log exceptions from workers" do
224
+ handler = Object.new
225
+ header = Object.new
226
+ w = AcksWorker.new(@queue, TestPool.new)
227
+ mock(w).work("msg").once{ raise "foo" }
228
+ mock(w.logger).error(/error="foo" error_class=RuntimeError backtrace=/)
229
+ mock(handler).error(header, nil, "msg", anything)
228
230
  w.do_work(header, nil, "msg", handler)
229
231
  end
230
232
 
@@ -233,55 +235,55 @@ describe Sneakers::Worker do
233
235
  stub(w).work("msg"){ sleep 10 }
234
236
 
235
237
  handler = Object.new
236
- mock(handler).timeout("tag")
237
-
238
238
  header = Object.new
239
- stub(header).delivery_tag { "tag" }
239
+
240
+ mock(handler).timeout(header, nil, "msg")
241
+ mock(w.logger).error(/timeout/)
240
242
 
241
243
  w.do_work(header, nil, "msg", handler)
242
244
  end
243
245
 
244
246
  describe "with ack" do
245
247
  before do
246
- @header = Object.new
247
- stub(@header).delivery_tag{ "tag" }
248
+ @delivery_info = Object.new
249
+ stub(@delivery_info).delivery_tag{ "tag" }
248
250
 
249
251
  @worker = AcksWorker.new(@queue, TestPool.new)
250
252
  end
251
253
 
252
254
  it "should work and handle acks" do
253
255
  handler = Object.new
254
- mock(handler).acknowledge("tag")
256
+ mock(handler).acknowledge(@delivery_info, nil, :ack)
255
257
 
256
- @worker.do_work(@header, nil, :ack, handler)
258
+ @worker.do_work(@delivery_info, nil, :ack, handler)
257
259
  end
258
260
 
259
261
  it "should work and handle rejects" do
260
262
  handler = Object.new
261
- mock(handler).reject("tag")
263
+ mock(handler).reject(@delivery_info, nil, :reject)
262
264
 
263
- @worker.do_work(@header, nil, :reject, handler)
265
+ @worker.do_work(@delivery_info, nil, :reject, handler)
264
266
  end
265
267
 
266
268
  it "should work and handle requeues" do
267
269
  handler = Object.new
268
- mock(handler).reject("tag", true)
270
+ mock(handler).reject(@delivery_info, nil, :requeue, true)
269
271
 
270
- @worker.do_work(@header, nil, :requeue, handler)
272
+ @worker.do_work(@delivery_info, nil, :requeue, handler)
271
273
  end
272
274
 
273
275
  it "should work and handle user-land timeouts" do
274
276
  handler = Object.new
275
- mock(handler).timeout("tag")
277
+ mock(handler).timeout(@delivery_info, nil, :timeout)
276
278
 
277
- @worker.do_work(@header, nil, :timeout, handler)
279
+ @worker.do_work(@delivery_info, nil, :timeout, handler)
278
280
  end
279
281
 
280
282
  it "should work and handle user-land error" do
281
283
  handler = Object.new
282
- mock(handler).error("tag",anything)
284
+ mock(handler).error(@delivery_info, nil, :error, anything)
283
285
 
284
- @worker.do_work(@header, nil, :error, handler)
286
+ @worker.do_work(@delivery_info, nil, :error, handler)
285
287
  end
286
288
  end
287
289
 
@@ -304,6 +306,12 @@ describe Sneakers::Worker do
304
306
  mock(@exchange).publish('msg', :routing_key => 'target').once
305
307
  w.do_work(nil, nil, 'msg', nil)
306
308
  end
309
+
310
+ it 'should be able to publish arbitrary metadata' do
311
+ w = PublishingWorker.new(@queue, TestPool.new)
312
+ mock(@exchange).publish('msg', :routing_key => 'target', :expiration => 1).once
313
+ w.publish 'msg', :to_queue => 'target', :expiration => 1
314
+ end
307
315
  end
308
316
 
309
317
 
@@ -317,20 +325,39 @@ describe Sneakers::Worker do
317
325
  w = LoggingWorker.new(@queue, TestPool.new)
318
326
  w.do_work(nil,nil,'msg',nil)
319
327
  end
328
+
329
+ it 'has a helper to constuct log prefix values' do
330
+ w = DummyWorker.new(@queue, TestPool.new)
331
+ w.instance_variable_set(:@id, 'worker-id')
332
+ m = w.log_msg('foo')
333
+ w.log_msg('foo').must_match(/\[worker-id\]\[#<Thread:.*>\]\[test-queue\]\[\{\}\] foo/)
334
+ end
335
+
336
+ describe '#worker_error' do
337
+ it 'only logs backtraces if present' do
338
+ w = DummyWorker.new(@queue, TestPool.new)
339
+ mock(w.logger).error(/cuz \[Exception error="boom!" error_class=RuntimeError\]/)
340
+ w.worker_error('cuz', RuntimeError.new('boom!'))
341
+ end
342
+ end
343
+
320
344
  end
321
345
 
322
346
 
323
347
  describe 'Metrics' do
324
348
  before do
325
349
  @handler = Object.new
326
- stub(@handler).acknowledge("tag")
327
- stub(@handler).reject("tag")
328
- stub(@handler).timeout("tag")
329
- stub(@handler).error("tag", anything)
330
- stub(@handler).noop("tag")
331
-
332
350
  @header = Object.new
333
- stub(@header).delivery_tag { "tag" }
351
+
352
+ # We don't care how these are called, we're focusing on metrics here.
353
+ stub(@handler).acknowledge
354
+ stub(@handler).reject
355
+ stub(@handler).timeout
356
+ stub(@handler).error
357
+ stub(@handler).noop
358
+
359
+ @delivery_info = Object.new
360
+ stub(@delivery_info).delivery_tag { "tag" }
334
361
 
335
362
  @w = MetricsWorker.new(@queue, TestPool.new)
336
363
  mock(@w.metrics).increment("work.MetricsWorker.started").once
@@ -341,25 +368,37 @@ describe Sneakers::Worker do
341
368
  it 'should be able to meter acks' do
342
369
  mock(@w.metrics).increment("foobar").once
343
370
  mock(@w.metrics).increment("work.MetricsWorker.handled.ack").once
344
- @w.do_work(@header, nil, :ack, @handler)
371
+ @w.do_work(@delivery_info, nil, :ack, @handler)
345
372
  end
346
373
 
347
374
  it 'should be able to meter rejects' do
348
375
  mock(@w.metrics).increment("foobar").once
349
376
  mock(@w.metrics).increment("work.MetricsWorker.handled.reject").once
350
- @w.do_work(@header, nil, nil, @handler)
377
+ @w.do_work(@header, nil, :reject, @handler)
378
+ end
379
+
380
+ it 'should be able to meter requeue' do
381
+ mock(@w.metrics).increment("foobar").once
382
+ mock(@w.metrics).increment("work.MetricsWorker.handled.requeue").once
383
+ @w.do_work(@header, nil, :requeue, @handler)
351
384
  end
352
385
 
353
386
  it 'should be able to meter errors' do
354
387
  mock(@w.metrics).increment("work.MetricsWorker.handled.error").once
355
388
  mock(@w).work('msg'){ raise :error }
356
- @w.do_work(@header, nil, 'msg', @handler)
389
+ @w.do_work(@delivery_info, nil, 'msg', @handler)
357
390
  end
358
391
 
359
392
  it 'should be able to meter timeouts' do
360
393
  mock(@w.metrics).increment("work.MetricsWorker.handled.timeout").once
361
394
  mock(@w).work('msg'){ sleep 10 }
362
- @w.do_work(@header, nil, 'msg', @handler)
395
+ @w.do_work(@delivery_info, nil, 'msg', @handler)
396
+ end
397
+
398
+ it 'defaults to noop when no response is specified' do
399
+ mock(@w.metrics).increment("foobar").once
400
+ mock(@w.metrics).increment("work.MetricsWorker.handled.noop").once
401
+ @w.do_work(@header, nil, nil, @handler)
363
402
  end
364
403
  end
365
404
 
@@ -367,23 +406,21 @@ describe Sneakers::Worker do
367
406
 
368
407
  describe 'With Params' do
369
408
  before do
409
+ @props = { :foo => 1 }
370
410
  @handler = Object.new
371
- stub(@handler).acknowledge("tag")
372
- stub(@handler).reject("tag")
373
- stub(@handler).timeout("tag")
374
- stub(@handler).error("tag", anything)
375
- stub(@handler).noop("tag")
376
-
377
411
  @header = Object.new
378
- stub(@header).delivery_tag { "tag" }
412
+
413
+ @delivery_info = Object.new
414
+
415
+ stub(@handler).noop(@delivery_info, {:foo => 1}, :ack)
379
416
 
380
417
  @w = WithParamsWorker.new(@queue, TestPool.new)
381
418
  mock(@w.metrics).timing("work.WithParamsWorker.time").yields.once
382
419
  end
383
420
 
384
421
  it 'should call work_with_params and not work' do
385
- mock(@w).work_with_params(:ack, @header, {:foo => 1}).once
386
- @w.do_work(@header, {:foo => 1 }, :ack, @handler)
422
+ mock(@w).work_with_params(:ack, @delivery_info, {:foo => 1}).once
423
+ @w.do_work(@delivery_info, {:foo => 1 }, :ack, @handler)
387
424
  end
388
425
  end
389
426
  end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require 'simplecov'
2
- SimpleCov.start
2
+ SimpleCov.start do
3
+ add_filter "/spec/"
4
+ end
3
5
 
4
6
  require 'minitest/autorun'
5
7