hutch 0.25.0-java → 2.0.0.rc1-java

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +402 -5
  3. data/LICENSE +1 -0
  4. data/README.md +157 -47
  5. data/bin/ci/before_build.sh +20 -0
  6. data/bin/ci/before_build_docker.sh +15 -0
  7. data/bin/ci/install_on_debian.sh +46 -0
  8. data/lib/hutch/acknowledgements/nack_on_all_failures.rb +1 -1
  9. data/lib/hutch/adapters/bunny.rb +29 -1
  10. data/lib/hutch/adapters/march_hare.rb +24 -0
  11. data/lib/hutch/broker.rb +98 -35
  12. data/lib/hutch/cli.rb +16 -12
  13. data/lib/hutch/config.rb +42 -13
  14. data/lib/hutch/consumer.rb +63 -4
  15. data/lib/hutch/error_handlers/bugsnag.rb +30 -0
  16. data/lib/hutch/error_handlers/{opbeat.rb → rollbar.rb} +11 -13
  17. data/lib/hutch/error_handlers/sentry.rb +6 -11
  18. data/lib/hutch/error_handlers.rb +2 -1
  19. data/lib/hutch/publisher.rb +1 -1
  20. data/lib/hutch/serializers/json.rb +3 -3
  21. data/lib/hutch/tracers/datadog.rb +18 -0
  22. data/lib/hutch/tracers.rb +1 -1
  23. data/lib/hutch/version.rb +1 -2
  24. data/lib/hutch/worker.rb +46 -9
  25. data/lib/hutch.rb +8 -4
  26. data/lib/yard-settings/yard-settings.rb +2 -2
  27. data/spec/hutch/broker_spec.rb +192 -14
  28. data/spec/hutch/cli_spec.rb +12 -9
  29. data/spec/hutch/config_spec.rb +84 -12
  30. data/spec/hutch/consumer_spec.rb +93 -4
  31. data/spec/hutch/error_handlers/airbrake_spec.rb +4 -1
  32. data/spec/hutch/error_handlers/bugsnag_spec.rb +56 -0
  33. data/spec/hutch/error_handlers/honeybadger_spec.rb +1 -1
  34. data/spec/hutch/error_handlers/logger_spec.rb +1 -1
  35. data/spec/hutch/error_handlers/rollbar_spec.rb +45 -0
  36. data/spec/hutch/error_handlers/sentry_spec.rb +13 -3
  37. data/spec/hutch/message_spec.rb +1 -1
  38. data/spec/hutch/tracers/datadog_spec.rb +46 -0
  39. data/spec/hutch/waiter_spec.rb +2 -2
  40. data/spec/hutch/worker_spec.rb +138 -24
  41. data/spec/integration/channel_recovery_spec.rb +187 -0
  42. data/spec/integration/publish_consume_spec.rb +47 -0
  43. metadata +32 -59
  44. data/.gitignore +0 -10
  45. data/.rspec +0 -1
  46. data/.travis.yml +0 -16
  47. data/.yardopts +0 -5
  48. data/Gemfile +0 -33
  49. data/Guardfile +0 -14
  50. data/Rakefile +0 -21
  51. data/examples/consumer.rb +0 -13
  52. data/examples/producer.rb +0 -10
  53. data/hutch.gemspec +0 -29
  54. data/lib/hutch/tracers/opbeat.rb +0 -37
  55. data/spec/hutch/error_handlers/opbeat_spec.rb +0 -37
  56. data/spec/spec_helper.rb +0 -42
  57. data/spec/tracers/opbeat_spec.rb +0 -44
@@ -0,0 +1,18 @@
1
+ require 'datadog'
2
+ require 'datadog/auto_instrument'
3
+
4
+ module Hutch
5
+ module Tracers
6
+ class Datadog
7
+ def initialize(klass)
8
+ @klass = klass
9
+ end
10
+
11
+ def handle(message)
12
+ ::Datadog::Tracing.trace(@klass.class.name, continue_from: nil, service: 'hutch', type: 'rabbitmq') do
13
+ @klass.process(message)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/hutch/tracers.rb CHANGED
@@ -2,6 +2,6 @@ module Hutch
2
2
  module Tracers
3
3
  autoload :NullTracer, 'hutch/tracers/null_tracer'
