state_machines_rspec 0.3.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,11 @@
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
+
8
+ module StateMachinesRspec
9
+ module Matchers
10
+ end
11
+ end
@@ -0,0 +1,70 @@
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 = 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
+ private
40
+
41
+ def state_machine
42
+ if @state_machine_name
43
+ unless machine = @subject.class.state_machines[@state_machine_name]
44
+ raise StateMachinesIntrospectorError,
45
+ "#{@subject.class} does not have a state machine defined " +
46
+ "on #{@state_machine_name}"
47
+ end
48
+ else
49
+ machine = @subject.class.state_machine
50
+ end
51
+
52
+ machine
53
+ end
54
+
55
+ def state_defined?(state_name)
56
+ state(state_name)
57
+ end
58
+
59
+ def event_defined?(event)
60
+ @subject.respond_to? "can_#{event}?"
61
+ end
62
+
63
+ def valid_event?(event)
64
+ @subject.send("can_#{event}?")
65
+ end
66
+
67
+ end
68
+
69
+ class StateMachinesIntrospectorError < StandardError
70
+ end
@@ -0,0 +1,3 @@
1
+ module StateMachinesRspec
2
+ VERSION = '0.3.0'
3
+ end
@@ -0,0 +1,327 @@
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 'has an initial state of "parked"' do
86
+ expect(vehicle).to be_parked
87
+ end
88
+
89
+ it 'has an initial alarm state of "active"' do
90
+ expect(vehicle.alarm_active?).to be_truthy
91
+ end
92
+
93
+ describe 'around transitions' do
94
+ it 'updates the time used' do
95
+ expect(vehicle).to receive(:time_used=).with(0)
96
+ Timecop.freeze { vehicle.ignite! }
97
+ end
98
+ end
99
+
100
+ context 'when parked' do
101
+ before { vehicle.state = :parked.to_s }
102
+
103
+ its(:speed) { is_expected.to be_zero }
104
+ it { is_expected.not_to be_moving }
105
+
106
+ describe 'before transitions' do
107
+ it 'puts on a seatbelt' do
108
+ expect(vehicle).to receive :put_on_seatbelt
109
+ vehicle.ignite!
110
+ end
111
+ end
112
+
113
+ describe 'ignite' do
114
+ it 'should transition to idling' do
115
+ vehicle.ignite!
116
+ expect(vehicle).to be_idling
117
+ end
118
+ end
119
+ end
120
+
121
+ context 'when transitioning to parked' do
122
+ before { vehicle.state = :idling.to_s }
123
+ it 'removes seatbelts' do
124
+ expect(vehicle).to receive(:seatbelt_on=).with(false)
125
+ vehicle.park!
126
+ end
127
+ end
128
+
129
+ context 'when idling' do
130
+ before { vehicle.state = :idling.to_s }
131
+
132
+ its(:speed) { is_expected.to eq 10 }
133
+ it { is_expected.not_to be_moving }
134
+
135
+ describe 'park' do
136
+ it 'should transition to a parked state' do
137
+ vehicle.park!
138
+ expect(vehicle).to be_parked
139
+ end
140
+ end
141
+
142
+ describe 'shift up' do
143
+ it 'should shift into first gear' do
144
+ vehicle.shift_up!
145
+ expect(vehicle).to be_first_gear
146
+ end
147
+ end
148
+
149
+ it_behaves_like 'crashable'
150
+ end
151
+
152
+ context 'when stalled' do
153
+ before { vehicle.state = :stalled.to_s }
154
+
155
+ it { is_expected.not_to be_moving }
156
+ it_behaves_like 'speedless'
157
+
158
+ describe 'ignite' do
159
+ it 'remains stalled' do
160
+ vehicle.ignite!
161
+ expect(vehicle).to be_stalled
162
+ end
163
+ end
164
+
165
+ describe 'repair' do
166
+ context 'the auto shop is busy' do
167
+ before { allow(vehicle).to receive_messages(:auto_shop_busy => true) }
168
+ it 'remains stalled' do
169
+ vehicle.repair!
170
+ expect(vehicle).to be_stalled
171
+ end
172
+ end
173
+
174
+ context 'the auto shop is not busy' do
175
+ before { allow(vehicle).to receive_messages(:auto_shop_busy => false) }
176
+ it 'is parked' do
177
+ vehicle.repair!
178
+ expect(vehicle).to be_parked
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ context 'when in first gear' do
185
+ before { vehicle.state = :first_gear.to_s }
186
+
187
+ its(:speed) { is_expected.to eq 10 }
188
+ it { is_expected.to be_moving }
189
+
190
+ describe 'park' do
191
+ it 'parks' do
192
+ vehicle.park!
193
+ expect(vehicle).to be_parked
194
+ end
195
+ end
196
+
197
+ describe 'idle' do
198
+ it 'idles' do
199
+ vehicle.idle!
200
+ expect(vehicle).to be_idling
201
+ end
202
+ end
203
+
204
+ describe 'shift up' do
205
+ it 'shift into second gear' do
206
+ vehicle.shift_up!
207
+ expect(vehicle).to be_second_gear
208
+ end
209
+ end
210
+
211
+ it_behaves_like 'crashable'
212
+ end
213
+
214
+ context 'when in second gear' do
215
+ before { vehicle.state = :second_gear.to_s }
216
+
217
+ it { is_expected.to be_moving }
218
+ it_behaves_like 'speedless'
219
+
220
+ describe 'shift up' do
221
+ it 'shifts into third gear' do
222
+ vehicle.shift_up!
223
+ expect(vehicle).to be_third_gear
224
+ end
225
+ end
226
+
227
+ describe 'shift down' do
228
+ it 'shifts back into first gear' do
229
+ vehicle.shift_down!
230
+ expect(vehicle).to be_first_gear
231
+ end
232
+ end
233
+
234
+ it_behaves_like 'crashable'
235
+ end
236
+
237
+ context 'when in third gear' do
238
+ before { vehicle.state = :third_gear.to_s }
239
+
240
+ it { is_expected.to be_moving }
241
+ it_behaves_like 'speedless'
242
+
243
+ describe 'shift down' do
244
+ it 'shifts back into second gear' do
245
+ vehicle.shift_down!
246
+ expect(vehicle).to be_second_gear
247
+ end
248
+ end
249
+
250
+ it_behaves_like 'crashable'
251
+ end
252
+
253
+ context 'on ignition' do
254
+ context 'when it fails' do
255
+ before { allow(vehicle).to receive_messages(:ignite => false) }
256
+ pending 'logs the failure' do
257
+ expect(vehicle).to receive(:log_start_failure)
258
+ vehicle.ignite
259
+ end
260
+ end
261
+ end
262
+
263
+ context 'on a crash' do
264
+ before { vehicle.state = :third_gear.to_s }
265
+ it 'gets towed' do
266
+ expect(vehicle).to receive(:tow)
267
+ vehicle.crash!
268
+ end
269
+ end
270
+
271
+ context 'upon being repaired' do
272
+ before { vehicle.state = :stalled.to_s }
273
+ it 'gets fixed' do
274
+ expect(vehicle).to receive(:fix)
275
+ vehicle.repair!
276
+ end
277
+ end
278
+ end
279
+
280
+ describe 'alarm state machines' do
281
+ it { is_expected.to have_state :active, on: :alarm_state, value: 1 }
282
+ it { is_expected.to have_state :off, on: :alarm_state, value: 0 }
283
+ it { is_expected.to reject_states :broken, :ringing, on: :alarm_state }
284
+
285
+ it { is_expected.to handle_events :enable_alarm, :disable_alarm,
286
+ when: :active, on: :alarm_state }
287
+ it { is_expected.to handle_events :enable_alarm, :disable_alarm,
288
+ when: :off, on: :alarm_state }
289
+
290
+ it 'has an initial state of activated' do
291
+ expect(vehicle.alarm_active?).to be_truthy
292
+ end
293
+
294
+ context 'when active' do
295
+ describe 'enable' do
296
+ it 'becomes active' do
297
+ vehicle.enable_alarm!
298
+ expect(vehicle.alarm_active?).to be_truthy
299
+ end
300
+ end
301
+
302
+ describe 'disable' do
303
+ it 'turns the alarm off' do
304
+ vehicle.disable_alarm!
305
+ expect(vehicle.alarm_off?).to be_truthy
306
+ end
307
+ end
308
+ end
309
+
310
+ context 'when off' do
311
+ before { vehicle.alarm_state = 0 }
312
+ describe 'enable' do
313
+ it 'becomes active' do
314
+ vehicle.enable_alarm!
315
+ expect(vehicle.alarm_active?).to be_truthy
316
+ end
317
+ end
318
+
319
+ describe 'disable' do
320
+ it 'turns the alarm off' do
321
+ vehicle.disable_alarm!
322
+ expect(vehicle.alarm_off?).to be_truthy
323
+ end
324
+ end
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,119 @@
1
+ require 'spec_helper'
2
+
3
+ class Vehicle
4
+ attr_accessor :seatbelt_on, :time_used, :auto_shop_busy
5
+
6
+ state_machine :state, :initial => :parked do
7
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
8
+
9
+ after_transition :on => :crash, :do => :tow
10
+ after_transition :on => :repair, :do => :fix
11
+ after_transition any => :parked do |vehicle, transition|
12
+ vehicle.seatbelt_on = false
13
+ end
14
+
15
+ after_failure :on => :ignite, :do => :log_start_failure
16
+
17
+ around_transition do |vehicle, transition, block|
18
+ start = Time.now
19
+ block.call
20
+ vehicle.time_used += Time.now - start
21
+ end
22
+
23
+ event :park do
24
+ transition [:idling, :first_gear] => :parked
25
+ end
26
+
27
+ event :ignite do
28
+ transition :stalled => same, :parked => :idling
29
+ end
30
+
31
+ event :idle do
32
+ transition :first_gear => :idling
33
+ end
34
+
35
+ event :shift_up do
36
+ transition :idling => :first_gear, :first_gear => :second_gear, :second_gear => :third_gear
37
+ end
38
+
39
+ event :shift_down do
40
+ transition :third_gear => :second_gear, :second_gear => :first_gear
41
+ end
42
+
43
+ event :crash do
44
+ transition all - [:parked, :stalled] => :stalled, :if => lambda {|vehicle| !vehicle.passed_inspection?}
45
+ end
46
+
47
+ event :repair do
48
+ # The first transition that matches the state and passes its conditions
49
+ # will be used
50
+ transition :stalled => :parked, :unless => :auto_shop_busy
51
+ transition :stalled => same
52
+ end
53
+
54
+ state :parked do
55
+ def speed
56
+ 0
57
+ end
58
+ end
59
+
60
+ state :idling, :first_gear do
61
+ def speed
62
+ 10
63
+ end
64
+ end
65
+
66
+ state all - [:parked, :stalled, :idling] do
67
+ def moving?
68
+ true
69
+ end
70
+ end
71
+
72
+ state :parked, :stalled, :idling do
73
+ def moving?
74
+ false
75
+ end
76
+ end
77
+ end
78
+
79
+ state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
80
+ event :enable do
81
+ transition all => :active
82
+ end
83
+
84
+ event :disable do
85
+ transition all => :off
86
+ end
87
+
88
+ state :active, :value => 1
89
+ state :off, :value => 0
90
+ end
91
+
92
+ def initialize
93
+ @seatbelt_on = false
94
+ @time_used = 0
95
+ @auto_shop_busy = true
96
+ super() # NOTE: This *must* be called, otherwise states won't get initialized
97
+ end
98
+
99
+ def put_on_seatbelt
100
+ @seatbelt_on = true
101
+ end
102
+
103
+ def passed_inspection?
104
+ false
105
+ end
106
+
107
+ def tow
108
+ # tow the vehicle
109
+ end
110
+
111
+ def fix
112
+ # get the vehicle fixed by a mechanic
113
+ end
114
+
115
+ def log_start_failure
116
+ # log a failed attempt to start the vehicle
117
+ end
118
+ end
119
+