banter 1.2.3 → 1.2.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b13b654e7432d2615637212aa89c5e2982132c4f
4
- data.tar.gz: 7574a9284911946d6ea20f7dab4c5edb4f6cd132
3
+ metadata.gz: 07b71e3cd5e4d50cdbadff4bfc6aeb90cffa749c
4
+ data.tar.gz: 541920f865408bedb92d9b9bd0b0490adf7f13d1
5
5
  SHA512:
6
- metadata.gz: 9b0e607a13ddfc3f25f018dc99c756c37969ed1cbd2054a297811e9aa87ed9d2d8377d97fad4bcb7fec4947aece16969d19ec1f6bd8a62ee016e115cf459fb85
7
- data.tar.gz: b22048fb8320d300d739b46ad5cfb4b77a06bba4e80384824924ba105a574a82ac1ad92c2d8ff5db2f72c9de56da2642eb1b03842c6886621cb4c2fb14dd35fb
6
+ metadata.gz: 656c84442869c91309fcbb93c1303b527c4b03b107d53cc4354154928e7a0a0273e9e9ad5bdbeafc0b841141d8f7c94e40e5adb0b7cc43952db1a832cc8ade44
7
+ data.tar.gz: 943010a912bd147ca77ec5d5c6658b89688c74d2c4b194a9eee2bcbbd5777bbcf935e6cc19234a28c6ca7937dc4e500420b13891b59296371a0fb5b2adc17b53
@@ -1,33 +1,32 @@
1
- # Internal log class that is used to log messages before sending, after receiving, and failure to send
1
+ # encoding: utf-8
2
+ # Internal log class that is used to log envelopes before sending, after receiving, and failure to send
2
3
  module Banter
3
4
 
4
5
  class RabbitLogger
6
+ @@log_map = [:debug, :info, :warn, :error, :fatal]
5
7
  def self.enabled?
6
- Banter::Configuration.logging_enabled
8
+ true
9
+ # Banter::Configuration.logging_enabled
7
10
  end
8
11
 
9
- def self.log_publish(routing_key, message)
10
- return unless enabled?
11
- tags = ["BANTER PUBLISH", message[:ts], message[:pub], message[:v], routing_key]
12
- logger.tagged(tags) { logger.warn message[:payload].as_json }
12
+ def self.log_publish(routing_key, envelope)
13
+ internal_log(:warn, "BANTER PUBLISH", envelope, routing_key, envelope[:payload])
13
14
  end
14
15
 
15
- def self.failed_publish(routing_key, properties, message)
16
- return unless enabled?
17
- tags = ["BANTER FAILED_SEND", message[:ts], message[:pub], message[:v], routing_key]
18
- logger.tagged(tags) { logger.error( { properties: properties, payload:message[:payload] }.as_json ) }
16
+ def self.failed_publish(routing_key, properties, envelope)
17
+ internal_log(:error, "BANTER FAILED_SEND", envelope, routing_key, { properties: properties, payload:envelope[:payload] })
19
18
  end
20
19
 
21
- def self.log_receive(routing_key, message)
22
- return unless enabled?
23
- tags = ["BANTER RECEIVED", message[:ts], message[:pub], message[:v], routing_key, Process::pid]
24
- logger.tagged(tags) { logger.warn message[:payload].as_json }
20
+ def self.log_receive(routing_key, envelope)
21
+ internal_log(:warn, "BANTER RECEIVED", envelope, routing_key, envelope[:payload])
25
22
  end
26
23
 
27
- def self.log_complete(routing_key, message)
28
- return unless enabled?
29
- tags = ["BANTER COMPLETED", message[:ts], message[:pub], message[:v], routing_key, Process::pid]
30
- logger.tagged(tags) { logger.warn message[:payload].as_json }
24
+ def self.log_complete(routing_key, envelope)
25
+ internal_log(:warn, "BANTER COMPLETED", envelope, routing_key, envelope[:payload])
26
+ end
27
+
28
+ def self.log_subscriber_failed(routing_key, delivery_info, properties, envelope)
29
+ internal_log(:warn, "BANTER SUBSCRIBER FAILED", envelope, routing_key, { delivery_info: delivery_info, properties: properties, contents: envelope[:payload]} )
31
30
  end