4
4
  autoload :NewRelic, 'hutch/tracers/newrelic'
5
- autoload :Opbeat, 'hutch/tracers/opbeat'
5
+ autoload :Datadog, 'hutch/tracers/datadog'
6
6
  end
7
7
  end
data/lib/hutch/version.rb CHANGED
@@ -1,4 +1,3 @@
1
1
  module Hutch
2
- VERSION = '0.25.0'.freeze
2
+ VERSION = '2.0.0.rc1'.freeze
3
3
  end
4
-
data/lib/hutch/worker.rb CHANGED
@@ -11,8 +11,9 @@ module Hutch
11
11
  include Logging
12
12
 
13
13
  def initialize(broker, consumers, setup_procs)
14
- @broker = broker
15
- self.consumers = consumers
14
+ @broker = broker
15
+ @recovery_lock = Mutex.new
16
+ self.consumers = consumers
16
17
  self.setup_procs = setup_procs
17
18
  end
18
19
 
@@ -45,17 +46,53 @@ module Hutch
45
46
  # Bind a consumer's routing keys to its queue, and set up a subscription to
46
47
  # receive messages sent to the queue.
47
48
  def setup_queue(consumer)
48
- logger.info "setting up queue: #{consumer.get_queue_name}"
49
+ queue_name = consumer.get_queue_name
50
+ queue_name = @broker.namespaced_queue_name(queue_name) unless consumer.without_namespace?
51
+ logger.info "setting up queue: #{queue_name}"
49
52
 
50
- queue = @broker.queue(consumer.get_queue_name, consumer.get_arguments)
53
+ queue = @broker.queue(queue_name, consumer.get_options)
51
54
  @broker.bind_queue(queue, consumer.routing_keys)
52
55
 
53
- queue.subscribe(consumer_tag: unique_consumer_tag, manual_ack: true) do |*args|
56
+ on_cancellation = proc { handle_cancellation(queue_name, queue.channel) }
57
+ queue.subscribe(consumer_tag: unique_consumer_tag, manual_ack: true,
58
+ on_cancellation: on_cancellation) do |*args|
54
59
  delivery_info, properties, payload = Hutch::Adapter.decode_message(*args)
55
60
  handle_message(consumer, delivery_info, properties, payload)
56
61
  end
57
62
  end
58
63
 
64
+ # A server-sent `basic.cancel` carries no reason, so a consumer timeout
65
+ # is indistinguishable from a queue deletion. Only the former is recoverable.
66
+ def handle_cancellation(queue_name, cancelled_channel)
67
+ unless @broker.queue_exists?(queue_name)
68
+ logger.error "consumer on queue #{queue_name} was cancelled by the server: the queue no longer exists"
69
+ return
70
+ end
71
+
72
+ logger.warn "consumer on queue #{queue_name} was cancelled by the server, re-subscribing"
73
+ resubscribe_on_a_new_channel(cancelled_channel)
74
+ rescue => ex
75
+ logger.error "consumer re-subscription failed: #{ex.class}: #{ex.message}"
76
+ end
77
+
78
+ # Runs on its own thread: closing the channel kills the consumer work
79
+ # pool this callback runs on. Returns the thread so that tests can join it.
80
+ def resubscribe_on_a_new_channel(cancelled_channel)
81
+ Thread.new do
82
+ begin
83
+ @recovery_lock.synchronize do
84
+ # All consumers share the channel, so only the first cancellation replaces it.
85
+ next unless @broker.channel.equal?(cancelled_channel)
86
+
87
+ @broker.replace_channel!
88
+ setup_queues
89
+ end
90
+ rescue => ex
91
+ logger.error "consumer re-subscription failed: #{ex.class}: #{ex.message}"
92
+ end
93
+ end
94
+ end
95
+
59
96
  # Called internally when a new messages comes in from RabbitMQ. Responsible
60
97
  # for wrapping up the message and passing it to the consumer.
61
98
  def handle_message(consumer, delivery_info, properties, payload)
@@ -71,10 +108,10 @@ module Hutch
71
108
  message = Message.new(delivery_info, properties, payload, serializer)
72
109
  consumer_instance = consumer.new.tap { |c| c.broker, c.delivery_info = @broker, delivery_info }
73
110
  with_tracing(consumer_instance).handle(message)
