state_machines-rspec 0.4.0

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.
@@ -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