statesmin 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rubocop.yml +48 -0
- data/.travis.yml +15 -0
- data/CONTRIBUTING.md +28 -0
- data/Gemfile +3 -0
- data/Guardfile +14 -0
- data/LICENSE.txt +22 -0
- data/README.md +556 -0
- data/Rakefile +6 -0
- data/lib/statesmin.rb +8 -0
- data/lib/statesmin/callback.rb +52 -0
- data/lib/statesmin/exceptions.rb +21 -0
- data/lib/statesmin/guard.rb +13 -0
- data/lib/statesmin/machine.rb +276 -0
- data/lib/statesmin/railtie.rb +5 -0
- data/lib/statesmin/transition_helper.rb +41 -0
- data/lib/statesmin/version.rb +3 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/statesmin/callback_spec.rb +120 -0
- data/spec/statesmin/guard_spec.rb +22 -0
- data/spec/statesmin/machine_spec.rb +704 -0
- data/spec/statesmin/transition_helper_spec.rb +170 -0
- data/statesmin.gemspec +26 -0
- metadata +142 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Statesmin::Callback do
|
4
|
+
let(:cb_lambda) { -> {} }
|
5
|
+
let(:callback) do
|
6
|
+
Statesmin::Callback.new(from: nil, to: nil, callback: cb_lambda)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "#initialize" do
|
10
|
+
context "with no callback" do
|
11
|
+
let(:cb_lambda) { nil }
|
12
|
+
|
13
|
+
it "raises an error" do
|
14
|
+
expect { callback }.to raise_error(Statesmin::InvalidCallbackError)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#call" do
|
20
|
+
let(:spy) { double.as_null_object }
|
21
|
+
let(:cb_lambda) { -> { spy.call } }
|
22
|
+
|
23
|
+
it "delegates to callback" do
|
24
|
+
callback.call
|
25
|
+
expect(spy).to have_received(:call)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#applies_to" do
|
30
|
+
let(:callback) do
|
31
|
+
Statesmin::Callback.new(from: :x, to: :y, callback: cb_lambda)
|
32
|
+
end
|
33
|
+
subject { callback.applies_to?(from: from, to: to) }
|
34
|
+
|
35
|
+
context "with any from value" do
|
36
|
+
let(:from) { nil }
|
37
|
+
|
38
|
+
context "and an allowed to value" do
|
39
|
+
let(:to) { :y }
|
40
|
+
it { is_expected.to be_truthy }
|
41
|
+
end
|
42
|
+
|
43
|
+
context "and a disallowed to value" do
|
44
|
+
let(:to) { :a }
|
45
|
+
it { is_expected.to be_falsey }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "with any to value" do
|
50
|
+
let(:to) { nil }
|
51
|
+
|
52
|
+
context "and an allowed 'from' value" do
|
53
|
+
let(:from) { :x }
|
54
|
+
it { is_expected.to be_truthy }
|
55
|
+
end
|
56
|
+
|
57
|
+
context "and a disallowed 'from' value" do
|
58
|
+
let(:from) { :a }
|
59
|
+
it { is_expected.to be_falsey }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "with any to and any from value on the callback" do
|
64
|
+
let(:callback) { Statesmin::Callback.new(callback: cb_lambda) }
|
65
|
+
let(:from) { :x }
|
66
|
+
let(:to) { :y }
|
67
|
+
|
68
|
+
it { is_expected.to be_truthy }
|
69
|
+
end
|
70
|
+
|
71
|
+
context "with any from value on the callback" do
|
72
|
+
let(:callback) do
|
73
|
+
Statesmin::Callback.new(to: [:y, :z], callback: cb_lambda)
|
74
|
+
end
|
75
|
+
let(:from) { :x }
|
76
|
+
|
77
|
+
context "and an allowed to value" do
|
78
|
+
let(:to) { :y }
|
79
|
+
it { is_expected.to be_truthy }
|
80
|
+
end
|
81
|
+
|
82
|
+
context "and another allowed to value" do
|
83
|
+
let(:to) { :z }
|
84
|
+
it { is_expected.to be_truthy }
|
85
|
+
end
|
86
|
+
|
87
|
+
context "and a disallowed to value" do
|
88
|
+
let(:to) { :a }
|
89
|
+
it { is_expected.to be_falsey }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "with any to value on the callback" do
|
94
|
+
let(:callback) { Statesmin::Callback.new(from: :x, callback: cb_lambda) }
|
95
|
+
let(:to) { :y }
|
96
|
+
|
97
|
+
context "and an allowed to value" do
|
98
|
+
let(:from) { :x }
|
99
|
+
it { is_expected.to be_truthy }
|
100
|
+
end
|
101
|
+
|
102
|
+
context "and a disallowed to value" do
|
103
|
+
let(:from) { :a }
|
104
|
+
it { is_expected.to be_falsey }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context "with allowed 'from' and 'to' values" do
|
109
|
+
let(:from) { :x }
|
110
|
+
let(:to) { :y }
|
111
|
+
it { is_expected.to be_truthy }
|
112
|
+
end
|
113
|
+
|
114
|
+
context "with disallowed 'from' and 'to' values" do
|
115
|
+
let(:from) { :a }
|
116
|
+
let(:to) { :b }
|
117
|
+
it { is_expected.to be_falsey }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Statesmin::Guard do
|
4
|
+
let(:callback) { -> {} }
|
5
|
+
let(:guard) { Statesmin::Guard.new(from: nil, to: nil, callback: callback) }
|
6
|
+
|
7
|
+
specify { expect(guard).to be_a(Statesmin::Callback) }
|
8
|
+
|
9
|
+
describe "#call" do
|
10
|
+
subject(:call) { guard.call }
|
11
|
+
|
12
|
+
context "success" do
|
13
|
+
let(:callback) { -> { true } }
|
14
|
+
specify { expect { call }.to_not raise_error }
|
15
|
+
end
|
16
|
+
|
17
|
+
context "error" do
|
18
|
+
let(:callback) { -> { false } }
|
19
|
+
specify { expect { call }.to raise_error(Statesmin::GuardFailedError) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,704 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Statesmin::Machine do
|
4
|
+
let(:machine) { Class.new { include Statesmin::Machine } }
|
5
|
+
let(:my_model) { Class.new { attr_accessor :current_state }.new }
|
6
|
+
|
7
|
+
describe ".state" do
|
8
|
+
before { machine.state(:x) }
|
9
|
+
before { machine.state(:y) }
|
10
|
+
specify { expect(machine.states).to eq(%w(x y)) }
|
11
|
+
|
12
|
+
context "initial" do
|
13
|
+
before { machine.state(:x, initial: true) }
|
14
|
+
specify { expect(machine.initial_state).to eq("x") }
|
15
|
+
|
16
|
+
context "when an initial state is already defined" do
|
17
|
+
it "raises an error" do
|
18
|
+
expect { machine.state(:y, initial: true) }.
|
19
|
+
to raise_error(Statesmin::InvalidStateError)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe ".retry_conflicts" do
|
26
|
+
before do
|
27
|
+
machine.class_eval do
|
28
|
+
state :x, initial: true
|
29
|
+
state :y
|
30
|
+
state :z
|
31
|
+
transition from: :x, to: :y
|
32
|
+
transition from: :y, to: :z
|
33
|
+
end
|
34
|
+
end
|
35
|
+
let(:instance) { machine.new(my_model) }
|
36
|
+
let(:retry_attempts) { 2 }
|
37
|
+
|
38
|
+
subject(:transition_state) do
|
39
|
+
Statesmin::Machine.retry_conflicts(retry_attempts) do
|
40
|
+
instance.transition_to(:y)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "when no exception occurs" do
|
45
|
+
it "runs the transition once" do
|
46
|
+
expect(instance).to receive(:transition_to).once
|
47
|
+
transition_state
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when an irrelevant exception occurs" do
|
52
|
+
it "runs the transition once" do
|
53
|
+
expect(instance).
|
54
|
+
to receive(:transition_to).once.
|
55
|
+
and_raise(StandardError)
|
56
|
+
transition_state rescue nil # rubocop:disable RescueModifier
|
57
|
+
end
|
58
|
+
|
59
|
+
it "re-raises the exception" do
|
60
|
+
allow(instance).to receive(:transition_to).once.
|
61
|
+
and_raise(StandardError)
|
62
|
+
expect { transition_state }.to raise_error(StandardError)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context "when a TransitionConflictError occurs" do
|
67
|
+
context "and is resolved on the second attempt" do
|
68
|
+
it "runs the transition twice" do
|
69
|
+
expect(instance).
|
70
|
+
to receive(:transition_to).once.
|
71
|
+
and_raise(Statesmin::TransitionConflictError).
|
72
|
+
ordered
|
73
|
+
expect(instance).
|
74
|
+
to receive(:transition_to).once.ordered.and_call_original
|
75
|
+
transition_state
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "and keeps occurring" do
|
80
|
+
it "runs the transition `retry_attempts + 1` times" do
|
81
|
+
expect(instance).
|
82
|
+
to receive(:transition_to).
|
83
|
+
exactly(retry_attempts + 1).times.
|
84
|
+
and_raise(Statesmin::TransitionConflictError)
|
85
|
+
transition_state rescue nil # rubocop:disable RescueModifier
|
86
|
+
end
|
87
|
+
|
88
|
+
it "re-raises the conflict" do
|
89
|
+
allow(instance).
|
90
|
+
to receive(:transition_to).
|
91
|
+
and_raise(Statesmin::TransitionConflictError)
|
92
|
+
expect { transition_state }.
|
93
|
+
to raise_error(Statesmin::TransitionConflictError)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe ".transition" do
|
100
|
+
before do
|
101
|
+
machine.class_eval do
|
102
|
+
state :x
|
103
|
+
state :y
|
104
|
+
state :z
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context "given neither a 'from' nor a 'to' state" do
|
109
|
+
it "raises an error" do
|
110
|
+
expect { machine.transition }.
|
111
|
+
to raise_error(Statesmin::InvalidStateError)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context "given no 'from' state and a valid 'to' state" do
|
116
|
+
it "raises an error" do
|
117
|
+
expect { machine.transition from: nil, to: :x }.
|
118
|
+
to raise_error(Statesmin::InvalidStateError)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "given a valid 'from' state and a no 'to' state" do
|
123
|
+
it "raises an error" do
|
124
|
+
expect { machine.transition from: :x, to: nil }.
|
125
|
+
to raise_error(Statesmin::InvalidStateError)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context "given a valid 'from' state and an empty 'to' state array" do
|
130
|
+
it "raises an error" do
|
131
|
+
expect { machine.transition from: :x, to: [] }.
|
132
|
+
to raise_error(Statesmin::InvalidStateError)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
context "given an invalid 'from' state" do
|
137
|
+
it "raises an error" do
|
138
|
+
expect { machine.transition(from: :a, to: :x) }.
|
139
|
+
to raise_error(Statesmin::InvalidStateError)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context "given an invalid 'to' state" do
|
144
|
+
it "raises an error" do
|
145
|
+
expect { machine.transition(from: :x, to: :a) }.
|
146
|
+
to raise_error(Statesmin::InvalidStateError)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
context "valid 'from' and 'to' states" do
|
151
|
+
it "records the transition" do
|
152
|
+
machine.transition(from: :x, to: :y)
|
153
|
+
machine.transition(from: :x, to: :z)
|
154
|
+
expect(machine.successors).to eq("x" => %w(y z))
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe ".validate_callback_condition" do
|
160
|
+
before do
|
161
|
+
machine.class_eval do
|
162
|
+
state :x
|
163
|
+
state :y
|
164
|
+
state :z
|
165
|
+
transition from: :x, to: :y
|
166
|
+
transition from: :y, to: :z
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context "with a terminal 'from' state" do
|
171
|
+
it "raises an exception" do
|
172
|
+
expect { machine.validate_callback_condition(from: :z, to: :y) }.
|
173
|
+
to raise_error(Statesmin::InvalidTransitionError)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context "with an initial 'to' state" do
|
178
|
+
it "raises an exception" do
|
179
|
+
expect { machine.validate_callback_condition(from: :y, to: :x) }.
|
180
|
+
to raise_error(Statesmin::InvalidTransitionError)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context "with an invalid transition" do
|
185
|
+
it "raises an exception" do
|
186
|
+
expect { machine.validate_callback_condition(from: :x, to: :z) }.
|
187
|
+
to raise_error(Statesmin::InvalidTransitionError)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
context "with any states" do
|
192
|
+
it "does not raise an exception" do
|
193
|
+
expect { machine.validate_callback_condition }.to_not raise_error
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
context "with a valid transition" do
|
198
|
+
it "does not raise an exception" do
|
199
|
+
expect { machine.validate_callback_condition(from: :x, to: :y) }.
|
200
|
+
to_not raise_error
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
shared_examples "a callback store" do |assignment_method, callback_store|
|
206
|
+
before do
|
207
|
+
machine.class_eval do
|
208
|
+
state :x, initial: true
|
209
|
+
state :y
|
210
|
+
state :z
|
211
|
+
transition from: :x, to: [:y, :z]
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
let(:options) { { from: nil, to: [] } }
|
216
|
+
let(:set_callback) { machine.send(assignment_method, options) {} }
|
217
|
+
|
218
|
+
shared_examples "fails" do |error_type|
|
219
|
+
specify { expect { set_callback }.to raise_error(error_type) }
|
220
|
+
|
221
|
+
it "does not add a callback" do
|
222
|
+
expect do
|
223
|
+
begin
|
224
|
+
set_callback
|
225
|
+
rescue error_type
|
226
|
+
nil
|
227
|
+
end
|
228
|
+
end.to_not change(machine.callbacks[callback_store], :count)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
shared_examples "adds callback" do
|
233
|
+
specify { expect { set_callback }.to_not raise_error }
|
234
|
+
|
235
|
+
it "stores callbacks" do
|
236
|
+
expect { set_callback }.
|
237
|
+
to change(machine.callbacks[callback_store], :count).by(1)
|
238
|
+
end
|
239
|
+
|
240
|
+
it "stores callback instances" do
|
241
|
+
set_callback
|
242
|
+
machine.callbacks[callback_store].each do |callback|
|
243
|
+
expect(callback).to be_a(Statesmin::Callback)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
context "with invalid states" do
|
249
|
+
context "when both are invalid" do
|
250
|
+
let(:options) { { from: :foo, to: :bar } }
|
251
|
+
it_behaves_like "fails", Statesmin::InvalidStateError
|
252
|
+
end
|
253
|
+
|
254
|
+
context "from a terminal state to anything" do
|
255
|
+
let(:options) { { from: :y, to: [] } }
|
256
|
+
it_behaves_like "fails", Statesmin::InvalidTransitionError
|
257
|
+
end
|
258
|
+
|
259
|
+
context "to an initial state and from anything" do
|
260
|
+
let(:options) { { from: nil, to: :x } }
|
261
|
+
it_behaves_like "fails", Statesmin::InvalidTransitionError
|
262
|
+
end
|
263
|
+
|
264
|
+
context "from a terminal state and to multiple states" do
|
265
|
+
let(:options) { { from: :y, to: [:x, :z] } }
|
266
|
+
it_behaves_like "fails", Statesmin::InvalidTransitionError
|
267
|
+
end
|
268
|
+
|
269
|
+
context "to an initial state and other states" do
|
270
|
+
let(:options) { { from: nil, to: [:y, :x, :z] } }
|
271
|
+
it_behaves_like "fails", Statesmin::InvalidTransitionError
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
context "with validate_states" do
|
276
|
+
context "from anything" do
|
277
|
+
let(:options) { { from: nil, to: :y } }
|
278
|
+
it_behaves_like "adds callback"
|
279
|
+
end
|
280
|
+
|
281
|
+
context "to anything" do
|
282
|
+
let(:options) { { from: :x, to: [] } }
|
283
|
+
it_behaves_like "adds callback"
|
284
|
+
end
|
285
|
+
|
286
|
+
context "to several" do
|
287
|
+
let(:options) { { from: :x, to: [:y, :z] } }
|
288
|
+
it_behaves_like "adds callback"
|
289
|
+
end
|
290
|
+
|
291
|
+
context "from any to several" do
|
292
|
+
let(:options) { { from: nil, to: [:y, :z] } }
|
293
|
+
it_behaves_like "adds callback"
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
describe ".before_transition" do
|
299
|
+
it_behaves_like "a callback store", :before_transition, :before
|
300
|
+
end
|
301
|
+
|
302
|
+
describe ".after_transition" do
|
303
|
+
it_behaves_like "a callback store", :after_transition, :after
|
304
|
+
end
|
305
|
+
|
306
|
+
describe ".guard_transition" do
|
307
|
+
it_behaves_like "a callback store", :guard_transition, :guards
|
308
|
+
end
|
309
|
+
|
310
|
+
describe "#initialize" do
|
311
|
+
before do
|
312
|
+
machine.class_eval do
|
313
|
+
state :x, initial: true
|
314
|
+
state :y
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
it "accepts an object to manipulate" do
|
319
|
+
machine_instance = machine.new(my_model)
|
320
|
+
expect(machine_instance.object).to be(my_model)
|
321
|
+
end
|
322
|
+
|
323
|
+
context "with a state option given" do
|
324
|
+
context "and the option is a valid state" do
|
325
|
+
it "sets the current_state to the supplied state option" do
|
326
|
+
machine_instance = machine.new(my_model, state: :y)
|
327
|
+
expect(machine_instance.current_state).to eq("y")
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
context "and the option is not a valid state" do
|
332
|
+
it "raises an InvalidStateError" do
|
333
|
+
expect { machine.new(my_model, state: :xyz) }.
|
334
|
+
to raise_error(Statesmin::InvalidStateError)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
context "without a state option given" do
|
340
|
+
it "sets the current_state to the class defined initial state" do
|
341
|
+
machine_instance = machine.new(my_model)
|
342
|
+
expect(machine_instance.current_state).to eq("x")
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
describe "#after_initialize" do
|
348
|
+
it "is called after initialize" do
|
349
|
+
machine.class_eval do
|
350
|
+
def after_initialize; end
|
351
|
+
end
|
352
|
+
expect_any_instance_of(machine).to receive :after_initialize
|
353
|
+
machine.new(my_model)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
describe "#current_state" do
|
358
|
+
before do
|
359
|
+
machine.class_eval do
|
360
|
+
state :x, initial: true
|
361
|
+
state :y
|
362
|
+
state :z
|
363
|
+
transition from: :x, to: :y
|
364
|
+
transition from: :y, to: :z
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
let(:instance) { machine.new(my_model) }
|
369
|
+
subject { instance.current_state }
|
370
|
+
|
371
|
+
context "with no transitions" do
|
372
|
+
it { is_expected.to eq(machine.initial_state) }
|
373
|
+
end
|
374
|
+
|
375
|
+
context "with multiple transitions" do
|
376
|
+
before { instance.transition_to!(:y) }
|
377
|
+
before { instance.transition_to!(:z) }
|
378
|
+
|
379
|
+
it { is_expected.to eq("z") }
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
describe "#in_state?" do
|
384
|
+
before do
|
385
|
+
machine.class_eval do
|
386
|
+
state :x, initial: true
|
387
|
+
state :y
|
388
|
+
transition from: :x, to: :y
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
let(:instance) { machine.new(my_model) }
|
393
|
+
subject { instance.in_state?(state) }
|
394
|
+
before { instance.transition_to!(:y) }
|
395
|
+
|
396
|
+
context "when machine is in given state" do
|
397
|
+
let(:state) { "y" }
|
398
|
+
it { is_expected.to eq(true) }
|
399
|
+
end
|
400
|
+
|
401
|
+
context "when machine is not in given state" do
|
402
|
+
let(:state) { "x" }
|
403
|
+
it { is_expected.to eq(false) }
|
404
|
+
end
|
405
|
+
|
406
|
+
context "when given a symbol" do
|
407
|
+
let(:state) { :y }
|
408
|
+
it { is_expected.to eq(true) }
|
409
|
+
end
|
410
|
+
|
411
|
+
context "when given multiple states" do
|
412
|
+
context "when given multiple arguments" do
|
413
|
+
context "when one of the states is the current state" do
|
414
|
+
subject { instance.in_state?(:x, :y) }
|
415
|
+
it { is_expected.to eq(true) }
|
416
|
+
end
|
417
|
+
|
418
|
+
context "when none of the states are the current state" do
|
419
|
+
subject { instance.in_state?(:x, :z) }
|
420
|
+
it { is_expected.to eq(false) }
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
context "when given an array" do
|
425
|
+
context "when one of the states is the current state" do
|
426
|
+
subject { instance.in_state?([:x, :y]) }
|
427
|
+
it { is_expected.to eq(true) }
|
428
|
+
end
|
429
|
+
|
430
|
+
context "when none of the states are the current state" do
|
431
|
+
subject { instance.in_state?([:x, :z]) }
|
432
|
+
it { is_expected.to eq(false) }
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
describe "#allowed_transitions" do
|
439
|
+
before do
|
440
|
+
machine.class_eval do
|
441
|
+
state :x, initial: true
|
442
|
+
state :y
|
443
|
+
state :z
|
444
|
+
transition from: :x, to: [:y, :z]
|
445
|
+
transition from: :y, to: :z
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
let(:instance) { machine.new(my_model) }
|
450
|
+
subject { instance.allowed_transitions }
|
451
|
+
|
452
|
+
context "with multiple possible states" do
|
453
|
+
it { is_expected.to eq(%w(y z)) }
|
454
|
+
end
|
455
|
+
|
456
|
+
context "with one possible state" do
|
457
|
+
before { instance.transition_to!(:y) }
|
458
|
+
it { is_expected.to eq(['z']) }
|
459
|
+
end
|
460
|
+
|
461
|
+
context "with no possible transitions" do
|
462
|
+
before { instance.transition_to!(:z) }
|
463
|
+
it { is_expected.to eq([]) }
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
describe "#can_transition_to?" do
|
468
|
+
before do
|
469
|
+
machine.class_eval do
|
470
|
+
state :x, initial: true
|
471
|
+
state :y
|
472
|
+
state :z
|
473
|
+
transition from: :x, to: :y
|
474
|
+
transition from: :y, to: :z
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
let(:instance) { machine.new(my_model) }
|
479
|
+
subject { instance.can_transition_to?(new_state) }
|
480
|
+
|
481
|
+
context "when the transition is invalid" do
|
482
|
+
context "with an initial to state" do
|
483
|
+
let(:new_state) { :x }
|
484
|
+
it { is_expected.to be_falsey }
|
485
|
+
end
|
486
|
+
|
487
|
+
context "with a terminal from state" do
|
488
|
+
before { instance.transition_to!(:y) }
|
489
|
+
let(:new_state) { :y }
|
490
|
+
it { is_expected.to be_falsey }
|
491
|
+
end
|
492
|
+
|
493
|
+
context "and is guarded" do
|
494
|
+
let(:guard_cb) { -> { false } }
|
495
|
+
let(:new_state) { :z }
|
496
|
+
before { machine.guard_transition(to: new_state, &guard_cb) }
|
497
|
+
|
498
|
+
it "does not fire guard" do
|
499
|
+
expect(guard_cb).not_to receive(:call)
|
500
|
+
is_expected.to be_falsey
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
context "when the transition valid" do
|
506
|
+
let(:new_state) { :y }
|
507
|
+
it { is_expected.to be_truthy }
|
508
|
+
|
509
|
+
context "but it has a failing guard" do
|
510
|
+
before { machine.guard_transition(to: :y) { false } }
|
511
|
+
it { is_expected.to be_falsey }
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
describe "#transition_to!" do
|
517
|
+
before do
|
518
|
+
machine.class_eval do
|
519
|
+
state :x, initial: true
|
520
|
+
state :y
|
521
|
+
state :z
|
522
|
+
transition from: :x, to: :y
|
523
|
+
transition from: :y, to: :z
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
let(:instance) { machine.new(my_model) }
|
528
|
+
|
529
|
+
context "when it is called with a block" do
|
530
|
+
let(:block_spy) { double(called: 'called') }
|
531
|
+
let(:block) { proc { block_spy.called } }
|
532
|
+
|
533
|
+
context "and the state cannot be transitioned to" do
|
534
|
+
it "does not call the block" do
|
535
|
+
expect(block_spy).to_not receive(:called)
|
536
|
+
expect { instance.transition_to!(:z, &block) }.to raise_error
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
context "and the state can be transitioned to" do
|
541
|
+
it "calls the block" do
|
542
|
+
expect(block_spy).to receive(:called)
|
543
|
+
instance.transition_to(:y, &block)
|
544
|
+
end
|
545
|
+
|
546
|
+
context 'and the block errors' do
|
547
|
+
let(:error_block) { proc { raise } }
|
548
|
+
|
549
|
+
it "raises the error" do
|
550
|
+
expect { instance.transition_to(:y, &error_block) }.
|
551
|
+
to raise_error(RuntimeError)
|
552
|
+
end
|
553
|
+
|
554
|
+
it "does not change the current_state" do
|
555
|
+
expect { instance.transition_to(:y, &error_block) }.to raise_error
|
556
|
+
expect(instance.current_state).to eq('x')
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
context 'and the block does not error' do
|
561
|
+
it "returns the value of the block" do
|
562
|
+
expect(instance.transition_to(:y, &block)).to eq('called')
|
563
|
+
end
|
564
|
+
|
565
|
+
it "updates the current_state" do
|
566
|
+
instance.transition_to(:y, &block)
|
567
|
+
expect(instance.current_state).to eq('y')
|
568
|
+
end
|
569
|
+
end
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
context "when the state cannot be transitioned to" do
|
574
|
+
it "raises an error" do
|
575
|
+
expect { instance.transition_to!(:z) }.
|
576
|
+
to raise_error(Statesmin::TransitionFailedError)
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
context "when the state can be transitioned to" do
|
581
|
+
it "changes state" do
|
582
|
+
instance.transition_to!(:y)
|
583
|
+
expect(instance.current_state).to eq("y")
|
584
|
+
end
|
585
|
+
|
586
|
+
specify { expect(instance.transition_to!(:y)).to eq(true) }
|
587
|
+
|
588
|
+
context "with a guard" do
|
589
|
+
let(:result) { true }
|
590
|
+
let(:guard_cb) { ->(*_args) { result } }
|
591
|
+
before { machine.guard_transition(from: :x, to: :y, &guard_cb) }
|
592
|
+
|
593
|
+
context "and an object to act on" do
|
594
|
+
let(:instance) { machine.new(my_model) }
|
595
|
+
|
596
|
+
it "passes the object to the guard" do
|
597
|
+
expect(guard_cb).to receive(:call).once.
|
598
|
+
with(my_model, {}).and_return(true)
|
599
|
+
instance.transition_to!(:y)
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
context "which passes" do
|
604
|
+
it "changes state" do
|
605
|
+
instance.transition_to!(:y)
|
606
|
+
expect(instance.current_state).to eq("y")
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
context "which fails" do
|
611
|
+
let(:result) { false }
|
612
|
+
|
613
|
+
it "raises an exception" do
|
614
|
+
expect { instance.transition_to!(:y) }.
|
615
|
+
to raise_error(Statesmin::GuardFailedError)
|
616
|
+
end
|
617
|
+
end
|
618
|
+
end
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
describe "#transition_to" do
|
623
|
+
let(:instance) { machine.new(my_model) }
|
624
|
+
let(:metadata) { { some: :metadata } }
|
625
|
+
subject { instance.transition_to(:some_state, metadata, &proc {}) }
|
626
|
+
|
627
|
+
context "when it is succesful" do
|
628
|
+
before do
|
629
|
+
expect(instance).to receive(:transition_to!).once.
|
630
|
+
with(:some_state, metadata).and_return(:some_state)
|
631
|
+
end
|
632
|
+
it { is_expected.to be(:some_state) }
|
633
|
+
end
|
634
|
+
|
635
|
+
context "when it is unsuccesful" do
|
636
|
+
before do
|
637
|
+
allow(instance).to receive(:transition_to!).
|
638
|
+
and_raise(Statesmin::GuardFailedError)
|
639
|
+
end
|
640
|
+
it { is_expected.to be_falsey }
|
641
|
+
end
|
642
|
+
|
643
|
+
context "when a non statesmin exception is raised" do
|
644
|
+
before do
|
645
|
+
allow(instance).to receive(:transition_to!).
|
646
|
+
and_raise(RuntimeError, 'user defined exception')
|
647
|
+
end
|
648
|
+
|
649
|
+
it "should not rescue the exception" do
|
650
|
+
expect { instance.transition_to(:some_state, metadata) }.
|
651
|
+
to raise_error(RuntimeError, 'user defined exception')
|
652
|
+
end
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
656
|
+
shared_examples "a callback filter" do |definer, phase|
|
657
|
+
before do
|
658
|
+
machine.class_eval do
|
659
|
+
state :x
|
660
|
+
state :y
|
661
|
+
state :z
|
662
|
+
transition from: :x, to: :y
|
663
|
+
transition from: :y, to: :z
|
664
|
+
end
|
665
|
+
end
|
666
|
+
|
667
|
+
let(:instance) { machine.new(my_model) }
|
668
|
+
let(:callbacks) { instance.send(:callbacks_for, phase, from: :x, to: :y) }
|
669
|
+
|
670
|
+
context "with no defined callbacks" do
|
671
|
+
specify { expect(callbacks).to eq([]) }
|
672
|
+
end
|
673
|
+
|
674
|
+
context "with defined callbacks" do
|
675
|
+
let(:callback_1) { -> { "Hi" } }
|
676
|
+
let(:callback_2) { -> { "Bye" } }
|
677
|
+
|
678
|
+
before do
|
679
|
+
machine.send(definer, from: :x, to: :y, &callback_1)
|
680
|
+
machine.send(definer, from: :y, to: :z, &callback_2)
|
681
|
+
end
|
682
|
+
|
683
|
+
it "contains the relevant callback" do
|
684
|
+
expect(callbacks.map(&:callback)).to include(callback_1)
|
685
|
+
end
|
686
|
+
|
687
|
+
it "does not contain the irrelevant callback" do
|
688
|
+
expect(callbacks.map(&:callback)).to_not include(callback_2)
|
689
|
+
end
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
describe "#guards_for" do
|
694
|
+
it_behaves_like "a callback filter", :guard_transition, :guards
|
695
|
+
end
|
696
|
+
|
697
|
+
describe "#before_callbacks_for" do
|
698
|
+
it_behaves_like "a callback filter", :before_transition, :before
|
699
|
+
end
|
700
|
+
|
701
|
+
describe "#after_callbacks_for" do
|
702
|
+
it_behaves_like "a callback filter", :after_transition, :after
|
703
|
+
end
|
704
|
+
end
|