32
31
 
33
32
  def self.log_service(service_name, message)
@@ -36,15 +35,24 @@ module Banter
36
35
  logger.tagged(tags) { logger.info message.as_json}
37
36
  end
38
37
 
39
- @@log_map = [:debug, :info, :warn, :error, :fatal]
40
38
  def self.log(log_level, message)
41
- tags = ["BANTER LOG_LEVEL:#{@@log_map[log_level].capitalize.to_s}", Process::pid]
42
- logger.tagged(tags) { logger.send(@@log_map[log_level], message.as_json ) }
39
+ tags = ["BANTER LOG_LEVEL:#{@@log_map[log_level].capitalize.to_s}", Process::pid.to_s].map{|x| utf(x)}
40
+ logger.tagged(tags) { logger.send(@@log_map[log_level], utf(message.as_json.to_s) ) }
41
+ end
42
+
43
+ def self.internal_log(log_level, log_type, envelope, routing_key, payload)
44
+ return unless enabled?
45
+ tags = [log_type, envelope[:ts].to_s, envelope[:pub], envelope[:v].to_s, routing_key].map{|x| utf(x)}
46
+ logger.tagged(tags) { logger.send(log_level, utf(payload.to_s)) }
43
47
  end
44
48
 
45
49
  def self.logger
46
50
  Banter.logger
47
51
  end
48
52
 
53
+ def self.utf(field)
54
+ field.force_encoding('utf-8')
55
+ end
56
+
49
57
  end
50
58
  end
@@ -19,7 +19,7 @@ module Banter
19
19
  end
20
20
 
21
21
  def start
22
- @subscriber.start(false) do |delivery_info, properties, envelope|
22
+ @subscriber.start do |delivery_info, properties, envelope|
23
23
  message_received(delivery_info, properties, envelope)
24
24
  true
25
25
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Banter
2
4
  module Server
3
5
 
@@ -23,15 +25,13 @@ module Banter
23
25
  @queue_name = queue_name
24
26
  end
25
27
 
26
- # pass in a lambda for this method to work. We might only want to expose the content instead of
27
- # all 3 chunks.
28
- def start(blocking=false)
28
+ def start(&block)
29
29
  @connection = Bunny.new(Configuration.connection)
30
30
  begin
31
31
  @connection.start
32
32
  rescue => e
33
33
  Banter::RabbitLogger.log(Logger::ERROR, "Cannot connect to rabbitmq")
34
- Airbrake.notify(e, params: { message: e.message, what_happened: "RabbitMQ unreachable!" }, environment_name: ENV['RAILS_ENV'])
34
+ Airbrake.notify(e, params: { message: e.message, what_happened: "RabbitMQ unreachable!" }, environment_name: environment)
35
35
  raise e
36
36
  end
37
37
 
@@ -44,28 +44,52 @@ module Banter
44
44
 
45
45
  @channel.basic_qos(@pool_size) if @pool_size != 0
46
46
 
47
+ @channel.on_uncaught_exception(&method(:notify_error))
48
+
47
49
  rabbit_queue = @channel.queue(@queue_name, durable: @durable, exclusive: false, arguments: queue_arguments)
48
- @listener = rabbit_queue.bind(@exchange, routing_key: @routing_key, exclusive: false)
50
+ @listener = rabbit_queue.bind(@exchange, routing_key: @routing_key, exclusive: false)
51
+
52
+ @callback_block = block
53
+ @consumer = @listener.subscribe({ consumer_tag: @queue_name, manual_ack: true, block: false}, &method(:process_message))
54
+ nil
55
+ end
49
56
 
50
- # Parameters for subscribe that might be useful:
51
- # :block=>true - Used for long running consumer applications. (backend servers?)
57
+ def process_message(delivery_info, properties, contents)
58
+ token = delivery_info.delivery_tag
59
+ begin
60
+ envelope = ::Banter::Message.new.parse(contents)
61
+ Banter::RabbitLogger.log(Logger::DEBUG, "Message delivery with contents: #{token}: #{delivery_info[:routing_key]}, #{contents}, #{contents.encoding}")
52
62
 
