sneakers 0.1.1.pre → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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