74
- @broker.ack(delivery_info.delivery_tag)
111
+ @broker.ack(delivery_info.delivery_tag, channel: delivery_info.channel) unless consumer_instance.message_rejected?
75
112
  rescue => ex
76
113
  acknowledge_error(delivery_info, properties, @broker, ex)
77
- handle_error(properties, payload, consumer, ex)
114
+ handle_error(properties, payload, consumer, ex, delivery_info)
78
115
  end
79
116
 
80
117
  def with_tracing(klass)
@@ -83,7 +120,7 @@ module Hutch
83
120
 
84
121
  def handle_error(*args)
85
122
  Hutch::Config[:error_handlers].each do |backend|
86
- backend.handle(*args)
123
+ backend.handle *args.first(backend.method(:handle).arity)
87
124
  end
88
125
  end
89
126
 
@@ -118,7 +155,7 @@ module Hutch
118
155
  def group_restricted?(consumer)
119
156
  consumers_to_load = consumer_groups[group]
120
157
  if consumers_to_load
121
- !allowed_consumers.include?(consumer.name)
158
+ !consumers_to_load.include?(consumer.name)
122
159
  else
123
160
  true
124
161
  end
data/lib/hutch.rb CHANGED
@@ -14,6 +14,8 @@ require 'hutch/exceptions'
14
14
  require 'hutch/tracers'
15
15
 
16
16
  module Hutch
17
+ @@connection_mutex = Mutex.new
18
+
17
19
  def self.register_consumer(consumer)
18
20
  self.consumers << consumer
19
21
  end
@@ -40,10 +42,12 @@ module Hutch
40
42
  # @param config [Hash] Configuration
41
43
  # @option options [Boolean] :enable_http_api_use
42
44
  def self.connect(options = {}, config = Hutch::Config)
43
- return if connected?
44
-
45
- @broker = Hutch::Broker.new(config)
46
- @broker.connect(options)
45
+ @@connection_mutex.synchronize do
46
+ unless connected?
47
+ @broker = Hutch::Broker.new(config)
48
+ @broker.connect(options)
49
+ end
50
+ end
47
51
  end
48
52
 
49
53
  def self.disconnect
@@ -1,2 +1,2 @@
1
- YARD::Templates::Engine.register_template_path(File.dirname(__FILE__) + '/../../templates')
2
- require File.join(File.dirname(__FILE__), 'handler') if RUBY19
1
+ YARD::Templates::Engine.register_template_path(File.join(__dir__, '../../templates'))
2
+ require_relative 'handler'
@@ -95,7 +95,7 @@ describe Hutch::Broker do
95
95
 
96
96
  context 'when configured with a URI' do
97
97
  context 'which specifies the port' do
98
- before { config[:uri] = 'amqp://guest:guest@127.0.0.1:5672/' }
98
+ before { config[:uri] = "amqp://#{config[:mq_username]}:#{config[:mq_password]}@#{config[:mq_host]}:#{config[:mq_port]}/" }
99
99
 
100
100
  it 'successfully connects' do
101
101
  c = broker.open_connection
@@ -105,7 +105,7 @@ describe Hutch::Broker do
105
105
  end
106
106
 
107
107
  context 'which does not specify port and uses the amqp scheme' do
108
- before { config[:uri] = 'amqp://guest:guest@127.0.0.1/' }
108
+ before { config[:uri] = "amqp://#{config[:mq_username]}:#{config[:mq_password]}@#{config[:mq_host]}/" }
109
109
 
110
110
  it 'successfully connects' do
111
111
  c = broker.open_connection
@@ -115,11 +115,11 @@ describe Hutch::Broker do
115
115
  end
116
116
 
117
117
  context 'which specifies the amqps scheme' do
118
- before { config[:uri] = 'amqps://guest:guest@127.0.0.1/' }
118
+ before { config[:uri] = "amqps://#{config[:mq_username]}:#{config[:mq_password]}@#{config[:mq_host]}/" }
119
119
 
120
120
  it 'utilises TLS' do
121
121
  expect(Hutch::Adapter).to receive(:new).with(
122
- hash_including(tls: true, port: 5671)
122
+ hash_including(tls: true)
123
123
  ).and_return(instance_double('Hutch::Adapter', start: nil))
124
124
 
125
125
  broker.open_connection