53
- @consumer = @listener.subscribe(consumer_tag: @queue_name, manual_ack: true, block: blocking) do |delivery_info, properties, contents|
54
- Banter::RabbitLogger.log(Logger::DEBUG, "Message delivery with contents: #{contents}")
55
63
  if delivery_info[:redelivered]
56
64
  e = StandardError.new("PubSub Message redelivery")
57
- Airbrake.notify(e, params: { info: delivery_info, props: properties, contents: contents }, environment_name: ENV['RAILS_ENV'], backtrace: caller)
65
+ Airbrake.notify(e, params: { info: delivery_info, props: properties, contents: contents }, backtrace: caller)
58
66
  end
59
- message = ::Banter::Message.new.parse(contents)
60
- Banter::RabbitLogger.log_receive(delivery_info[:routing_key], message)
61
- yield delivery_info, properties, message
62
- Banter::RabbitLogger.log_complete(delivery_info[:routing_key], message)
63
- Banter::RabbitLogger.log(Logger::DEBUG, "Message acknowledged with tag #{delivery_info.delivery_tag}")
67
+
68
+ Banter::RabbitLogger.log_receive(delivery_info[:routing_key], envelope)
69
+ @callback_block.call( delivery_info, properties, envelope)
70
+ Banter::RabbitLogger.log_complete(delivery_info[:routing_key], envelope)
71
+
64
72
  # Need to acknowledge the message for the next message to come down.
65
- @channel.ack(delivery_info.delivery_tag)
66
- true
73
+ Banter::RabbitLogger.log(Logger::DEBUG, "Message acknowledged with tag #{token}")
74
+ @channel.ack(token)
75
+ rescue => e
76
+ Banter::RabbitLogger.log(Logger::WARN, "Error in message: #{e}")
77
+ e.backtrace.each{|line| Banter::RabbitLogger.log(Logger::WARN, "-- #{line}") }
78
+ Banter::RabbitLogger.log(Logger::WARN, "contents: #{contents}")
79
+ Banter::RabbitLogger.log(Logger::WARN, { routing_key: delivery_info[:routing_key], delivery_info: delivery_info, properties: properties, envelope: envelope} )
80
+ Banter::RabbitLogger.log_subscriber_failed(delivery_info[:routing_key], delivery_info, properties, envelope)
81
+
82
+ # Does not get put back on the queue, and instead, will need to be processed either by the log parser
83
+ # later or by dead letter exchange
84
+ @channel.reject(token, false)
85
+ Airbrake.notify(e, params: { delivery_info: delivery_info, properties: properties, contents: contents, error: e.message }, environment_name: environment)
67
86
  end
68
- nil
87
+ end
88
+
89
+ def notify_error(error, consumer)
90
+ Banter::RabbitLogger.log(Logger::WARN, "Error with the message: #{error.message}: #{consumer}")
91
+ Banter::RabbitLogger.log(Logger::WARN, "#{caller.inspect}")
92
+ Airbrake.notify(error, params: { consumer: consumer, error: error.message }, environment_name: environment)
69
93
  end
70
94
 
71
95
  def teardown
@@ -73,9 +97,13 @@ module Banter
73
97
  @consumer.cancel if @consumer.present?
74
98
  @connection.close if @connection.present?
75
99
  rescue => e
76
- Airbrake.notify(e, params: { error: e.message }, environment_name: ENV['RAILS_ENV'], backtrace: caller)
100
+ Airbrake.notify(e, params: { error: e.message }, environment_name: environment)
77
101
  end
78
102
  end
103
+
104
+ def environment
105
+ ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
106
+ end
79
107
  end
80
108
  end
81
109
  end
@@ -1,3 +1,3 @@
1
1
  module Banter
2
- VERSION = "1.2.3"
2
+ VERSION = "1.2.4"
3
3
  end
@@ -5,10 +5,11 @@ describe Banter::Publisher do
5
5
  let(:routing_key) { "test/logger" }
6
6
  let(:publisher) { Banter::Publisher.new }
