message-driver 0.6.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +20 -2
- data/.rubocop_todo.yml +15 -23
- data/.travis.yml +10 -22
- data/CHANGELOG.md +9 -0
- data/Gemfile +34 -24
- data/Guardfile +46 -29
- data/LICENSE +1 -1
- data/Rakefile +14 -6
- data/features/CHANGELOG.md +1 -0
- data/features/step_definitions/logging_steps.rb +3 -2
- data/features/support/firewall_helper.rb +2 -2
- data/features/support/no_error_matcher.rb +1 -1
- data/lib/message_driver/adapters/base.rb +115 -11
- data/lib/message_driver/adapters/bunny_adapter.rb +58 -46
- data/lib/message_driver/adapters/in_memory_adapter.rb +57 -35
- data/lib/message_driver/adapters/stomp_adapter.rb +10 -10
- data/lib/message_driver/broker.rb +16 -19
- data/lib/message_driver/client.rb +3 -7
- data/lib/message_driver/destination.rb +4 -4
- data/lib/message_driver/message.rb +3 -2
- data/lib/message_driver/middleware/block_middleware.rb +1 -1
- data/lib/message_driver/subscription.rb +1 -1
- data/lib/message_driver/version.rb +1 -1
- data/message-driver.gemspec +6 -6
- data/spec/integration/bunny/amqp_integration_spec.rb +6 -4
- data/spec/integration/bunny/bunny_adapter_spec.rb +1 -3
- data/spec/integration/in_memory/in_memory_adapter_spec.rb +46 -6
- data/spec/integration/stomp/stomp_adapter_spec.rb +0 -2
- data/spec/spec_helper.rb +6 -0
- data/spec/support/matchers/override_method_matcher.rb +7 -0
- data/spec/support/shared/adapter_examples.rb +3 -0
- data/spec/support/shared/client_ack_examples.rb +26 -4
- data/spec/support/shared/context_examples.rb +46 -0
- data/spec/support/shared/destination_examples.rb +28 -0
- data/spec/support/shared/subscription_examples.rb +6 -1
- data/spec/support/shared/transaction_examples.rb +35 -4
- data/spec/support/test_adapter.rb +19 -0
- data/spec/support/utils.rb +1 -5
- data/spec/units/message_driver/adapters/base_spec.rb +37 -31
- data/spec/units/message_driver/broker_spec.rb +1 -2
- data/spec/units/message_driver/client_spec.rb +3 -3
- data/spec/units/message_driver/destination_spec.rb +4 -2
- data/spec/units/message_driver/message_spec.rb +9 -3
- data/test_lib/broker_config.rb +0 -2
- data/test_lib/provider/base.rb +2 -6
- data/test_lib/provider/rabbitmq.rb +3 -3
- metadata +18 -16
- data/ci/travis_setup +0 -7
- data/features/CHANGELOG.md +0 -102
@@ -129,7 +129,6 @@ module MessageDriver
|
|
129
129
|
end
|
130
130
|
|
131
131
|
describe '#create_destination' do
|
132
|
-
|
133
132
|
context 'with defaults' do
|
134
133
|
context 'the resulting destination' do
|
135
134
|
let(:dest_name) { 'my_dest' }
|
@@ -327,7 +326,6 @@ module MessageDriver
|
|
327
326
|
adapter_context.create_destination(dest_name, type: :exchange, declare: { auto_delete: true })
|
328
327
|
end.to raise_error MessageDriver::Error, /you must provide a valid exchange type/
|
329
328
|
end
|
330
|
-
|
331
329
|
end
|
332
330
|
|
333
331
|
context 'and bindings are provided' do
|
@@ -363,7 +361,7 @@ module MessageDriver
|
|
363
361
|
it 'raises in an error' do
|
364
362
|
expect do
|
365
363
|
adapter_context.create_destination('my_dest', type: :foo_bar)
|
366
|
-
end.to raise_error MessageDriver::Error,
|
364
|
+
end.to raise_error MessageDriver::Error, 'invalid destination type foo_bar'
|
367
365
|
end
|
368
366
|
end
|
369
367
|
end
|
@@ -5,8 +5,10 @@ require 'message_driver/adapters/in_memory_adapter'
|
|
5
5
|
module MessageDriver
|
6
6
|
module Adapters
|
7
7
|
RSpec.describe InMemoryAdapter, :in_memory, type: :integration do
|
8
|
-
let(:broker) {
|
9
|
-
subject(:adapter) {
|
8
|
+
let(:broker) { Broker.configure(adapter: :in_memory) }
|
9
|
+
subject(:adapter) { broker.adapter }
|
10
|
+
|
11
|
+
it { is_expected.to be_a InMemoryAdapter }
|
10
12
|
|
11
13
|
describe '#new_context' do
|
12
14
|
it 'returns a InMemoryAdapter::InMemoryContext' do
|
@@ -70,9 +72,8 @@ module MessageDriver
|
|
70
72
|
adapter.reset_after_tests
|
71
73
|
|
72
74
|
destinations.each do |destination|
|
73
|
-
expect(destination.
|
75
|
+
expect(destination.subscriptions).to be_empty
|
74
76
|
end
|
75
|
-
|
76
77
|
end
|
77
78
|
end
|
78
79
|
|
@@ -87,7 +88,7 @@ module MessageDriver
|
|
87
88
|
dest1.subscribe(&consumer)
|
88
89
|
end
|
89
90
|
it 'is the same consumer on the other destination' do
|
90
|
-
expect(dest2.
|
91
|
+
expect(dest2.subscriptions.first.consumer).to be(consumer)
|
91
92
|
end
|
92
93
|
end
|
93
94
|
|
@@ -123,7 +124,46 @@ module MessageDriver
|
|
123
124
|
describe 'subscribing a consumer' do
|
124
125
|
let(:destination) { adapter.create_destination(:my_queue) }
|
125
126
|
|
126
|
-
|
127
|
+
context 'when there are already messages on the queue' do
|
128
|
+
it 'sends those initial messages to the first subscription created' do
|
129
|
+
4.times { destination.publish('a message') }
|
130
|
+
msgs1 = []
|
131
|
+
msgs2 = []
|
132
|
+
|
133
|
+
destination.subscribe do |msg|
|
134
|
+
msgs1 << msg
|
135
|
+
end
|
136
|
+
|
137
|
+
destination.subscribe do |msg|
|
138
|
+
msgs2 << msg
|
139
|
+
end
|
140
|
+
|
141
|
+
aggregate_failures do
|
142
|
+
expect(msgs1.size).to eq(4)
|
143
|
+
expect(msgs2.size).to eq(0)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'supports multiple subscriptions on a given destination and distributes the messages between them' do
|
149
|
+
msgs1 = []
|
150
|
+
msgs2 = []
|
151
|
+
|
152
|
+
destination.subscribe do |msg|
|
153
|
+
msgs1 << msg
|
154
|
+
end
|
155
|
+
|
156
|
+
destination.subscribe do |msg|
|
157
|
+
msgs2 << msg
|
158
|
+
end
|
159
|
+
|
160
|
+
4.times { destination.publish('a message') }
|
161
|
+
|
162
|
+
aggregate_failures do
|
163
|
+
expect(msgs1.size).to eq(2)
|
164
|
+
expect(msgs2.size).to eq(2)
|
165
|
+
end
|
166
|
+
end
|
127
167
|
end
|
128
168
|
end
|
129
169
|
end
|
@@ -5,7 +5,6 @@ require 'message_driver/adapters/stomp_adapter'
|
|
5
5
|
module MessageDriver
|
6
6
|
module Adapters
|
7
7
|
RSpec.describe StompAdapter, :stomp, type: :integration do
|
8
|
-
|
9
8
|
let(:valid_connection_attrs) { BrokerConfig.config }
|
10
9
|
|
11
10
|
describe '#initialize' do
|
@@ -113,7 +112,6 @@ module MessageDriver
|
|
113
112
|
it_behaves_like 'subscriptions are not supported'
|
114
113
|
|
115
114
|
describe '#create_destination' do
|
116
|
-
|
117
115
|
context 'the resulting destination' do
|
118
116
|
let(:dest_name) { '/queue/stomp_destination_spec' }
|
119
117
|
subject(:destination) { adapter_context.create_destination(dest_name) }
|
data/spec/spec_helper.rb
CHANGED
@@ -1,18 +1,40 @@
|
|
1
1
|
RSpec.shared_examples 'client acks are supported' do
|
2
|
-
describe '#supports_client_acks' do
|
2
|
+
describe '#supports_client_acks?' do
|
3
3
|
it 'returns true' do
|
4
4
|
expect(subject.supports_client_acks?).to eq(true)
|
5
5
|
end
|
6
6
|
end
|
7
7
|
|
8
|
-
it { is_expected.to
|
9
|
-
it { is_expected.
|
8
|
+
it { is_expected.to override_method :handle_ack_message }
|
9
|
+
it { is_expected.not_to override_method :ack_message }
|
10
|
+
it { is_expected.to override_method :handle_nack_message }
|
11
|
+
it { is_expected.not_to override_method :nack_message }
|
10
12
|
end
|
11
13
|
|
12
14
|
RSpec.shared_examples 'client acks are not supported' do
|
13
|
-
|
15
|
+
it { is_expected.not_to override_method :handle_ack_message }
|
16
|
+
it { is_expected.not_to override_method :handle_nack_message }
|
17
|
+
describe '#supports_client_acks?' do
|
14
18
|
it 'returns false' do
|
15
19
|
expect(subject.supports_client_acks?).to eq(false)
|
16
20
|
end
|
17
21
|
end
|
22
|
+
|
23
|
+
describe '#ack_message' do
|
24
|
+
it 'raises an error' do
|
25
|
+
message = double('message')
|
26
|
+
expect do
|
27
|
+
subject.ack_message(message)
|
28
|
+
end.to raise_error "#ack_message is not supported by #{subject.adapter.class}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#nack_message' do
|
33
|
+
it 'raises an error' do
|
34
|
+
message = double('message')
|
35
|
+
expect do
|
36
|
+
subject.nack_message(message)
|
37
|
+
end.to raise_error "#nack_message is not supported by #{subject.adapter.class}"
|
38
|
+
end
|
39
|
+
end
|
18
40
|
end
|
@@ -5,6 +5,52 @@ RSpec.shared_examples 'an adapter context' do
|
|
5
5
|
it { expect(subject.adapter).to be adapter }
|
6
6
|
end
|
7
7
|
|
8
|
+
describe 'interface' do
|
9
|
+
it { is_expected.to respond_to(:create_destination).with(1..3).arguments }
|
10
|
+
it { is_expected.to respond_to(:handle_create_destination).with(1..3).arguments }
|
11
|
+
|
12
|
+
it { is_expected.to respond_to(:publish).with(2..4).arguments }
|
13
|
+
it { is_expected.to respond_to(:handle_publish).with(2..4).arguments }
|
14
|
+
|
15
|
+
it { is_expected.to respond_to(:pop_message).with(1..2).arguments }
|
16
|
+
it { is_expected.to respond_to(:handle_pop_message).with(1..2).arguments }
|
17
|
+
|
18
|
+
it { is_expected.to respond_to(:subscribe).with(1..2).arguments }
|
19
|
+
it { is_expected.to respond_to(:handle_subscribe).with(1..2).arguments }
|
20
|
+
|
21
|
+
it { is_expected.to respond_to(:ack_message).with(1..2).arguments }
|
22
|
+
it { is_expected.to respond_to(:handle_ack_message).with(1..2).arguments }
|
23
|
+
it { is_expected.to respond_to(:nack_message).with(1..2).arguments }
|
24
|
+
it { is_expected.to respond_to(:handle_nack_message).with(1..2).arguments }
|
25
|
+
|
26
|
+
it { is_expected.to respond_to(:begin_transaction).with(0..1).arguments }
|
27
|
+
it { is_expected.to respond_to(:handle_begin_transaction).with(0..1).arguments }
|
28
|
+
it { is_expected.to respond_to(:commit_transaction).with(0..1).arguments }
|
29
|
+
it { is_expected.to respond_to(:handle_commit_transaction).with(0..1).arguments }
|
30
|
+
it { is_expected.to respond_to(:rollback_transaction).with(0..1).arguments }
|
31
|
+
it { is_expected.to respond_to(:handle_rollback_transaction).with(0..1).arguments }
|
32
|
+
it { is_expected.to respond_to(:in_transaction?).with(0).arguments }
|
33
|
+
|
34
|
+
it { is_expected.to respond_to(:message_count).with(1).arguments }
|
35
|
+
it { is_expected.to respond_to(:handle_message_count).with(1).arguments }
|
36
|
+
it { is_expected.to respond_to(:consumer_count).with(1).arguments }
|
37
|
+
it { is_expected.to respond_to(:handle_consumer_count).with(1).arguments }
|
38
|
+
|
39
|
+
describe 'overrides' do
|
40
|
+
it { expect(subject.class).not_to override_method(:create_destination) }
|
41
|
+
it { expect(subject.class).to override_method(:handle_create_destination) }
|
42
|
+
|
43
|
+
it { expect(subject.class).not_to override_method(:publish) }
|
44
|
+
it { expect(subject.class).to override_method(:handle_publish) }
|
45
|
+
|
46
|
+
it { expect(subject.class).not_to override_method(:pop_message) }
|
47
|
+
it { expect(subject.class).to override_method(:handle_pop_message) }
|
48
|
+
|
49
|
+
it { expect(subject.class).not_to override_method(:message_count) }
|
50
|
+
it { expect(subject.class).not_to override_method(:consumer_count) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
8
54
|
it 'is initially valid' do
|
9
55
|
is_expected.to be_valid
|
10
56
|
end
|
@@ -17,6 +17,14 @@ RSpec.shared_examples 'a destination' do
|
|
17
17
|
|
18
18
|
it { is_expected.to be_a MessageDriver::Message::Base }
|
19
19
|
|
20
|
+
it 'has a reference to the context that fetched it' do
|
21
|
+
expect(message.ctx).to be_a MessageDriver::Adapters::ContextBase
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'has a reference to the destination that it was fetched from' do
|
25
|
+
expect(message.destination).to be_a MessageDriver::Destination::Base
|
26
|
+
end
|
27
|
+
|
20
28
|
describe '#body' do
|
21
29
|
it { expect(subject.body).to eq(body) }
|
22
30
|
end
|
@@ -30,6 +38,14 @@ RSpec.shared_examples 'a destination' do
|
|
30
38
|
end
|
31
39
|
end
|
32
40
|
end
|
41
|
+
|
42
|
+
context 'interface' do
|
43
|
+
it { is_expected.to respond_to(:publish).with(1..3).arguments }
|
44
|
+
it { is_expected.to respond_to(:pop_message).with(0..1).arguments }
|
45
|
+
it { is_expected.to respond_to(:message_count).with(0).arguments }
|
46
|
+
it { is_expected.to respond_to(:subscribe).with(0..1).arguments }
|
47
|
+
it { is_expected.to respond_to(:consumer_count).with(0).arguments }
|
48
|
+
end
|
33
49
|
end
|
34
50
|
|
35
51
|
RSpec.shared_examples "doesn't support #message_count" do
|
@@ -50,6 +66,12 @@ RSpec.shared_examples 'supports #message_count' do
|
|
50
66
|
pause_if_needed
|
51
67
|
end.to change { destination.message_count }.by(2)
|
52
68
|
end
|
69
|
+
|
70
|
+
it { is_expected.not_to override_method :message_count }
|
71
|
+
|
72
|
+
it 'the adapter context overrides #handle_message_count' do
|
73
|
+
expect(subject.adapter.broker.client.current_adapter_context).to override_method :handle_message_count
|
74
|
+
end
|
53
75
|
end
|
54
76
|
|
55
77
|
RSpec.shared_examples "doesn't support #consumer_count" do
|
@@ -79,4 +101,10 @@ RSpec.shared_examples 'supports #consumer_count' do
|
|
79
101
|
end.to change { destination.consumer_count }.by(-2)
|
80
102
|
end
|
81
103
|
end
|
104
|
+
|
105
|
+
it { is_expected.not_to override_method :consumer_count }
|
106
|
+
|
107
|
+
it 'the adapter context overrides #handle_consumer_count' do
|
108
|
+
expect(subject.adapter.broker.client.current_adapter_context).to override_method :handle_message_count
|
109
|
+
end
|
82
110
|
end
|
@@ -5,6 +5,8 @@ RSpec.shared_examples 'subscriptions are not supported' do
|
|
5
5
|
end
|
6
6
|
end
|
7
7
|
|
8
|
+
it { is_expected.not_to override_method(:handle_subscribe) }
|
9
|
+
|
8
10
|
describe '#subscribe' do
|
9
11
|
it 'raises an error' do
|
10
12
|
destination = double('destination')
|
@@ -23,6 +25,8 @@ RSpec.shared_examples 'subscriptions are supported' do |subscription_type|
|
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
28
|
+
it { is_expected.to override_method(:handle_subscribe) }
|
29
|
+
|
26
30
|
let(:destination) { adapter_context.create_destination('subscriptions_example_queue') }
|
27
31
|
|
28
32
|
let(:message1) { 'message 1' }
|
@@ -87,6 +91,7 @@ RSpec.shared_examples 'subscriptions are supported' do |subscription_type|
|
|
87
91
|
subscription
|
88
92
|
pause_if_needed
|
89
93
|
end.to change { messages.size }.from(0).to(2)
|
94
|
+
expect(messages.map(&:destination)).to all(be_a(MessageDriver::Destination::Base))
|
90
95
|
bodies = messages.map(&:body)
|
91
96
|
expect(bodies).to include(message1)
|
92
97
|
expect(bodies).to include(message2)
|
@@ -121,7 +126,7 @@ RSpec.shared_examples 'subscriptions are supported' do |subscription_type|
|
|
121
126
|
let(:error) { RuntimeError.new('oh nos!') }
|
122
127
|
let(:consumer) do
|
123
128
|
lambda do |_|
|
124
|
-
|
129
|
+
raise error
|
125
130
|
end
|
126
131
|
end
|
127
132
|
|
@@ -4,6 +4,34 @@ RSpec.shared_examples 'transactions are not supported' do
|
|
4
4
|
expect(subject.supports_transactions?).to eq(false)
|
5
5
|
end
|
6
6
|
end
|
7
|
+
|
8
|
+
describe '#begin_transaction' do
|
9
|
+
it 'raises an error' do
|
10
|
+
expect do
|
11
|
+
subject.begin_transaction
|
12
|
+
end.to raise_error "transactions are not supported by #{subject.adapter.class}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#commit_transaction' do
|
17
|
+
it 'raises an error' do
|
18
|
+
expect do
|
19
|
+
subject.commit_transaction
|
20
|
+
end.to raise_error "transactions are not supported by #{subject.adapter.class}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#rollback_transaction' do
|
25
|
+
it 'raises an error' do
|
26
|
+
expect do
|
27
|
+
subject.rollback_transaction
|
28
|
+
end.to raise_error "transactions are not supported by #{subject.adapter.class}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it { is_expected.not_to override_method :handle_begin_transaction }
|
33
|
+
it { is_expected.not_to override_method :handle_commit_transaction }
|
34
|
+
it { is_expected.not_to override_method :handle_rollback_transaction }
|
7
35
|
end
|
8
36
|
|
9
37
|
RSpec.shared_examples 'transactions are supported' do
|
@@ -13,10 +41,13 @@ RSpec.shared_examples 'transactions are supported' do
|
|
13
41
|
end
|
14
42
|
end
|
15
43
|
|
16
|
-
it { is_expected.to
|
17
|
-
it { is_expected.
|
18
|
-
it { is_expected.to
|
19
|
-
it { is_expected.
|
44
|
+
it { is_expected.to override_method :handle_begin_transaction }
|
45
|
+
it { is_expected.not_to override_method :begin_transaction }
|
46
|
+
it { is_expected.to override_method :handle_commit_transaction }
|
47
|
+
it { is_expected.not_to override_method :commit_transaction }
|
48
|
+
it { is_expected.to override_method :handle_rollback_transaction }
|
49
|
+
it { is_expected.not_to override_method :rollback_transaction }
|
50
|
+
it { is_expected.to override_method :in_transaction? }
|
20
51
|
|
21
52
|
describe '#in_transaction?' do
|
22
53
|
it "returns false if you aren't in a transaction" do
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module MessageDriver
|
2
|
+
class TestAdapter < Adapters::Base
|
3
|
+
def initialize(broker, _config)
|
4
|
+
@broker = broker
|
5
|
+
end
|
6
|
+
|
7
|
+
def build_context
|
8
|
+
TestContext.new(self)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class TestContext < Adapters::ContextBase
|
13
|
+
def handle_create_destination(_name, _dest_options = nil, _message_props = nil); end
|
14
|
+
|
15
|
+
def handle_publish(_destination, _body, _dest_options = nil, _message_props = nil); end
|
16
|
+
|
17
|
+
def handle_pop_message(_destination, _options = nil); end
|
18
|
+
end
|
19
|
+
end
|
data/spec/support/utils.rb
CHANGED
@@ -3,11 +3,12 @@ require 'spec_helper'
|
|
3
3
|
module MessageDriver
|
4
4
|
module Adapters
|
5
5
|
RSpec.describe Base do
|
6
|
-
|
7
|
-
|
6
|
+
let(:spec_adapter_class) do
|
7
|
+
Class.new(described_class) do
|
8
|
+
def initialize; end
|
8
9
|
end
|
9
10
|
end
|
10
|
-
subject(:adapter) {
|
11
|
+
subject(:adapter) { spec_adapter_class.new }
|
11
12
|
|
12
13
|
describe '#new_context' do
|
13
14
|
it 'raises an error' do
|
@@ -17,40 +18,45 @@ module MessageDriver
|
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
it 'raises an error' do
|
32
|
-
expect do
|
33
|
-
subject.create_destination('foo')
|
34
|
-
end.to raise_error 'Must be implemented in subclass'
|
21
|
+
context 'with a test adapter' do
|
22
|
+
subject(:adapter) { TestAdapter.new(nil, {}) }
|
23
|
+
|
24
|
+
describe ContextBase do
|
25
|
+
context 'with a test context subclass' do
|
26
|
+
subject(:adapter_context) { TestContext.new(adapter) }
|
27
|
+
|
28
|
+
it_behaves_like 'an adapter context'
|
29
|
+
it_behaves_like 'transactions are not supported'
|
30
|
+
it_behaves_like 'client acks are not supported'
|
31
|
+
it_behaves_like 'subscriptions are not supported'
|
35
32
|
end
|
36
|
-
end
|
37
33
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
34
|
+
subject(:adapter_context) { ContextBase.new(adapter) }
|
35
|
+
|
36
|
+
describe '#create_destination' do
|
37
|
+
it 'raises an error' do
|
38
|
+
expect do
|
39
|
+
subject.create_destination('foo')
|
40
|
+
end.to raise_error 'Must be implemented in subclass'
|
41
|
+
end
|
43
42
|
end
|
44
|
-
end
|
45
43
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
44
|
+
describe '#publish' do
|
45
|
+
it 'raises an error' do
|
46
|
+
expect do
|
47
|
+
subject.publish(:destination, foo: 'bar')
|
48
|
+
end.to raise_error 'Must be implemented in subclass'
|
49
|
+
end
|
51
50
|
end
|
52
|
-
end
|
53
51
|
|
52
|
+
describe '#pop_message' do
|
53
|
+
it 'raises an error' do
|
54
|
+
expect do
|
55
|
+
subject.pop_message(:destination)
|
56
|
+
end.to raise_error 'Must be implemented in subclass'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
54
60
|
end
|
55
61
|
end
|
56
62
|
end
|