@@ -128,6 +128,76 @@ describe Hutch::Broker do
128
128
  end
129
129
  end
130
130
 
131
+ describe '#parse_uri' do
132
+ {
133
+ "amqp://host/vhost" => "vhost",
134
+ "amqp://host/v%2fhost" => "v/host",
135
+ "amqp://host/%2f" => "/",
136
+ "amqp://host/" => Hutch::Adapter::DEFAULT_VHOST,
137
+ "amqp://host" => Hutch::Adapter::DEFAULT_VHOST,
138
+ }.each do |uri, vhost|
139
+ it "parses the vhost of #{uri} as #{vhost.inspect}" do
140
+ config[:uri] = uri
141
+ broker.send(:parse_uri)
142
+
143
+ expect(config[:mq_vhost]).to eq(vhost)
144
+ end
145
+ end
146
+
147
+ it 'passes the vhost to the connection unchanged' do
148
+ config[:uri] = "amqp://host/%2f"
149
+
150
+ expect(broker.send(:connection_params)[:vhost]).to eq("/")
151
+ end
152
+
153
+ it 'percent-decodes the username and password' do
154
+ config[:uri] = "amqp://al%23pha:be%20ta@host:10000/vhost"
155
+ broker.send(:parse_uri)
156
+
157
+ expect(config[:mq_username]).to eq("al#pha")
158
+ expect(config[:mq_password]).to eq("be ta")
159
+ end
160
+
161
+ it 'leaves absent credentials nil' do
162
+ config[:uri] = "amqp://host:10000/vhost"
163
+ broker.send(:parse_uri)
164
+
165
+ expect(config[:mq_username]).to be_nil
166
+ expect(config[:mq_password]).to be_nil
167
+ end
168
+ end
169
+
170
+ describe '#bindings', 'vhost filtering' do
171
+ let(:api_bindings) do
172
+ [{ 'source' => 'hutch', 'vhost' => '/', 'destination' => 'q', 'routing_key' => 'key' }]
173
+ end
174
+
175
+ before do
176
+ config[:mq_exchange] = 'hutch'
177
+ broker.api_client = double('api_client', bindings: api_bindings)
178
+ end
179
+
180
+ it 'filters on the default vhost when the configured vhost is blank' do
181
+ config[:mq_vhost] = ''
182
+
183
+ expect(broker.bindings).to eq('q' => ['key'])
184
+ end
185
+ end
186
+
187
+ describe '#sanitized_uri' do
188
+ it 'percent-encodes the decoded username and vhost again' do
189
+ config[:uri] = 'amqp://al%23pha:be%20ta@host:10000/v%2Fhost'
190
+
191
+ expect(broker.send(:sanitized_uri)).to eq('amqp://al%23pha@host:10000/v%2Fhost')
192
+ end
193
+
194
+ it 'renders the default vhost as a bare trailing slash' do
195
+ config[:uri] = 'amqp://alpha:beta@host:10000/'
196
+
197
+ expect(broker.send(:sanitized_uri)).to eq('amqp://alpha@host:10000/')
198
+ end
199
+ end
200
+
131
201
  describe '#open_connection!' do
132
202
  it 'sets the #connection to #open_connection' do
133
203
  connection = double('connection').as_null_object
@@ -199,6 +269,100 @@ describe Hutch::Broker do
199
269
  end
200
270
  end
201
271
 