7
7
  let(:context) { {unique_id: "1234", orig_ip_address: "127.0.0.1"}}
8
+ let(:contents) { Message::new.serialize(context, routing_key, {"a"=>"b"})}
8
9
  let(:channel_mocker) {
9
10
  topic = double("::Bunny::Exchange", :publish=> true )
10
11
  if publish_fails
11
- topic.stub(:on_return).and_yield({routing_key: 'a.b.c'}, {some: 'property'}, '{"a":"b"}').and_return(false)
12
+ topic.stub(:on_return).and_yield({routing_key: 'a.b.c'}, {some: 'property'}, contents).and_return(false)
12
13
  else
13
14
  topic.stub(:on_return).and_return true
14
15
  end
@@ -1,98 +1,117 @@
1
+ #encoding: utf-8
1
2
  require 'spec_helper'
2
3
 
3
4
  describe Banter::RabbitLogger do
4
- let(:routing_key) { "test/logger" }
5
+ let(:routing_key) { "test.logger" }
5
6
  let(:subject) { Banter::RabbitLogger }
6
7
  let(:payload) { {"hit"=>"me"} }
7
8
  let(:message) { Banter::Message.new.serialize(context, routing_key, payload) }
8
9
  let(:config_buffer) { StringIO.new }
9
10
  let(:output_string) { config_buffer.string }
11
+ let(:delivery_info) { {empty: true}}
12
+ let(:properties) { {empty: true}}
10
13
  let(:context) { {unique_id: "1234", orig_ip_address: "127.0.0.1"}}
11
14
 
12
- before do
13
- allow(Banter.logger).to receive(:debug)
14
- allow(Banter.logger).to receive(:info)
15
- allow(Banter.logger).to receive(:warn)
16
- allow(Banter.logger).to receive(:error)
17
- allow(Banter.logger).to receive(:log)
18
- end
15
+ context "standard character set" do
16
+ before do
17
+ allow(Banter.logger).to receive(:debug)
18
+ allow(Banter.logger).to receive(:info)
19
+ allow(Banter.logger).to receive(:warn)
20
+ allow(Banter.logger).to receive(:error)
21
+ allow(Banter.logger).to receive(:log)
22
+ end
19
23
 
20
- describe "#log_publish" do
21
- let!(:result) { subject.log_publish(routing_key, message)}
24
+ describe "#log_publish" do
25
+ let!(:result) { subject.log_publish(routing_key, message)}
22
26
 
23
- it "should not fail" do
24
- expect{ result }.not_to raise_error
25
- end
27
+ it "should not fail" do
28
+ expect{ result }.not_to raise_error
29
+ end
30
+
31
+ it "should write a row to the log" do
32
+ expect(Banter.logger).to have_received(:warn).with(anything)
33
+ end
26
34
 
27
- it "should write a row to the log" do
28
- expect(Banter.logger).to have_received(:warn).with(anything)
29
35
  end
30
36
 
31
- end
37
+ describe "#failed_publish" do
38
+ let!(:result) { subject.failed_publish(routing_key, {}, message)}
32
39
 
33
- describe "#failed_publish" do
34
- let!(:result) { subject.failed_publish(routing_key, {}, message)}
40
+ context "warning log level" do
35
41
 
36
- context "warning log level" do
42
+ it "should not fail" do
43
+ expect{ result }.not_to raise_error()
44
+ end
45
+
46
+ it "should respect the log level of the file" do
47
+ expect(Banter.logger).to have_received(:error).with(anything)
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "#log_receive" do
53
+ let!(:result) { subject.log_receive(routing_key, message)}
37
54
 
38
55
  it "should not fail" do
39
56
  expect{ result }.not_to raise_error()
40
57
  end
41
58
 
42
59
  it "should respect the log level of the file" do
43
- expect(Banter.logger).to have_received(:error).with(anything)
60
+ expect(Banter.logger).to have_received(:warn).with(anything)
44
61
  end
45
62
  end
46
- end
47
63
 
48
- describe "#log_receive" do
49
- let!(:result) { subject.log_receive(routing_key, message)}
64
+ describe "#log_complete" do
65
+ let!(:result) { subject.log_complete(routing_key, message)}
50
66
 
51
- it "should not fail" do
52
- expect{ result }.not_to raise_error()
53
- end
67
+ it "should not fail" do
68
+ expect{ result }.not_to raise_error()
69
+ end
54
70
 
55
- it "should respect the log level of the file" do
56
- expect(Banter.logger).to have_received(:warn).with(anything)
71
+ it "should respect the log level of the file" do
72
+ expect(Banter.logger).to have_received(:warn).with(anything)
73
+ end
57
74
  end
58
- end
59
75
 
60
- describe "#log_complete" do
61
- let!(:result) { subject.log_complete(routing_key, message)}
76
+ describe "#log" do
77
+ let!(:result) { subject.log(::Logger::DEBUG, message)}
62
78
 
63
- it "should not fail" do
64
- expect{ result }.not_to raise_error()
65
- end
79
+ it "should not fail" do
80
+ expect{ result }.not_to raise_error()
81
+ end
66
82
 
67
- it "should respect the log level of the file" do
68
- expect(Banter.logger).to have_received(:warn).with(anything)
83
+ it "should respect the log level of the file" do
84
+ expect(Banter.logger).to have_received(:debug).with(anything)
85
+ end
69
86
  end
70
- end
71
87
 
72
- describe "#log" do
73
- let!(:result) { subject.log(::Logger::DEBUG, message)}
88
+ describe "#log_service" do
89
+ let(:service_name) { "test" }
90
+ let!(:result) { subject.log_service(service_name, message)}
74
91
 
75
- it "should not fail" do
76
- expect{ result }.not_to raise_error()
77
- end
92
+ context "debugger log" do
93
+ it "should not fail" do
94
+ expect{ result }.not_to raise_error()
95
+ end
78
96
 
79
- it "should respect the log level of the file" do
80
- expect(Banter.logger).to have_received(:debug).with(anything)
97
+ it "should respect the log level of the file" do
98
+ expect(Banter.logger).to have_received(:info).with(anything)
99
+ end
100
+ end
81
101
  end
82
- end
83
102
 
84
- describe "#log_service" do
85
- let(:service_name) { "test" }
86
- let!(:result) { subject.log_service(service_name, message)}
103
+ describe "#log_subscriber_failed" do
104
+ let!(:result) { subject.log_subscriber_failed(routing_key, delivery_info, properties, message)}
87
105
 
88
- context "debugger log" do
89
106
  it "should not fail" do
90
107
  expect{ result }.not_to raise_error()
91
108
  end
92
109
 
93
110
  it "should respect the log level of the file" do
94
- expect(Banter.logger).to have_received(:info).with(anything)
111
+ expect(Banter.logger).to have_received(:warn).with(anything)
95
112
  end
113
+
96
114
  end
97
115
  end
116
+
98
117
  end
@@ -1,5 +1,126 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Banter::Server::RabbitMQSubscriber do
4
- subject { Banter::Server::RabbitMQSubscriber.new( routing_key ) }
5
- end
4
+ let(:routing_key) { "fake.route"}
5
+ let(:subscriber){ Banter::Server::RabbitMQSubscriber.new( routing_key, "queue_name", 100 ) }
6
+ let(:delivery_info) { Hashie::Mash.new({ :delivery_tag=>1, :routing_key=> routing_key}) }
7
+ let(:properties) { {} }
8
+ let(:contents) { File.read( File.join(File.dirname(__FILE__), "../../fixtures/utf_message.json")) }
9
+
10
+
11
+ def do_nothing(delivery, props, content)
12
+ end
13
+
14
+ def raise_method(delivery, props, content)
15
+ raise StandardError.new("Bad Subscriber")
16
+ end
17
+
18
+ describe "#process_message" do
19
+ let(:result) {
20
+ subscriber.process_message(delivery_info, properties, contents)
21
+ }
22
+
23
+ before do
24
+ subscriber.instance_variable_set(:@channel, double("channel", :ack=>true, :reject=>true))
25
+ allow(subscriber.channel).to receive(:ack)
26
+ allow(subscriber.channel).to receive(:reject)
27
+ allow(Airbrake).to receive(:notify)
28
+ end
29
+
30
+
31
+ context "subscriber has an error" do
32
+ before do
33
+ subscriber.instance_variable_set(:@callback_block, method(:raise_method) )
34
+ end
35
+
36
+
37
+ it "should not raise any errors" do
38
+ expect{ result }.not_to raise_error
39
+ end
40
+
41
+ it "should have rejected the message" do
42
+ result
43
+ expect(subscriber.channel).to have_received(:reject).with(1, false)
44
+ end
45
+
46
+ it "should have generated an airbrake" do
47
+ result
48
+ expect(Airbrake).to have_received(:notify).exactly(1).times
49
+ end
50
+ end
51
+
52
+ context "subscriber works correctly" do
53
+ before do
54
+ subscriber.instance_variable_set(:@callback_block, method(:do_nothing) )
55
+ end
56
+
57
+ it "should not raise any errors" do
58
+ expect{ result }.not_to raise_error
59
+ end
60
+
61
+ it "should have acknowledged the message" do
62
+ result
63
+ expect(subscriber.channel).to have_received(:ack).with(1)
64
+ end
65
+
66
+ it "should not have generated an airbrake" do
67
+ result
68
+ expect(Airbrake).not_to have_received(:notify)
69
+ end
70
+ end
71
+
72
+ context "logger has problems" do
73
+
74
+ before do
75
+ subscriber.instance_variable_set(:@callback_block, method(:do_nothing) )
76
+ allow(Banter::RabbitLogger).to receive(:log_receive).and_raise("Cannot log")
77
+ allow(Banter::RabbitLogger).to receive(:log_complete)
78
+ end
79
+
80
+ it "should not raise any errors" do
81
+ expect{ result }.not_to raise_error
82
+ end
83
+
84
+ it "should have rejected the message" do
85
+ result
86
+ expect(subscriber.channel).to have_received(:reject).with(1, false)
87
+ end
88
+
89
+ it "should not have logged a complete message" do
90
+ result
91
+ expect(Banter::RabbitLogger).not_to have_received(:log_complete)
92
+ end
93
+
94
+
95
+ it "should have generated an airbrake" do
96
+ result
97
+ expect(Airbrake).to have_received(:notify).exactly(1).times
98
+ end
99
+
100
+
101
+ end
102
+
103
+ end
104
+
105
+ describe "#notify_error" do
106
+ let(:error) { StandardError.new("Error Message")}
107
+ let(:consumer) { Object.new}
108
+ let(:result) { subscriber.notify_error(error, consumer)}
109
+
110
+ before do
111
+ allow(Airbrake).to receive(:notify)
112
+ end
113
+
114
+ it "should not raise any errors" do
115
+ expect{result}.not_to raise_error
116
+ end
117
+
118
+ it "should inform airbrake of the problem" do
119
+ result
120
+ expect(Airbrake).to have_received(:notify).exactly(1).times
121
+ end
122
+
123
+ end
124
+
125
+
126
+ end
@@ -0,0 +1,8 @@
1
+ {
2
+ "ts":123,
3
+ "pub":"order.state_changed.delivered",
4
+ "v":"1",
5
+ "payload": {
6
+ "jp" : "\xa1\xa1"
7
+ }
8
+ }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: banter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.3
4
+ version: 1.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Honest Company
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2014-09-23 00:00:00.000000000 Z
14
+ date: 2014-10-09 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: bundler
@@ -229,6 +229,7 @@ files:
229
229
  - spec/config.yml
230
230
  - spec/factories/banter_messages.rb
231
231
  - spec/factories/banter_workers.rb
232
+ - spec/fixtures/utf_message.json
232
233
  - spec/mongoid.yml
233
234
  - spec/spec_helper.rb
234
235
  - web/assets/javascripts/banter.js
@@ -294,5 +295,6 @@ test_files:
294
295
  - spec/config.yml
295
296
  - spec/factories/banter_messages.rb
296
297
  - spec/factories/banter_workers.rb
298
+ - spec/fixtures/utf_message.json
297
299
  - spec/mongoid.yml
298
300
  - spec/spec_helper.rb