state_machines_rspec 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+