272
+ describe '#replace_channel!', adapter: :bunny do
273
+ let(:old_channel) { double('Bunny::Channel', open?: true, close: nil) }
274
+ let(:new_channel) { double('Bunny::Channel').as_null_object }
275
+ let(:work_pool) { double('Bunny::ConsumerWorkPool', shutdown: nil, join: nil, kill: nil) }
276
+
277
+ before do
278
+ broker.channel = old_channel
279
+ allow(broker).to receive(:channel_work_pool).and_return(work_pool)
280
+ allow(broker).to receive(:open_channel).and_return(new_channel)
281
+ allow(broker).to receive(:declare_exchange!)
282
+ allow(broker).to receive(:declare_publisher!)
283
+ end
284
+
285
+ it 'drains the work pool before closing the old channel' do
286
+ expect(work_pool).to receive(:shutdown).ordered
287
+ expect(work_pool).to receive(:join).with(config[:graceful_exit_timeout]).ordered
288
+ expect(work_pool).to receive(:kill).ordered
289
+ expect(old_channel).to receive(:close).ordered
290
+
291
+ broker.replace_channel!
292
+ end
293
+
294
+ it 'redeclares the exchange and the publisher on the new channel' do
295
+ expect(broker).to receive(:declare_exchange!)
296
+ expect(broker).to receive(:declare_publisher!)
297
+
298
+ broker.replace_channel!
299
+
300
+ expect(broker.channel).to eq(new_channel)
301
+ end
302
+
303
+ context 'when the old channel is already closed' do
304
+ let(:old_channel) { double('Bunny::Channel', open?: false) }
305
+
306
+ it 'opens a new channel anyway' do
307
+ expect(old_channel).not_to receive(:close)
308
+ expect(work_pool).not_to receive(:shutdown)
309
+
310
+ broker.replace_channel!
311
+
312
+ expect(broker.channel).to eq(new_channel)
313
+ end
314
+ end
315
+
316
+ context 'when the server closes the old channel concurrently' do
317
+ it 'opens a new channel anyway' do
318
+ allow(old_channel).to receive(:close).
319
+ and_raise(Hutch::Adapter::ChannelAlreadyClosed.new('already closed', old_channel))
320
+
321
+ broker.replace_channel!
322
+
323
+ expect(broker.channel).to eq(new_channel)
324
+ end
325
+ end
326
+ end
327
+
328
+ describe 'acknowledgements' do
329
+ let(:current_channel) { double('Channel').as_null_object }
330
+ let(:superseded_channel) { double('Channel') }
331
+
332
+ before { broker.channel = current_channel }
333
+
334
+ it 'sends them on the channel the delivery arrived on' do
335
+ expect(current_channel).to receive(:ack).with('dt', false)
336
+ broker.ack('dt', channel: current_channel)
337
+
338
+ expect(current_channel).to receive(:nack).with('dt', false, false)
339
+ broker.nack('dt', channel: current_channel)
340
+
341
+ expect(current_channel).to receive(:reject).with('dt', false)
342
+ broker.reject('dt', channel: current_channel)
343
+
344
+ expect(current_channel).to receive(:reject).with('dt', true)
345
+ broker.requeue('dt', channel: current_channel)
346
+ end
347
+
348
+ it 'drops them when the channel has been replaced' do
349
+ expect(superseded_channel).not_to receive(:ack)
350
+ expect(superseded_channel).not_to receive(:nack)
351
+ expect(superseded_channel).not_to receive(:reject)
352
+
353
+ broker.ack('dt', channel: superseded_channel)
354
+ broker.nack('dt', channel: superseded_channel)
355
+ broker.reject('dt', channel: superseded_channel)
356
+ broker.requeue('dt', channel: superseded_channel)
357
+ end
358
+
359
+ it 'defaults to the current channel' do
360
+ expect(current_channel).to receive(:ack).with('dt', false)
361
+
362
+ broker.ack('dt')
363
+ end
364
+ end
365
+
202
366
  describe '#declare_exchange' do
203
367
  before do
204
368
  broker.open_connection!
@@ -245,9 +409,9 @@ describe Hutch::Broker do
245
409
  context 'when given invalid details' do
246
410
  before { config[:mq_api_host] = 'notarealhost' }
247
411
  after { broker.disconnect }
248
- let(:set_up_api_connection) { ->{ broker.set_up_api_connection } }
412
+ let(:set_up_api_connection) { broker.set_up_api_connection }
249
413
 
250
- specify { expect(set_up_api_connection).to raise_error(StandardError) }
414
+ specify { expect { broker.set_up_api_connection }.to raise_error(StandardError) }
251
415
  end
252
416
  end
253
417
 
@@ -256,13 +420,27 @@ describe Hutch::Broker do
256
420
  let(:arguments) { { foo: :bar } }
257
421
  before { allow(broker).to receive(:channel) { channel } }
258
422
 
