state_machines_activerecord_rspec 0.1.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,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 machine' 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 machine' 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
+
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+
3
+ describe StateMachinesActiverecordRspec::Matchers::HandleEventMatcher do
4
+ describe '#matches?' do
5
+ context 'when :when state is specified' do
6
+ context 'but the state doesn\'t exist' do
7
+ before do
8
+ matcher_class = Class.new do
9
+ state_machine :state, initial: :mathy
10
+ end
11
+ @matcher_subject = matcher_class.new
12
+ @matcher = described_class.new([when: :artsy])
13
+ end
14
+
15
+ it 'raises' do
16
+ expect { @matcher.matches? @matcher_subject }.
17
+ to raise_error StateMachineIntrospectorError
18
+ end
19
+ end
20
+
21
+ context 'and that state exists' do
22
+ before do
23
+ matcher_class = Class.new do
24
+ state_machine :state, initial: :mathy do
25
+ state :artsy
26
+ end
27
+ end
28
+ @matcher_subject = matcher_class.new
29
+ @matcher = described_class.new([when: :artsy])
30
+ end
31
+
32
+ it 'sets the state' do
33
+ @matcher.matches? @matcher_subject
34
+ expect(@matcher_subject.state).to eq 'artsy'
35
+ end
36
+ end
37
+ end
38
+
39
+ context 'when subject can perform events' do
40
+ before do
41
+ matcher_class = Class.new do
42
+ state_machine :mathiness, initial: :mathy do
43
+ event(:mathematize) { transition any => same }
44
+ end
45
+ end
46
+ @matcher_subject = matcher_class.new
47
+ @matcher = described_class.new([:mathematize, on: :mathiness])
48
+ end
49
+
50
+ it 'does not set a failure message' do
51
+ @matcher.matches? @matcher_subject
52
+ expect(@matcher.failure_message).to be_nil
53
+ end
54
+ it 'returns true' do
55
+ expect(@matcher.matches?(@matcher_subject)).to be_truthy
56
+ end
57
+ end
58
+
59
+ context 'when subject cannot perform events' do
60
+ before do
61
+ matcher_class = Class.new do
62
+ state_machine :state, initial: :mathy do
63
+ state :polynomial
64
+
65
+ event(:mathematize) { transition any => same }
66
+ event(:algebraify) { transition :polynomial => same }
67
+ event(:trigonomalize) { transition :trigonomalize => same }
68
+ end
69
+ end
70
+ @matcher_subject = matcher_class.new
71
+ end
72
+
73
+ context 'because it cannot perform the transition' do
74
+ before do
75
+ @matcher = described_class.new([:mathematize, :algebraify, :trigonomalize])
76
+ end
77
+
78
+ it 'sets a failure message' do
79
+ @matcher.matches? @matcher_subject
80
+ expect(@matcher.failure_message).to eq('Expected to be able to handle events: algebraify, trigonomalize ' +
81
+ 'in state: mathy')
82
+ end
83
+ it 'returns false' do
84
+ expect(@matcher.matches?(@matcher_subject)).to be_falsey
85
+ end
86
+ end
87
+
88
+ context 'because no such events exist' do
89
+ before do
90
+ @matcher = described_class.new([:polynomialize, :eulerasterize])
91
+ end
92
+
93
+ it 'does not raise' do
94
+ expect { @matcher.matches?(@matcher_subject) }.not_to raise_error
95
+ end
96
+ it 'sets a failure message' do
97
+ @matcher.matches? @matcher_subject
98
+ expect(@matcher.failure_message).to eq('state_machine: state does not ' +
99
+ 'define events: polynomialize, eulerasterize')
100
+ end
101
+ it 'returns false' do
102
+ expect(@matcher.matches?(@matcher_subject)).to be_falsey
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ describe '#description' do
109
+ context 'with no options' do
110
+ let(:matcher) { described_class.new([:placate, :mollify]) }
111
+
112
+ it 'returns a string description' do
113
+ expect(matcher.description).to eq('handle :placate, :mollify')
114
+ end
115
+ end
116
+
117
+ context 'when :when state is specified' do
118
+ let(:matcher) { described_class.new([:destroy_food, when: :hangry]) }
119
+
120
+ it 'mentions the requisite state' do
121
+ expect(matcher.description).to eq('handle :destroy_food when :hangry')
122
+ end
123
+ end
124
+
125
+ context 'when :on is specified' do
126
+ let(:matcher) { described_class.new([:ensmarmify, on: :tired_investors]) }
127
+
128
+ it 'mentions the state machine variable' do
129
+ expect(matcher.description).to eq('handle :ensmarmify on :tired_investors')
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+
3
+ describe StateMachinesActiverecordRspec::Matchers::RejectEventMatcher do
4
+ describe '#matches?' do
5
+ context 'when :when state is specified' do
6
+ context 'but that state doesn\'t exist' do
7
+ before do
8
+ matcher_class = Class.new do
9
+ state_machine :state, initial: :sleazy
10
+ end
11
+ @matcher_subject = matcher_class.new
12
+ @matcher = described_class.new([when: :sneezy])
13
+ end
14
+
15
+ it 'raises' do
16
+ expect { @matcher.matches? @matcher_subject }.
17
+ to raise_error StateMachineIntrospectorError
18
+ end
19
+ end
20
+
21
+ context 'and that state exists' do
22
+ before do
23
+ matcher_class = Class.new do
24
+ state_machine :state, initial: :sleazy do
25
+ state :sneezy
26
+ end
27
+ end
28
+ @matcher_subject = matcher_class.new
29
+ @matcher = described_class.new([when: :sneezy])
30
+ end
31
+
32
+ it 'sets the state' do
33
+ @matcher.matches? @matcher_subject
34
+ expect(@matcher_subject.state).to eq('sneezy')
35
+ end
36
+ end
37
+ end
38
+
39
+ context 'when an expectation is made on an event that is undefined' do
40
+ before do
41
+ matcher_class = Class.new do
42
+ state_machine :state, initial: :snarky do
43
+ event(:primmadonnalize) { transition any => same }
44
+ end
45
+ end
46
+ @matcher_subject = matcher_class.new
47
+ @matcher = described_class.new([:primmadonnalize, :martinilunchitize])
48
+ end
49
+
50
+ it 'does not raise' do
51
+ expect { @matcher.matches?(@matcher_subject) }.not_to raise_error
52
+ end
53
+ it 'sets a failure message' do
54
+ @matcher.matches? @matcher_subject
55
+ expect(@matcher.failure_message).to eq('state_machine: state does not ' +
56
+ 'define events: martinilunchitize')
57
+ end
58
+ it 'returns false' do
59
+ expect(@matcher.matches?(@matcher_subject)).to be_falsey
60
+ end
61
+ end
62
+
63
+ context 'when subject cannot perform any of the specified events' do
64
+ before do
65
+ matcher_class = Class.new do
66
+ state_machine :state, initial: :snarky do
67
+ state :haughty
68
+ event(:primmadonnalize) { transition :haughty => same }
69
+ end
70
+ end
71
+ @matcher_subject = matcher_class.new
72
+ @matcher = described_class.new([:primmadonnalize])
73
+ end
74
+
75
+ it 'does not set a failure message' do
76
+ @matcher.matches? @matcher_subject
77
+ expect(@matcher.failure_message).to be_nil
78
+ end
79
+ it 'returns true' do
80
+ expect(@matcher.matches?(@matcher_subject)).to be_truthy
81
+ end
82
+ end
83
+
84
+ context 'when subject can perform any one of the specified events' do
85
+ before do
86
+ matcher_class = Class.new do
87
+ state_machine :state, initial: :snarky do
88
+ state :haughty
89
+ event(:primmadonnalize) { transition :haughty => same }
90
+ event(:defer_to_management) { transition any => same }
91
+ end
92
+ end
93
+ @matcher_subject = matcher_class.new
94
+ @matcher = described_class.new([:primmadonnalize, :defer_to_management])
95
+ end
96
+
97
+ it 'sets a failure message' do
98
+ @matcher.matches? @matcher_subject
99
+ expect(@matcher.failure_message).to eq('Did not expect to be able to handle events: defer_to_management ' +
100
+ 'in state: snarky')
101
+ end
102
+ it 'returns false' do
103
+ expect(@matcher.matches?(@matcher_subject)).to be_falsey
104
+ end
105
+ end
106
+ end
107
+
108
+ describe '#description' do
109
+ context 'with no options' do
110
+ let(:matcher) { described_class.new([:makeadealify, :hustlinate]) }
111
+
112
+ it 'returns a string description' do
113
+ expect(matcher.description).to eq('reject :makeadealify, :hustlinate')
114
+ end
115
+ end
116
+
117
+ context 'when :when state is specified' do
118
+ let(:matcher) { described_class.new([:begargle, when: :sleep_encrusted]) }
119
+
120
+ it 'mentions the requisite state' do
121
+ expect(matcher.description).to eq('reject :begargle when :sleep_encrusted')
122
+ end
123
+ end
124
+
125
+ context 'when :on is specified' do
126
+ let(:matcher) { described_class.new([:harrangue, on: :suspicious_crowd]) }
127
+
128
+ it 'mentions the state machine variable' do
129
+ expect(matcher.description).to eq('reject :harrangue on :suspicious_crowd')
130
+ end
131
+ end
132
+ end
133
+ end