state_machines-rspec 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ require 'matchers/states/matcher'
2
+
3
+ module StateMachinesRspec
4
+ module Matchers
5
+ def reject_states(state, *states)
6
+ RejectStateMatcher.new(states.unshift(state))
7
+ end
8
+ alias_method :reject_state, :reject_states
9
+
10
+ class RejectStateMatcher < StateMachinesRspec::Matchers::States::Matcher
11
+ def matches_states?(states)
12
+ no_defined_states?
13
+ end
14
+
15
+ def description
16
+ message = super
17
+ message << " on #{state_machine_scope.inspect}" if state_machine_scope
18
+ "not have #{message}"
19
+ end
20
+
21
+ private
22
+
23
+ def no_defined_states?
24
+ defined_states = @introspector.defined_states(@states)
25
+ unless defined_states.empty?
26
+ @failure_message = "Did not expect #{@introspector.state_machine_attribute} " +
27
+ "to allow states: #{defined_states.join(', ')}"
28
+ end
29
+
30
+ defined_states.empty?
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,83 @@
1
+ require 'active_support/core_ext/array/extract_options'
2
+
3
+ module StateMachinesRspec
4
+ module Matchers
5
+ def transition_from(*values)
6
+ HandleTransitionFromMatcher.new(*values)
7
+ end
8
+
9
+ alias transitions_from transition_from
10
+
11
+ class HandleTransitionFromMatcher
12
+ attr_reader :failure_message, :options, :from_states, :from_state,
13
+ :state_machine_scope
14
+
15
+ def initialize(*values)
16
+ @options = values.extract_options!
17
+ @state_machine_scope = @options.fetch(:on, nil)
18
+ @from_states = values
19
+ end
20
+
21
+ def matches?(subject)
22
+ @subject = subject
23
+ @introspector = StateMachinesIntrospector.new(@subject, state_machine_scope)
24
+ from_states.each do |from_state|
25
+ @from_state = from_state
26
+ enter_from_state
27
+ return false unless valid_transition?
28
+ break if @failure_message
29
+ end
30
+ @failure_message.nil?
31
+ end
32
+
33
+ def description
34
+ message = "transition state to :#{options[:to_state]} from "
35
+ message << from_states.map{ |state| ":#{state}" }.join(', ')
36
+ message << " on event :#{options[:on_event]}"
37
+ message << " on #{state_machine_scope.inspect}" if state_machine_scope
38
+ message
39
+ end
40
+
41
+ def valid_transition?
42
+ valid_transition = @introspector.valid_transition?(event, to_state)
43
+ unless valid_transition
44
+ @failure_message = 'Expected to be able to transition state from: ' \
45
+ "#{from_state} to: #{to_state}, on_event: #{event}"
46
+ end
47
+
48
+ valid_transition
49
+ end
50
+
51
+ def event
52
+ @event ||=
53
+ unless event = options[:on_event]
54
+ raise StateMachinesIntrospectorError, 'Option :on_event cannot be nil'
55
+ end
56
+ unless @introspector.event_defined?(event)
57
+ raise StateMachinesIntrospectorError, "#{@subject.class} does not define event :#{event}"
58
+ end
59
+ event
60
+ end
61
+
62
+ def to_state
63
+ @to_state ||=
64
+ unless state_name = options[:to_state]
65
+ raise StateMachinesIntrospectorError, 'Option :to_state cannot be nil'
66
+ end
67
+ unless state = @introspector.state(state_name)
68
+ raise StateMachinesIntrospectorError, "#{@subject.class} does not define state: #{state_name}"
69
+ end
70
+ state.value
71
+ end
72
+
73
+ private
74
+
75
+ def enter_from_state
76
+ unless state = @introspector.state(from_state)
77
+ raise StateMachinesIntrospectorError, "#{@subject.class} does not define state: #{from_state}"
78
+ end
79
+ @subject.send("#{@introspector.state_machine_attribute}=", state.value)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,12 @@
1
+ require 'state_machines_rspec/version'
2
+ require 'state_machines_rspec/state_machines_introspector'
3
+ require 'matchers/events/handle_event'
4
+ require 'matchers/events/reject_event'
5
+ require 'matchers/states/have_state'
6
+ require 'matchers/states/reject_state'
7
+ require 'matchers/transitions/transition_from'
8
+
9
+ module StateMachinesRspec
10
+ module Matchers
11
+ end
12
+ end
@@ -0,0 +1,74 @@
1
+ class StateMachinesIntrospector
2
+ def initialize(subject, state_machine_name=nil)
3
+ @subject = subject
4
+ @state_machine_name = state_machine_name
5
+ end
6
+
7
+ def state_machine_attribute
8
+ state_machine.attribute
9
+ end
10
+
11
+ def current_state_value
12
+ @subject.send(state_machine_attribute)
13
+ end
14
+
15
+ def state(name)
16
+ state_machine.states.find { |s| s.name == name }
17
+ end
18
+
19
+ def undefined_states(states)
20
+ states.reject { |s| state_defined? s }
21
+ end
22
+
23
+ def defined_states(states)
24
+ states.select { |s| state_defined? s }
25
+ end
26
+
27
+ def undefined_events(events)
28
+ events.reject { |e| event_defined? e }
29
+ end
30
+
31
+ def valid_events(events)
32
+ events.select { |e| valid_event? e }
33
+ end
34
+
35
+ def invalid_events(events)
36
+ events.reject { |e| valid_event? e }
37
+ end
38
+
39
+ def event_defined?(event)
40
+ @subject.respond_to? "can_#{event}?"
41
+ end
42
+
43
+ def valid_transition?(event, to_state)
44
+ @subject.send(event)
45
+ current_state_value == to_state
46
+ end
47
+
48
+ private
49
+
50
+ def state_machine
51
+ if @state_machine_name
52
+ unless machine = @subject.class.state_machines[@state_machine_name]
53
+ raise StateMachinesIntrospectorError,
54
+ "#{@subject.class} does not have a state machine defined " \
55
+ "on #{@state_machine_name}"
56
+ end
57
+ else
58
+ machine = @subject.class.state_machine
59
+ end
60
+
61
+ machine
62
+ end
63
+
64
+ def state_defined?(state_name)
65
+ state(state_name)
66
+ end
67
+
68
+ def valid_event?(event)
69
+ @subject.send("can_#{event}?")
70
+ end
71
+ end
72
+
73
+ class StateMachinesIntrospectorError < StandardError
74
+ end
@@ -0,0 +1,3 @@
1
+ module StateMachinesRspec
2
+ VERSION = '0.4.0'
3
+ end
@@ -0,0 +1,332 @@
1
+ require 'spec_helper'
2
+ require 'integration/models/vehicle'
3
+
4
+ describe Vehicle do
5
+ let(:vehicle) { Vehicle.new }
6
+ subject { vehicle }
7
+
8
+ its(:passed_inspection?) { is_expected.to be_falsey }
9
+
10
+ shared_examples 'crashable' do
11
+ describe 'crash' do
12
+ context 'having passed inspection' do
13
+ before { allow(vehicle).to receive_messages(:passed_inspection => true) }
14
+ pending 'keeps running' do
15
+ initial_state = vehicle.state
16
+ vehicle.crash!
17
+
18
+ expect(vehicle.state).to eq initial_state
19
+ end
20
+ end
21
+
22
+ context 'not having passed inspection' do
23
+ before { allow(vehicle).to receive_messages(:passed_inspection => false) }
24
+ it 'stalls' do
25
+ vehicle.crash!
26
+ expect(vehicle.state).to eq :stalled.to_s
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ shared_examples 'speedless' do
33
+ it 'does not respond to speed' do
34
+ expect { vehicle.speed }.to raise_error StateMachines::InvalidContext
35
+ end
36
+ end
37
+
38
+ describe '#initialize' do
39
+ its(:seatbelt_on) { is_expected.to be_falsey }
40
+ its(:time_used) { is_expected.to eq 0 }
41
+ its(:auto_shop_busy) { is_expected.to be_truthy }
42
+ end
43
+
44
+ describe '#put_on_seatbelt' do
45
+ it 'sets seatbelt_on to true' do
46
+ vehicle.seatbelt_on = false
47
+ vehicle.put_on_seatbelt
48
+
49
+ expect(vehicle.seatbelt_on).to be_truthy
50
+ end
51
+ end
52
+
53
+ describe 'state machines' do
54
+ it { is_expected.to have_states :parked, :idling, :stalled, :first_gear,
55
+ :second_gear, :third_gear }
56
+ it { is_expected.to reject_state :flying }
57
+
58
+ it { is_expected.to handle_event :ignite, when: :parked }
59
+ it { is_expected.to reject_events :park, :idle, :shift_up,
60
+ :shift_down, :crash, :repair,
61
+ when: :parked }
62
+
63
+ it { is_expected.to handle_events :park, :shift_up, :crash, when: :idling }
64
+ it { is_expected.to reject_events :ignite, :idle, :shift_down, :repair,
65
+ when: :idling }
66
+
67
+ it { is_expected.to handle_events :ignite, :repair, when: :stalled }
68
+ it { is_expected.to reject_events :park, :idle, :shift_up, :shift_down, :crash,
69
+ when: :stalled }
70
+
71
+ it { is_expected.to handle_events :park, :idle, :shift_up, :crash,
72
+ when: :first_gear }
73
+ it { is_expected.to reject_events :ignite, :shift_down, :repair,
74
+ when: :first_gear }
75
+
76
+ it { is_expected.to handle_events :shift_up, :shift_down, :crash,
77
+ when: :second_gear }
78
+ it { is_expected.to reject_events :park, :ignite, :idle, :repair,
79
+ when: :second_gear }
80
+
81
+ it { is_expected.to handle_events :shift_down, :crash, when: :third_gear }
82
+ it { is_expected.to reject_events :park, :ignite, :idle, :shift_up, :repair,
83
+ when: :third_gear }
84
+
85
+ it { is_expected.to transition_from :idling, :first_gear, to_state: :parked, on_event: :park }
86
+ it { is_expected.to transition_from :idling, to_state: :parked, on_event: :park }
87
+
88
+ it 'has an initial state of "parked"' do
89
+ expect(vehicle).to be_parked
90
+ end
91
+
92
+ it 'has an initial alarm state of "active"' do
93
+ expect(vehicle.alarm_active?).to be_truthy
94
+ end
95
+
96
+ describe 'around transitions' do
97
+ it 'updates the time used' do
98
+ expect(vehicle).to receive(:time_used=).with(0)
99
+ Timecop.freeze { vehicle.ignite! }
100
+ end
101
+ end
102
+
103
+ context 'when parked' do
104
+ before { vehicle.state = :parked.to_s }
105
+
106
+ its(:speed) { is_expected.to be_zero }
107
+ it { is_expected.not_to be_moving }
108
+
109
+ describe 'before transitions' do
110
+ it 'puts on a seatbelt' do
111
+ expect(vehicle).to receive :put_on_seatbelt
112
+ vehicle.ignite!
113
+ end
114
+ end
115
+
116
+ describe 'ignite' do
117
+ it 'should transition to idling' do
118
+ vehicle.ignite!
119
+ expect(vehicle).to be_idling
120
+ end
121
+ end
122
+ end
123
+
124
+ context 'when transitioning to parked' do
125
+ before { vehicle.state = :idling.to_s }
126
+ it 'removes seatbelts' do
127
+ expect(vehicle).to receive(:seatbelt_on=).with(false)
128
+ vehicle.park!
129
+ end
130
+ end
131
+
132
+ context 'when idling' do
133
+ before { vehicle.state = :idling.to_s }
134
+
135
+ its(:speed) { is_expected.to eq 10 }
136
+ it { is_expected.not_to be_moving }
137
+
138
+ describe 'park' do
139
+ it 'should transition to a parked state' do
140
+ vehicle.park!
141
+ expect(vehicle).to be_parked
142
+ end
143
+ end
144
+
145
+ describe 'shift up' do
146
+ it 'should shift into first gear' do
147
+ vehicle.shift_up!
148
+ expect(vehicle).to be_first_gear
149
+ end
150
+ end
151
+
152
+ it_behaves_like 'crashable'
153
+ end
154
+
155
+ context 'when stalled' do
156
+ before { vehicle.state = :stalled.to_s }
157
+
158
+ it { is_expected.not_to be_moving }
159
+ it_behaves_like 'speedless'
160
+
161
+ describe 'ignite' do
162
+ it 'remains stalled' do
163
+ vehicle.ignite!
164
+ expect(vehicle).to be_stalled
165
+ end
166
+ end
167
+
168
+ describe 'repair' do
169
+ context 'the auto shop is busy' do
170
+ before { allow(vehicle).to receive_messages(:auto_shop_busy => true) }
171
+ it 'remains stalled' do
172
+ vehicle.repair!
173
+ expect(vehicle).to be_stalled
174
+ end
175
+ end
176
+
177
+ context 'the auto shop is not busy' do
178
+ before { allow(vehicle).to receive_messages(:auto_shop_busy => false) }
179
+ it 'is parked' do
180
+ vehicle.repair!
181
+ expect(vehicle).to be_parked
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ context 'when in first gear' do
188
+ before { vehicle.state = :first_gear.to_s }
189
+
190
+ its(:speed) { is_expected.to eq 10 }
191
+ it { is_expected.to be_moving }
192
+
193
+ describe 'park' do
194
+ it 'parks' do
195
+ vehicle.park!
196
+ expect(vehicle).to be_parked
197
+ end
198
+ end
199
+
200
+ describe 'idle' do
201
+ it 'idles' do
202
+ vehicle.idle!
203
+ expect(vehicle).to be_idling
204
+ end
205
+ end
206
+
207
+ describe 'shift up' do
208
+ it 'shift into second gear' do
209
+ vehicle.shift_up!
210
+ expect(vehicle).to be_second_gear
211
+ end
212
+ end
213
+
214
+ it_behaves_like 'crashable'
215
+ end
216
+
217
+ context 'when in second gear' do
218
+ before { vehicle.state = :second_gear.to_s }
219
+
220
+ it { is_expected.to be_moving }
221
+ it_behaves_like 'speedless'
222
+
223
+ describe 'shift up' do
224
+ it 'shifts into third gear' do
225
+ vehicle.shift_up!
226
+ expect(vehicle).to be_third_gear
227
+ end
228
+ end
229
+
230
+ describe 'shift down' do
231
+ it 'shifts back into first gear' do
232
+ vehicle.shift_down!
233
+ expect(vehicle).to be_first_gear
234
+ end
235
+ end
236
+
237
+ it_behaves_like 'crashable'
238
+ end
239
+
240
+ context 'when in third gear' do
241
+ before { vehicle.state = :third_gear.to_s }
242
+
243
+ it { is_expected.to be_moving }
244
+ it_behaves_like 'speedless'
245
+
246
+ describe 'shift down' do
247
+ it 'shifts back into second gear' do
248
+ vehicle.shift_down!
249
+ expect(vehicle).to be_second_gear
250
+ end
251
+ end
252
+
253
+ it_behaves_like 'crashable'
254
+ end
255
+
256
+ context 'on ignition' do
257
+ context 'when it fails' do
258
+ before { allow(vehicle).to receive_messages(:ignite => false) }
259
+ pending 'logs the failure' do
260
+ expect(vehicle).to receive(:log_start_failure)
261
+ vehicle.ignite
262
+ end
263
+ end
264
+ end
265
+
266
+ context 'on a crash' do
267
+ before { vehicle.state = :third_gear.to_s }
268
+ it 'gets towed' do
269
+ expect(vehicle).to receive(:tow)
270
+ vehicle.crash!
271
+ end
272
+ end
273
+
274
+ context 'upon being repaired' do
275
+ before { vehicle.state = :stalled.to_s }
276
+ it 'gets fixed' do
277
+ expect(vehicle).to receive(:fix)
278
+ vehicle.repair!
279
+ end
280
+ end
281
+ end
282
+
283
+ describe 'alarm state machines' do
284
+ it { is_expected.to have_state :active, on: :alarm_state, value: 1 }
285
+ it { is_expected.to have_state :off, on: :alarm_state, value: 0 }
286
+ it { is_expected.to reject_states :broken, :ringing, on: :alarm_state }
287
+
288
+ it { is_expected.to handle_events :enable_alarm, :disable_alarm,
289
+ when: :active, on: :alarm_state }
290
+ it { is_expected.to handle_events :enable_alarm, :disable_alarm,
291
+ when: :off, on: :alarm_state }
292
+
293
+ it { is_expected.to transition_from :active, to_state: :off, on_event: :disable_alarm, on: :alarm_state }
294
+
295
+ it 'has an initial state of activated' do
296
+ expect(vehicle.alarm_active?).to be_truthy
297
+ end
298
+
299
+ context 'when active' do
300
+ describe 'enable' do
301
+ it 'becomes active' do
302
+ vehicle.enable_alarm!
303
+ expect(vehicle.alarm_active?).to be_truthy
304
+ end
305
+ end
306
+
307
+ describe 'disable' do
308
+ it 'turns the alarm off' do
309
+ vehicle.disable_alarm!
310
+ expect(vehicle.alarm_off?).to be_truthy
311
+ end
312
+ end
313
+ end
314
+
315
+ context 'when off' do
316
+ before { vehicle.alarm_state = 0 }
317
+ describe 'enable' do
318
+ it 'becomes active' do
319
+ vehicle.enable_alarm!
320
+ expect(vehicle.alarm_active?).to be_truthy
321
+ end
322
+ end
323
+
324
+ describe 'disable' do
325
+ it 'turns the alarm off' do
326
+ vehicle.disable_alarm!
327
+ expect(vehicle.alarm_off?).to be_truthy
328
+ end
329
+ end
330
+ end
331
+ end
332
+ end