259
- it 'applies a global namespace' do
260
- config[:namespace] = 'mirror-all.service'
261
- expect(broker.channel).to receive(:queue) do |*args|
262
- args.first == ''
263
- args.last == arguments
423
+ it 'creates a queue with the given name' do
424
+ expect(broker.channel).to receive(:queue).with('test', arguments: arguments)
425
+ broker.queue('test'.freeze, arguments: arguments)
426
+ end
427
+ end
428
+
429
+ describe '#namespaced_queue_name' do
430
+ context 'with a namespace configured' do
431
+ before { config[:namespace] = 'mirror-all.service' }
432
+
433
+ it 'prepends the namespace' do
434
+ expect(broker.namespaced_queue_name('test')).to eq('mirror-all.service:test')
435
+ end
436
+ end
437
+
438
+ context 'without a namespace configured' do
439
+ before { config[:namespace] = nil }
440
+
441
+ it 'returns the name unchanged' do
442
+ expect(broker.namespaced_queue_name('test')).to eq('test')
264
443
  end
265
- broker.queue('test', arguments)
266
444
  end
267
445
  end
268
446
 
@@ -279,7 +457,7 @@ describe Hutch::Broker do
279
457
 
280
458
  context 'with a binding' do
281
459
  around do |example|
282
- queue = broker.queue('test').bind(broker.exchange, routing_key: 'key')
460
+ queue = broker.queue('test', durable: true).bind(broker.exchange, routing_key: 'key')
283
461
  example.run
284
462
  queue.unbind(broker.exchange, routing_key: 'key').delete
285
463
  end
@@ -309,7 +487,7 @@ describe Hutch::Broker do
309
487
  end
310
488
 
311
489
  context '(rabbitmq integration test)', rabbitmq: true do
312
- let(:queue) { broker.queue('consumer') }
490
+ let(:queue) { broker.queue('consumer', durable: true) }
313
491
  let(:routing_key) { 'key' }
314
492
 
315
493
  before { allow(broker).to receive(:bindings).and_call_original }
@@ -31,9 +31,10 @@ describe Hutch::CLI do
31
31
  end
32
32
 
33
33
  context "when the config file exists" do
34
- let(:file) do
35
- Tempfile.new("hutch-test-config.yaml").to_path
36
- end
34
+ # Kept in a let, so the Tempfile is not garbage collected - and with it
35
+ # unlinked - before the CLI checks that the path exists.
36
+ let(:config_file) { Tempfile.new("hutch-test-config.yaml") }
37
+ let(:file) { config_file.to_path }
37
38
 
38
39
  it "parses the config" do
39
40
  expect(Hutch::Config).to receive(:load_from_file)
@@ -55,9 +56,10 @@ describe Hutch::CLI do
55
56
  end
56
57
 
57
58
  context "when the keyfile file exists" do
58
- let(:file) do
59
- Tempfile.new("hutch-test-key.pem").to_path
60
- end
59
+ # Kept in a let, so the Tempfile is not garbage collected - and with it
60
+ # unlinked - before the CLI checks that the path exists.
61
+ let(:key_file) { Tempfile.new("hutch-test-key.pem") }
62
+ let(:file) { key_file.to_path }
61
63
 
62
64
  it "sets mq_tls_key to the file" do
63
65
  expect(Hutch::Config).to receive(:mq_tls_key=)
@@ -79,9 +81,10 @@ describe Hutch::CLI do
79
81
  end
80
82
 
81
83
  context "when the certfile file exists" do
82
- let(:file) do
83
- Tempfile.new("hutch-test-cert.pem").to_path
84
- end
84
+ # Kept in a let, so the Tempfile is not garbage collected - and with it
85
+ # unlinked - before the CLI checks that the path exists.
86
+ let(:cert_file) { Tempfile.new("hutch-test-cert.pem") }
87
+ let(:file) { cert_file.to_path }
85
88
 
86
89
  it "sets mq_tls_cert to the file" do
87
90
  expect(Hutch::Config).to receive(:mq_tls_cert=)
@@ -31,12 +31,10 @@ describe Hutch::Config do
31
31
  end
32
32
 
33
33
  context 'for invalid attributes' do
34
- let(:invalid_get) do
35
- -> { Hutch::Config.get(:invalid_attr) }
36
- end
34
+ let(:invalid_get) { Hutch::Config.get(:invalid_attr) }
37
35
 
38
36
  specify do
39
- expect(invalid_get).to raise_error Hutch::UnknownAttributeError
37
+ expect { invalid_get }.to raise_error Hutch::UnknownAttributeError
40
38
  end
41
39
  end
