statesman 7.4.0 → 12.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/tests.yml +112 -0
- data/.gitignore +65 -15
- data/.rspec +1 -0
- data/.rubocop.yml +14 -1
- data/.rubocop_todo.yml +37 -28
- data/.ruby-version +1 -0
- data/CHANGELOG.md +262 -41
- data/CONTRIBUTING.md +23 -4
- data/Gemfile +4 -6
- data/README.md +243 -43
- data/docs/COMPATIBILITY.md +2 -2
- data/lib/generators/statesman/active_record_transition_generator.rb +1 -1
- data/lib/generators/statesman/generator_helpers.rb +12 -4
- data/lib/statesman/adapters/active_record.rb +84 -55
- data/lib/statesman/adapters/active_record_queries.rb +19 -7
- data/lib/statesman/adapters/active_record_transition.rb +5 -1
- data/lib/statesman/adapters/memory.rb +5 -1
- data/lib/statesman/adapters/type_safe_active_record_queries.rb +21 -0
- data/lib/statesman/callback.rb +2 -2
- data/lib/statesman/config.rb +3 -10
- data/lib/statesman/exceptions.rb +13 -7
- data/lib/statesman/guard.rb +1 -1
- data/lib/statesman/machine.rb +68 -0
- data/lib/statesman/version.rb +1 -1
- data/lib/statesman.rb +5 -5
- data/lib/tasks/statesman.rake +5 -5
- data/spec/generators/statesman/active_record_transition_generator_spec.rb +7 -1
- data/spec/generators/statesman/migration_generator_spec.rb +5 -1
- data/spec/spec_helper.rb +44 -7
- data/spec/statesman/adapters/active_record_queries_spec.rb +34 -12
- data/spec/statesman/adapters/active_record_spec.rb +176 -51
- data/spec/statesman/adapters/active_record_transition_spec.rb +5 -2
- data/spec/statesman/adapters/memory_spec.rb +0 -1
- data/spec/statesman/adapters/memory_transition_spec.rb +0 -1
- data/spec/statesman/adapters/shared_examples.rb +3 -4
- data/spec/statesman/adapters/type_safe_active_record_queries_spec.rb +206 -0
- data/spec/statesman/callback_spec.rb +0 -2
- data/spec/statesman/config_spec.rb +0 -2
- data/spec/statesman/exceptions_spec.rb +17 -4
- data/spec/statesman/guard_spec.rb +0 -2
- data/spec/statesman/machine_spec.rb +252 -15
- data/spec/statesman/utils_spec.rb +0 -2
- data/spec/support/active_record.rb +156 -24
- data/spec/support/exactly_query_databases.rb +35 -0
- data/statesman.gemspec +9 -10
- metadata +32 -59
- data/.circleci/config.yml +0 -187
@@ -0,0 +1,206 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe Statesman::Adapters::TypeSafeActiveRecordQueries, :active_record do
|
4
|
+
def configure(klass, transition_class)
|
5
|
+
klass.send(:extend, described_class)
|
6
|
+
klass.configure_state_machine(
|
7
|
+
transition_class: transition_class,
|
8
|
+
initial_state: :initial,
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
before do
|
13
|
+
prepare_model_table
|
14
|
+
prepare_transitions_table
|
15
|
+
prepare_other_model_table
|
16
|
+
prepare_other_transitions_table
|
17
|
+
|
18
|
+
Statesman.configure do
|
19
|
+
storage_adapter(Statesman::Adapters::ActiveRecord)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
|
24
|
+
|
25
|
+
let!(:model) do
|
26
|
+
model = MyActiveRecordModel.create
|
27
|
+
model.state_machine.transition_to(:succeeded)
|
28
|
+
model
|
29
|
+
end
|
30
|
+
|
31
|
+
let!(:other_model) do
|
32
|
+
model = MyActiveRecordModel.create
|
33
|
+
model.state_machine.transition_to(:failed)
|
34
|
+
model
|
35
|
+
end
|
36
|
+
|
37
|
+
let!(:initial_state_model) { MyActiveRecordModel.create }
|
38
|
+
|
39
|
+
let!(:returned_to_initial_model) do
|
40
|
+
model = MyActiveRecordModel.create
|
41
|
+
model.state_machine.transition_to(:failed)
|
42
|
+
model.state_machine.transition_to(:initial)
|
43
|
+
model
|
44
|
+
end
|
45
|
+
|
46
|
+
shared_examples "testing methods" do
|
47
|
+
before do
|
48
|
+
configure(MyActiveRecordModel, MyActiveRecordModelTransition)
|
49
|
+
configure(OtherActiveRecordModel, OtherActiveRecordModelTransition)
|
50
|
+
|
51
|
+
MyActiveRecordModel.send(:has_one, :other_active_record_model)
|
52
|
+
OtherActiveRecordModel.send(:belongs_to, :my_active_record_model)
|
53
|
+
end
|
54
|
+
|
55
|
+
describe ".in_state" do
|
56
|
+
context "given a single state" do
|
57
|
+
subject { MyActiveRecordModel.in_state(:succeeded) }
|
58
|
+
|
59
|
+
it { is_expected.to include model }
|
60
|
+
it { is_expected.to_not include other_model }
|
61
|
+
end
|
62
|
+
|
63
|
+
context "given multiple states" do
|
64
|
+
subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
|
65
|
+
|
66
|
+
it { is_expected.to include model }
|
67
|
+
it { is_expected.to include other_model }
|
68
|
+
end
|
69
|
+
|
70
|
+
context "given the initial state" do
|
71
|
+
subject { MyActiveRecordModel.in_state(:initial) }
|
72
|
+
|
73
|
+
it { is_expected.to include initial_state_model }
|
74
|
+
it { is_expected.to include returned_to_initial_model }
|
75
|
+
end
|
76
|
+
|
77
|
+
context "given an array of states" do
|
78
|
+
subject { MyActiveRecordModel.in_state(%i[succeeded failed]) }
|
79
|
+
|
80
|
+
it { is_expected.to include model }
|
81
|
+
it { is_expected.to include other_model }
|
82
|
+
end
|
83
|
+
|
84
|
+
context "merging two queries" do
|
85
|
+
subject do
|
86
|
+
MyActiveRecordModel.in_state(:succeeded).
|
87
|
+
joins(:other_active_record_model).
|
88
|
+
merge(OtherActiveRecordModel.in_state(:initial))
|
89
|
+
end
|
90
|
+
|
91
|
+
it { is_expected.to be_empty }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe ".not_in_state" do
|
96
|
+
context "given a single state" do
|
97
|
+
subject { MyActiveRecordModel.not_in_state(:failed) }
|
98
|
+
|
99
|
+
it { is_expected.to include model }
|
100
|
+
it { is_expected.to_not include other_model }
|
101
|
+
end
|
102
|
+
|
103
|
+
context "given multiple states" do
|
104
|
+
subject(:not_in_state) { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
|
105
|
+
|
106
|
+
it do
|
107
|
+
expect(not_in_state).to contain_exactly(initial_state_model,
|
108
|
+
returned_to_initial_model)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "given an array of states" do
|
113
|
+
subject(:not_in_state) { MyActiveRecordModel.not_in_state(%i[succeeded failed]) }
|
114
|
+
|
115
|
+
it do
|
116
|
+
expect(not_in_state).to contain_exactly(initial_state_model,
|
117
|
+
returned_to_initial_model)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "with a custom name for the transition association" do
|
123
|
+
before do
|
124
|
+
# Switch to using OtherActiveRecordModelTransition, so the existing
|
125
|
+
# relation with MyActiveRecordModelTransition doesn't interfere with
|
126
|
+
# this spec.
|
127
|
+
MyActiveRecordModel.send(:has_many,
|
128
|
+
:custom_name,
|
129
|
+
class_name: "OtherActiveRecordModelTransition")
|
130
|
+
|
131
|
+
MyActiveRecordModel.class_eval do
|
132
|
+
def self.transition_class
|
133
|
+
OtherActiveRecordModelTransition
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe ".in_state" do
|
139
|
+
subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
|
140
|
+
|
141
|
+
specify { expect { query }.to_not raise_error }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
context "with a custom primary key for the model" do
|
146
|
+
before do
|
147
|
+
# Switch to using OtherActiveRecordModelTransition, so the existing
|
148
|
+
# relation with MyActiveRecordModelTransition doesn't interfere with
|
149
|
+
# this spec.
|
150
|
+
# Configure the relationship to use a different primary key,
|
151
|
+
MyActiveRecordModel.send(:has_many,
|
152
|
+
:custom_name,
|
153
|
+
class_name: "OtherActiveRecordModelTransition",
|
154
|
+
primary_key: :external_id)
|
155
|
+
|
156
|
+
MyActiveRecordModel.class_eval do
|
157
|
+
def self.transition_class
|
158
|
+
OtherActiveRecordModelTransition
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe ".in_state" do
|
164
|
+
subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
|
165
|
+
|
166
|
+
specify { expect { query }.to_not raise_error }
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context "after_commit transactional integrity" do
|
171
|
+
before do
|
172
|
+
MyStateMachine.class_eval do
|
173
|
+
cattr_accessor(:after_commit_callback_executed) { false }
|
174
|
+
|
175
|
+
after_transition(from: :initial, to: :succeeded, after_commit: true) do
|
176
|
+
# This leaks state in a testable way if transactional integrity is broken.
|
177
|
+
MyStateMachine.after_commit_callback_executed = true
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
after do
|
183
|
+
MyStateMachine.class_eval do
|
184
|
+
callbacks[:after_commit] = []
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
let!(:model) do
|
189
|
+
MyActiveRecordModel.create
|
190
|
+
end
|
191
|
+
|
192
|
+
it do
|
193
|
+
expect do
|
194
|
+
ActiveRecord::Base.transaction do
|
195
|
+
model.state_machine.transition_to!(:succeeded)
|
196
|
+
raise ActiveRecord::Rollback
|
197
|
+
end
|
198
|
+
end.to_not change(MyStateMachine, :after_commit_callback_executed)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
context "using configuration method" do
|
204
|
+
include_examples "testing methods"
|
205
|
+
end
|
206
|
+
end
|
@@ -1,12 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
describe Statesman do
|
3
|
+
describe "Exceptions" do
|
6
4
|
describe "InvalidStateError" do
|
7
5
|
subject(:error) { Statesman::InvalidStateError.new }
|
8
6
|
|
9
7
|
its(:message) { is_expected.to eq("Statesman::InvalidStateError") }
|
8
|
+
|
10
9
|
its "string matches its message" do
|
11
10
|
expect(error.to_s).to eq(error.message)
|
12
11
|
end
|
@@ -16,6 +15,7 @@ describe Statesman do
|
|
16
15
|
subject(:error) { Statesman::InvalidTransitionError.new }
|
17
16
|
|
18
17
|
its(:message) { is_expected.to eq("Statesman::InvalidTransitionError") }
|
18
|
+
|
19
19
|
its "string matches its message" do
|
20
20
|
expect(error.to_s).to eq(error.message)
|
21
21
|
end
|
@@ -25,6 +25,7 @@ describe Statesman do
|
|
25
25
|
subject(:error) { Statesman::InvalidTransitionError.new }
|
26
26
|
|
27
27
|
its(:message) { is_expected.to eq("Statesman::InvalidTransitionError") }
|
28
|
+
|
28
29
|
its "string matches its message" do
|
29
30
|
expect(error.to_s).to eq(error.message)
|
30
31
|
end
|
@@ -34,6 +35,7 @@ describe Statesman do
|
|
34
35
|
subject(:error) { Statesman::TransitionConflictError.new }
|
35
36
|
|
36
37
|
its(:message) { is_expected.to eq("Statesman::TransitionConflictError") }
|
38
|
+
|
37
39
|
its "string matches its message" do
|
38
40
|
expect(error.to_s).to eq(error.message)
|
39
41
|
end
|
@@ -43,6 +45,7 @@ describe Statesman do
|
|
43
45
|
subject(:error) { Statesman::MissingTransitionAssociation.new }
|
44
46
|
|
45
47
|
its(:message) { is_expected.to eq("Statesman::MissingTransitionAssociation") }
|
48
|
+
|
46
49
|
its "string matches its message" do
|
47
50
|
expect(error.to_s).to eq(error.message)
|
48
51
|
end
|
@@ -52,17 +55,25 @@ describe Statesman do
|
|
52
55
|
subject(:error) { Statesman::TransitionFailedError.new("from", "to") }
|
53
56
|
|
54
57
|
its(:message) { is_expected.to eq("Cannot transition from 'from' to 'to'") }
|
58
|
+
|
55
59
|
its "string matches its message" do
|
56
60
|
expect(error.to_s).to eq(error.message)
|
57
61
|
end
|
58
62
|
end
|
59
63
|
|
60
64
|
describe "GuardFailedError" do
|
61
|
-
subject(:error) { Statesman::GuardFailedError.new("from", "to") }
|
65
|
+
subject(:error) { Statesman::GuardFailedError.new("from", "to", callback) }
|
66
|
+
|
67
|
+
let(:callback) { -> { "hello" } }
|
62
68
|
|
63
69
|
its(:message) do
|
64
70
|
is_expected.to eq("Guard on transition from: 'from' to 'to' returned false")
|
65
71
|
end
|
72
|
+
|
73
|
+
its(:backtrace) do
|
74
|
+
is_expected.to eq([callback.source_location.join(":")])
|
75
|
+
end
|
76
|
+
|
66
77
|
its "string matches its message" do
|
67
78
|
expect(error.to_s).to eq(error.message)
|
68
79
|
end
|
@@ -72,6 +83,7 @@ describe Statesman do
|
|
72
83
|
subject(:error) { Statesman::UnserializedMetadataError.new("foo") }
|
73
84
|
|
74
85
|
its(:message) { is_expected.to match(/foo#metadata is not serialized/) }
|
86
|
+
|
75
87
|
its "string matches its message" do
|
76
88
|
expect(error.to_s).to eq(error.message)
|
77
89
|
end
|
@@ -81,6 +93,7 @@ describe Statesman do
|
|
81
93
|
subject(:error) { Statesman::IncompatibleSerializationError.new("foo") }
|
82
94
|
|
83
95
|
its(:message) { is_expected.to match(/foo#metadata column type cannot be json/) }
|
96
|
+
|
84
97
|
its "string matches its message" do
|
85
98
|
expect(error.to_s).to eq(error.message)
|
86
99
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "spec_helper"
|
4
|
-
|
5
3
|
describe Statesman::Machine do
|
6
4
|
let(:machine) { Class.new { include Statesman::Machine } }
|
7
5
|
let(:my_model) { Class.new { attr_accessor :current_state }.new }
|
@@ -27,6 +25,112 @@ describe Statesman::Machine do
|
|
27
25
|
end
|
28
26
|
end
|
29
27
|
|
28
|
+
describe ".remove_state" do
|
29
|
+
subject(:remove_state) { machine.remove_state(:x) }
|
30
|
+
|
31
|
+
before do
|
32
|
+
machine.class_eval do
|
33
|
+
state :x
|
34
|
+
state :y
|
35
|
+
state :z
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it "removes the state" do
|
40
|
+
expect { remove_state }.
|
41
|
+
to change(machine, :states).
|
42
|
+
from(match_array(%w[x y z])).
|
43
|
+
to(%w[y z])
|
44
|
+
end
|
45
|
+
|
46
|
+
context "with a transition from the removed state" do
|
47
|
+
before { machine.transition from: :x, to: :y }
|
48
|
+
|
49
|
+
it "removes the transition" do
|
50
|
+
expect { remove_state }.
|
51
|
+
to change(machine, :successors).
|
52
|
+
from({ "x" => ["y"] }).
|
53
|
+
to({})
|
54
|
+
end
|
55
|
+
|
56
|
+
context "with multiple transitions" do
|
57
|
+
before { machine.transition from: :x, to: :z }
|
58
|
+
|
59
|
+
it "removes all transitions" do
|
60
|
+
expect { remove_state }.
|
61
|
+
to change(machine, :successors).
|
62
|
+
from({ "x" => %w[y z] }).
|
63
|
+
to({})
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "with a transition to the removed state" do
|
69
|
+
before { machine.transition from: :y, to: :x }
|
70
|
+
|
71
|
+
it "removes the transition" do
|
72
|
+
expect { remove_state }.
|
73
|
+
to change(machine, :successors).
|
74
|
+
from({ "y" => ["x"] }).
|
75
|
+
to({})
|
76
|
+
end
|
77
|
+
|
78
|
+
context "with multiple transitions" do
|
79
|
+
before { machine.transition from: :z, to: :x }
|
80
|
+
|
81
|
+
it "removes all transitions" do
|
82
|
+
expect { remove_state }.
|
83
|
+
to change(machine, :successors).
|
84
|
+
from({ "y" => ["x"], "z" => ["x"] }).
|
85
|
+
to({})
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "with a callback from the removed state" do
|
91
|
+
before do
|
92
|
+
machine.class_eval do
|
93
|
+
transition from: :x, to: :y
|
94
|
+
transition from: :x, to: :z
|
95
|
+
guard_transition(from: :x) { return false }
|
96
|
+
guard_transition(from: :x, to: :z) { return true }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
let(:guards) do
|
101
|
+
[having_attributes(from: "x", to: []), having_attributes(from: "x", to: ["z"])]
|
102
|
+
end
|
103
|
+
|
104
|
+
it "removes the guard" do
|
105
|
+
expect { remove_state }.
|
106
|
+
to change(machine, :callbacks).
|
107
|
+
from(a_hash_including(guards: match_array(guards))).
|
108
|
+
to(a_hash_including(guards: []))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "with a callback to the removed state" do
|
113
|
+
before do
|
114
|
+
machine.class_eval do
|
115
|
+
transition from: :y, to: :x
|
116
|
+
guard_transition(to: :x) { return false }
|
117
|
+
guard_transition(from: :y, to: :x) { return true }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
let(:guards) do
|
122
|
+
[having_attributes(from: nil, to: ["x"]), having_attributes(from: "y", to: ["x"])]
|
123
|
+
end
|
124
|
+
|
125
|
+
it "removes the guard" do
|
126
|
+
expect { remove_state }.
|
127
|
+
to change(machine, :callbacks).
|
128
|
+
from(a_hash_including(guards: match_array(guards))).
|
129
|
+
to(a_hash_including(guards: []))
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
30
134
|
describe ".retry_conflicts" do
|
31
135
|
subject(:transition_state) do
|
32
136
|
described_class.retry_conflicts(retry_attempts) do
|
@@ -170,6 +274,42 @@ describe Statesman::Machine do
|
|
170
274
|
end
|
171
275
|
end
|
172
276
|
|
277
|
+
describe ".remove_transitions" do
|
278
|
+
before do
|
279
|
+
machine.class_eval do
|
280
|
+
state :x
|
281
|
+
state :y
|
282
|
+
state :z
|
283
|
+
transition from: :x, to: :y
|
284
|
+
transition from: :x, to: :z
|
285
|
+
transition from: :y, to: :z
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
let(:initial_successors) { { "x" => %w[y z], "y" => ["z"] } }
|
290
|
+
|
291
|
+
it "removes the correct transitions when given a from state" do
|
292
|
+
expect { machine.remove_transitions(from: :x) }.
|
293
|
+
to change(machine, :successors).
|
294
|
+
from(initial_successors).
|
295
|
+
to({ "y" => ["z"] })
|
296
|
+
end
|
297
|
+
|
298
|
+
it "removes the correct transitions when given a to state" do
|
299
|
+
expect { machine.remove_transitions(to: :z) }.
|
300
|
+
to change(machine, :successors).
|
301
|
+
from(initial_successors).
|
302
|
+
to({ "x" => ["y"] })
|
303
|
+
end
|
304
|
+
|
305
|
+
it "removes the correct transitions when given a from and to state" do
|
306
|
+
expect { machine.remove_transitions(from: :x, to: :z) }.
|
307
|
+
to change(machine, :successors).
|
308
|
+
from(initial_successors).
|
309
|
+
to({ "x" => ["y"], "y" => ["z"] })
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
173
313
|
describe ".validate_callback_condition" do
|
174
314
|
before do
|
175
315
|
machine.class_eval do
|
@@ -234,11 +374,9 @@ describe Statesman::Machine do
|
|
234
374
|
|
235
375
|
it "does not add a callback" do
|
236
376
|
expect do
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
nil
|
241
|
-
end
|
377
|
+
set_callback
|
378
|
+
rescue error_type
|
379
|
+
nil
|
242
380
|
end.to_not change(machine.callbacks[callback_store], :count)
|
243
381
|
end
|
244
382
|
end
|
@@ -340,12 +478,83 @@ describe Statesman::Machine do
|
|
340
478
|
it_behaves_like "a callback store", :after_guard_failure, :after_guard_failure
|
341
479
|
end
|
342
480
|
|
481
|
+
shared_examples "initial transition is not created" do
|
482
|
+
it "doesn't call .create on storage adapter" do
|
483
|
+
expect_any_instance_of(Statesman.storage_adapter).to_not receive(:create)
|
484
|
+
machine.new(my_model, options)
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
shared_examples "initial transition is created" do
|
489
|
+
it "calls .create on storage adapter" do
|
490
|
+
expect_any_instance_of(Statesman.storage_adapter).to receive(:create).with(nil, "x")
|
491
|
+
machine.new(my_model, options)
|
492
|
+
end
|
493
|
+
|
494
|
+
it "creates a new transition object" do
|
495
|
+
instance = machine.new(my_model, options)
|
496
|
+
|
497
|
+
expect(instance.history.count).to eq(1)
|
498
|
+
expect(instance.history.first.to_state).to eq("x")
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
343
502
|
describe "#initialize" do
|
344
503
|
it "accepts an object to manipulate" do
|
345
504
|
machine_instance = machine.new(my_model)
|
346
505
|
expect(machine_instance.object).to be(my_model)
|
347
506
|
end
|
348
507
|
|
508
|
+
context "initial_transition is not provided" do
|
509
|
+
let(:options) { {} }
|
510
|
+
|
511
|
+
it_behaves_like "initial transition is not created"
|
512
|
+
end
|
513
|
+
|
514
|
+
context "initial_transition is provided" do
|
515
|
+
context "initial_transition is true" do
|
516
|
+
let(:options) do
|
517
|
+
{ initial_transition: true,
|
518
|
+
transition_class: Statesman::Adapters::MemoryTransition }
|
519
|
+
end
|
520
|
+
|
521
|
+
context "history is empty" do
|
522
|
+
context "initial state is defined" do
|
523
|
+
before { machine.state(:x, initial: true) }
|
524
|
+
|
525
|
+
it_behaves_like "initial transition is created"
|
526
|
+
end
|
527
|
+
|
528
|
+
context "initial state is not defined" do
|
529
|
+
it_behaves_like "initial transition is not created"
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
context "history is not empty" do
|
534
|
+
before do
|
535
|
+
allow_any_instance_of(Statesman.storage_adapter).to receive(:history).
|
536
|
+
and_return([{}])
|
537
|
+
end
|
538
|
+
|
539
|
+
context "initial state is defined" do
|
540
|
+
before { machine.state(:x, initial: true) }
|
541
|
+
|
542
|
+
it_behaves_like "initial transition is not created"
|
543
|
+
end
|
544
|
+
|
545
|
+
context "initial state is not defined" do
|
546
|
+
it_behaves_like "initial transition is not created"
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
context "initial_transition is false" do
|
552
|
+
let(:options) { { initial_transition: false } }
|
553
|
+
|
554
|
+
it_behaves_like "initial transition is not created"
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
349
558
|
context "transition class" do
|
350
559
|
it "sets a default" do
|
351
560
|
expect(Statesman.storage_adapter).to receive(:new).once.
|
@@ -537,6 +746,34 @@ describe Statesman::Machine do
|
|
537
746
|
end
|
538
747
|
end
|
539
748
|
|
749
|
+
describe "#last_transition_to" do
|
750
|
+
subject { instance.last_transition_to(:y) }
|
751
|
+
|
752
|
+
before do
|
753
|
+
machine.class_eval do
|
754
|
+
state :x, initial: true
|
755
|
+
state :y
|
756
|
+
state :z
|
757
|
+
transition from: :x, to: :y
|
758
|
+
transition from: :y, to: :z
|
759
|
+
transition from: :z, to: :y
|
760
|
+
end
|
761
|
+
|
762
|
+
instance.transition_to!(:y)
|
763
|
+
instance.transition_to!(:z)
|
764
|
+
end
|
765
|
+
|
766
|
+
let(:instance) { machine.new(my_model) }
|
767
|
+
|
768
|
+
it { is_expected.to have_attributes(to_state: "y") }
|
769
|
+
|
770
|
+
context "when there are 2 transitions to the state" do
|
771
|
+
before { instance.transition_to!(:y) }
|
772
|
+
|
773
|
+
it { is_expected.to eq(instance.last_transition) }
|
774
|
+
end
|
775
|
+
end
|
776
|
+
|
540
777
|
describe "#can_transition_to?" do
|
541
778
|
subject(:can_transition_to?) { instance.can_transition_to?(new_state, metadata) }
|
542
779
|
|
@@ -767,10 +1004,10 @@ describe Statesman::Machine do
|
|
767
1004
|
it { is_expected.to be(:some_state) }
|
768
1005
|
end
|
769
1006
|
|
770
|
-
context "when it is
|
1007
|
+
context "when it is unsuccessful" do
|
771
1008
|
before do
|
772
1009
|
allow(instance).to receive(:transition_to!).
|
773
|
-
and_raise(Statesman::GuardFailedError.new(:x, :some_state))
|
1010
|
+
and_raise(Statesman::GuardFailedError.new(:x, :some_state, nil))
|
774
1011
|
end
|
775
1012
|
|
776
1013
|
it { is_expected.to be_falsey }
|
@@ -808,20 +1045,20 @@ describe Statesman::Machine do
|
|
808
1045
|
end
|
809
1046
|
|
810
1047
|
context "with defined callbacks" do
|
811
|
-
let(:
|
812
|
-
let(:
|
1048
|
+
let(:callback_one) { -> { "Hi" } }
|
1049
|
+
let(:callback_two) { -> { "Bye" } }
|
813
1050
|
|
814
1051
|
before do
|
815
|
-
machine.send(definer, from: :x, to: :y, &
|
816
|
-
machine.send(definer, from: :y, to: :z, &
|
1052
|
+
machine.send(definer, from: :x, to: :y, &callback_one)
|
1053
|
+
machine.send(definer, from: :y, to: :z, &callback_two)
|
817
1054
|
end
|
818
1055
|
|
819
1056
|
it "contains the relevant callback" do
|
820
|
-
expect(callbacks.map(&:callback)).to include(
|
1057
|
+
expect(callbacks.map(&:callback)).to include(callback_one)
|
821
1058
|
end
|
822
1059
|
|
823
1060
|
it "does not contain the irrelevant callback" do
|
824
|
-
expect(callbacks.map(&:callback)).to_not include(
|
1061
|
+
expect(callbacks.map(&:callback)).to_not include(callback_two)
|
825
1062
|
end
|
826
1063
|
end
|
827
1064
|
end
|