statesman 7.4.0 → 12.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.
- 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
|