42
40
  end
@@ -49,15 +47,89 @@ describe Hutch::Config do
49
47
  context 'sets value in user config hash' do
50
48
  it { is_expected.to eq(new_value) }
51
49
  end
50
+
51
+ context 'type casting' do
52
+ context 'number attributes' do
53
+ before { Hutch::Config.set(:heartbeat, new_value) }
54
+ subject(:value) { Hutch::Config.user_config[:heartbeat] }
55
+
56
+ let(:new_value) { "0" }
57
+
58
+
59
+ specify 'casts values to integers' do
60
+ expect(value).to eq 0
61
+ end
62
+ end
63
+ end
64
+
65
+ context 'boolean attributes' do
66
+ context 'from non-empty string' do
67
+ before { Hutch::Config.set(:autoload_rails, new_value) }
68
+ subject(:value) { Hutch::Config.user_config[:autoload_rails] }
69
+
70
+ let(:new_value) { "t" }
71
+
72
+
73
+ specify 'casts values to booleans' do
74
+ expect(value).to eq true
75
+ end
76
+ end
77
+
78
+ context 'from empty string' do
79
+ before { Hutch::Config.set(:autoload_rails, new_value) }
80
+ subject(:value) { Hutch::Config.user_config[:autoload_rails] }
81
+
82
+ let(:new_value) { "" }
83
+
84
+
85
+ specify 'casts values to booleans' do
86
+ expect(value).to eq false
87
+ end
88
+ end
89
+
90
+ context 'from boolean' do
91
+ before { Hutch::Config.set(:autoload_rails, new_value) }
92
+ subject(:value) { Hutch::Config.user_config[:autoload_rails] }
93
+
94
+ let(:new_value) { true }
95
+
96
+
97
+ specify 'casts values to booleans' do
98
+ expect(value).to eq true
99
+ end
100
+ end
101
+
102
+ context 'from nil' do
103
+ before { Hutch::Config.set(:autoload_rails, new_value) }
104
+ subject(:value) { Hutch::Config.user_config[:autoload_rails] }
105
+
106
+ let(:new_value) { nil }
107
+
108
+
109
+ specify 'casts values to booleans' do
110
+ expect(value).to eq false
111
+ end
112
+ end
113
+ end
114
+
115
+ context 'string attributes' do
116
+ before { Hutch::Config.set(:mq_exchange_type, new_value) }
117
+ subject(:value) { Hutch::Config.user_config[:mq_exchange_type] }
118
+
119
+ let(:new_value) { 1 }
120
+
121
+
122
+ specify 'does not perform any typecasting' do
123
+ expect(value).to eq new_value
124
+ end
125
+ end
52
126
  end
53
127
 
54
128
  context 'for invalid attributes' do
55
- let(:invalid_set) do
56
- -> { Hutch::Config.set(:invalid_attr, new_value) }
57
- end
129
+ let(:invalid_set) { Hutch::Config.set(:invalid_attr, new_value) }
58
130
 
59
131
  specify do
60
- expect(invalid_set).to raise_error Hutch::UnknownAttributeError
132
+ expect { invalid_set }.to raise_error Hutch::UnknownAttributeError
61
133
  end
62
134
  end
63
135
  end
@@ -71,8 +143,8 @@ describe Hutch::Config do
71
143
  end
72
144
 
73
145
  context 'for an invalid attribute' do
74
- let(:invalid_getter) { -> { Hutch::Config.invalid_attr } }
75
- specify { expect(invalid_getter).to raise_error NoMethodError }
146
+ let(:invalid_getter) { Hutch::Config.invalid_attr }
147
+ specify { expect { invalid_getter }.to raise_error NoMethodError }
76
148
  end
77
149
 
78
150
  context 'for an ENV-overriden value attribute' do
@@ -109,8 +181,8 @@ describe Hutch::Config do
109
181
  end
110
182
 
111
183
  context 'for an invalid attribute' do
112
- let(:invalid_setter) { -> { Hutch::Config.invalid_attr = new_value } }
113
- specify { expect(invalid_setter).to raise_error NoMethodError }
184
+ let(:invalid_setter) { Hutch::Config.invalid_attr = new_value }
185
+ specify { expect { invalid_setter }.to raise_error NoMethodError }
114
186
  end
115
187
  end
116
188