state_machines 0.6.0 → 0.20.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/README.md +93 -13
- data/lib/state_machines/branch.rb +8 -4
- data/lib/state_machines/callback.rb +2 -0
- data/lib/state_machines/core.rb +3 -2
- data/lib/state_machines/core_ext/class/state_machine.rb +2 -0
- data/lib/state_machines/core_ext.rb +2 -0
- data/lib/state_machines/error.rb +2 -0
- data/lib/state_machines/eval_helpers.rb +38 -9
- data/lib/state_machines/event.rb +22 -7
- data/lib/state_machines/event_collection.rb +2 -0
- data/lib/state_machines/extensions.rb +2 -0
- data/lib/state_machines/helper_module.rb +2 -0
- data/lib/state_machines/integrations/base.rb +2 -0
- data/lib/state_machines/integrations.rb +2 -0
- data/lib/state_machines/machine/class_methods.rb +79 -0
- data/lib/state_machines/machine.rb +21 -67
- data/lib/state_machines/machine_collection.rb +5 -1
- data/lib/state_machines/macro_methods.rb +2 -0
- data/lib/state_machines/matcher.rb +3 -0
- data/lib/state_machines/matcher_helpers.rb +2 -0
- data/lib/state_machines/node_collection.rb +5 -1
- data/lib/state_machines/options_validator.rb +72 -0
- data/lib/state_machines/path.rb +5 -1
- data/lib/state_machines/path_collection.rb +5 -1
- data/lib/state_machines/state.rb +71 -43
- data/lib/state_machines/state_collection.rb +2 -0
- data/lib/state_machines/state_context.rb +5 -1
- data/lib/state_machines/stdio_renderer.rb +74 -0
- data/lib/state_machines/test_helper.rb +305 -0
- data/lib/state_machines/transition.rb +2 -0
- data/lib/state_machines/transition_collection.rb +5 -1
- data/lib/state_machines/version.rb +3 -1
- data/lib/state_machines.rb +4 -1
- metadata +11 -9
- data/lib/state_machines/assertions.rb +0 -40
@@ -0,0 +1,305 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
# Test helper module providing assertion methods for state machine testing
|
5
|
+
# Designed to work with Minitest, RSpec, and future testing frameworks
|
6
|
+
#
|
7
|
+
# @example Basic usage with Minitest
|
8
|
+
# class MyModelTest < Minitest::Test
|
9
|
+
# include StateMachines::TestHelper
|
10
|
+
#
|
11
|
+
# def test_initial_state
|
12
|
+
# model = MyModel.new
|
13
|
+
# assert_state(model, :state_machine_name, :initial_state)
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @example Usage with RSpec
|
18
|
+
# RSpec.describe MyModel do
|
19
|
+
# include StateMachines::TestHelper
|
20
|
+
#
|
21
|
+
# it "starts in initial state" do
|
22
|
+
# model = MyModel.new
|
23
|
+
# assert_state(model, :state_machine_name, :initial_state)
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# @since 0.10.0
|
28
|
+
module TestHelper
|
29
|
+
# Assert that an object is in a specific state for a given state machine
|
30
|
+
#
|
31
|
+
# @param object [Object] The object with state machines
|
32
|
+
# @param machine_name [Symbol] The name of the state machine
|
33
|
+
# @param expected_state [Symbol] The expected state
|
34
|
+
# @param message [String, nil] Custom failure message
|
35
|
+
# @return [void]
|
36
|
+
# @raise [AssertionError] If the state doesn't match
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# user = User.new
|
40
|
+
# assert_state(user, :status, :active)
|
41
|
+
def assert_state(object, machine_name, expected_state, message = nil)
|
42
|
+
actual = object.send("#{machine_name}_name")
|
43
|
+
default_message = "Expected #{object.class}##{machine_name} to be #{expected_state}, but was #{actual}"
|
44
|
+
|
45
|
+
if defined?(::Minitest)
|
46
|
+
assert_equal expected_state.to_s, actual.to_s, message || default_message
|
47
|
+
elsif defined?(::RSpec)
|
48
|
+
expect(actual.to_s).to eq(expected_state.to_s), message || default_message
|
49
|
+
else
|
50
|
+
raise "Expected #{expected_state}, but got #{actual}" unless expected_state.to_s == actual.to_s
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Assert that an object can transition via a specific event
|
55
|
+
#
|
56
|
+
# @param object [Object] The object with state machines
|
57
|
+
# @param event [Symbol] The event name
|
58
|
+
# @param message [String, nil] Custom failure message
|
59
|
+
# @return [void]
|
60
|
+
# @raise [AssertionError] If the transition is not available
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
# user = User.new
|
64
|
+
# assert_can_transition(user, :activate)
|
65
|
+
def assert_can_transition(object, event, message = nil)
|
66
|
+
can_method = "can_#{event}?"
|
67
|
+
default_message = "Expected to be able to trigger event :#{event}, but #{can_method} returned false"
|
68
|
+
|
69
|
+
if defined?(::Minitest)
|
70
|
+
assert object.send(can_method), message || default_message
|
71
|
+
elsif defined?(::RSpec)
|
72
|
+
expect(object.send(can_method)).to be_truthy, message || default_message
|
73
|
+
else
|
74
|
+
raise default_message unless object.send(can_method)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Assert that an object cannot transition via a specific event
|
79
|
+
#
|
80
|
+
# @param object [Object] The object with state machines
|
81
|
+
# @param event [Symbol] The event name
|
82
|
+
# @param message [String, nil] Custom failure message
|
83
|
+
# @return [void]
|
84
|
+
# @raise [AssertionError] If the transition is available
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# user = User.new
|
88
|
+
# assert_cannot_transition(user, :delete)
|
89
|
+
def assert_cannot_transition(object, event, message = nil)
|
90
|
+
can_method = "can_#{event}?"
|
91
|
+
default_message = "Expected not to be able to trigger event :#{event}, but #{can_method} returned true"
|
92
|
+
|
93
|
+
if defined?(::Minitest)
|
94
|
+
refute object.send(can_method), message || default_message
|
95
|
+
elsif defined?(::RSpec)
|
96
|
+
expect(object.send(can_method)).to be_falsy, message || default_message
|
97
|
+
elsif object.send(can_method)
|
98
|
+
raise default_message
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Assert that triggering an event changes the object to the expected state
|
103
|
+
#
|
104
|
+
# @param object [Object] The object with state machines
|
105
|
+
# @param event [Symbol] The event to trigger
|
106
|
+
# @param machine_name [Symbol] The name of the state machine
|
107
|
+
# @param expected_state [Symbol] The expected state after transition
|
108
|
+
# @param message [String, nil] Custom failure message
|
109
|
+
# @return [void]
|
110
|
+
# @raise [AssertionError] If the transition fails or results in wrong state
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# user = User.new
|
114
|
+
# assert_transition(user, :activate, :status, :active)
|
115
|
+
def assert_transition(object, event, machine_name, expected_state, message = nil)
|
116
|
+
object.send("#{event}!")
|
117
|
+
assert_state(object, machine_name, expected_state, message)
|
118
|
+
end
|
119
|
+
|
120
|
+
# === Extended State Machine Assertions ===
|
121
|
+
|
122
|
+
def assert_sm_states_list(machine, expected_states, message = nil)
|
123
|
+
actual_states = machine.states.map(&:name).compact
|
124
|
+
default_message = "Expected states #{expected_states} but got #{actual_states}"
|
125
|
+
|
126
|
+
if defined?(::Minitest)
|
127
|
+
assert_equal expected_states.sort, actual_states.sort, message || default_message
|
128
|
+
elsif defined?(::RSpec)
|
129
|
+
expect(actual_states.sort).to eq(expected_states.sort), message || default_message
|
130
|
+
else
|
131
|
+
raise default_message unless expected_states.sort == actual_states.sort
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def refute_sm_state_defined(machine, state, message = nil)
|
136
|
+
state_exists = machine.states.any? { |s| s.name == state }
|
137
|
+
default_message = "Expected state #{state} to not be defined in machine"
|
138
|
+
|
139
|
+
if defined?(::Minitest)
|
140
|
+
refute state_exists, message || default_message
|
141
|
+
elsif defined?(::RSpec)
|
142
|
+
expect(state_exists).to be_falsy, message || default_message
|
143
|
+
elsif state_exists
|
144
|
+
raise default_message
|
145
|
+
end
|
146
|
+
end
|
147
|
+
alias assert_sm_state_not_defined refute_sm_state_defined
|
148
|
+
|
149
|
+
def assert_sm_initial_state(machine, expected_state, message = nil)
|
150
|
+
state_obj = machine.state(expected_state)
|
151
|
+
is_initial = state_obj&.initial?
|
152
|
+
default_message = "Expected state #{expected_state} to be the initial state"
|
153
|
+
|
154
|
+
if defined?(::Minitest)
|
155
|
+
assert is_initial, message || default_message
|
156
|
+
elsif defined?(::RSpec)
|
157
|
+
expect(is_initial).to be_truthy, message || default_message
|
158
|
+
else
|
159
|
+
raise default_message unless is_initial
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def assert_sm_final_state(machine, state, message = nil)
|
164
|
+
state_obj = machine.states[state]
|
165
|
+
is_final = state_obj&.final?
|
166
|
+
default_message = "Expected state #{state} to be final"
|
167
|
+
|
168
|
+
if defined?(::Minitest)
|
169
|
+
assert is_final, message || default_message
|
170
|
+
elsif defined?(::RSpec)
|
171
|
+
expect(is_final).to be_truthy, message || default_message
|
172
|
+
else
|
173
|
+
raise default_message unless is_final
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def assert_sm_possible_transitions(machine, from:, expected_to_states:, message: nil)
|
178
|
+
actual_transitions = machine.events.flat_map do |event|
|
179
|
+
event.branches.select { |branch| branch.known_states.include?(from) }
|
180
|
+
.map(&:to)
|
181
|
+
end.uniq
|
182
|
+
default_message = "Expected transitions from #{from} to #{expected_to_states} but got #{actual_transitions}"
|
183
|
+
|
184
|
+
if defined?(::Minitest)
|
185
|
+
assert_equal expected_to_states.sort, actual_transitions.sort, message || default_message
|
186
|
+
elsif defined?(::RSpec)
|
187
|
+
expect(actual_transitions.sort).to eq(expected_to_states.sort), message || default_message
|
188
|
+
else
|
189
|
+
raise default_message unless expected_to_states.sort == actual_transitions.sort
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def refute_sm_transition_allowed(machine, from:, to:, on:, message: nil)
|
194
|
+
event = machine.events[on]
|
195
|
+
is_allowed = event&.branches&.any? { |branch| branch.known_states.include?(from) && branch.to == to }
|
196
|
+
default_message = "Expected transition from #{from} to #{to} on #{on} to not be allowed"
|
197
|
+
|
198
|
+
if defined?(::Minitest)
|
199
|
+
refute is_allowed, message || default_message
|
200
|
+
elsif defined?(::RSpec)
|
201
|
+
expect(is_allowed).to be_falsy, message || default_message
|
202
|
+
elsif is_allowed
|
203
|
+
raise default_message
|
204
|
+
end
|
205
|
+
end
|
206
|
+
alias assert_sm_transition_not_allowed refute_sm_transition_allowed
|
207
|
+
|
208
|
+
def assert_sm_event_triggers(object, event, message = nil)
|
209
|
+
initial_state = object.state
|
210
|
+
object.send("#{event}!")
|
211
|
+
state_changed = initial_state != object.state
|
212
|
+
default_message = "Expected event #{event} to trigger state change"
|
213
|
+
|
214
|
+
if defined?(::Minitest)
|
215
|
+
assert state_changed, message || default_message
|
216
|
+
elsif defined?(::RSpec)
|
217
|
+
expect(state_changed).to be_truthy, message || default_message
|
218
|
+
else
|
219
|
+
raise default_message unless state_changed
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def refute_sm_event_triggers(object, event, message = nil)
|
224
|
+
initial_state = object.state
|
225
|
+
begin
|
226
|
+
object.send("#{event}!")
|
227
|
+
state_unchanged = initial_state == object.state
|
228
|
+
default_message = "Expected event #{event} to not trigger state change"
|
229
|
+
|
230
|
+
if defined?(::Minitest)
|
231
|
+
assert state_unchanged, message || default_message
|
232
|
+
elsif defined?(::RSpec)
|
233
|
+
expect(state_unchanged).to be_truthy, message || default_message
|
234
|
+
else
|
235
|
+
raise default_message unless state_unchanged
|
236
|
+
end
|
237
|
+
rescue StateMachines::InvalidTransition
|
238
|
+
# Expected behavior - transition was blocked
|
239
|
+
end
|
240
|
+
end
|
241
|
+
alias assert_sm_event_not_triggers refute_sm_event_triggers
|
242
|
+
|
243
|
+
def assert_sm_event_raises_error(object, event, error_class, message = nil)
|
244
|
+
default_message = "Expected event #{event} to raise #{error_class}"
|
245
|
+
|
246
|
+
if defined?(::Minitest)
|
247
|
+
assert_raises(error_class, message || default_message) do
|
248
|
+
object.send("#{event}!")
|
249
|
+
end
|
250
|
+
elsif defined?(::RSpec)
|
251
|
+
expect { object.send("#{event}!") }.to raise_error(error_class), message || default_message
|
252
|
+
else
|
253
|
+
begin
|
254
|
+
object.send("#{event}!")
|
255
|
+
raise default_message
|
256
|
+
rescue error_class
|
257
|
+
# Expected behavior
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def assert_sm_callback_executed(object, callback_name, message = nil)
|
263
|
+
callbacks_executed = object.instance_variable_get(:@_sm_callbacks_executed) || []
|
264
|
+
callback_was_executed = callbacks_executed.include?(callback_name)
|
265
|
+
default_message = "Expected callback #{callback_name} to be executed"
|
266
|
+
|
267
|
+
if defined?(::Minitest)
|
268
|
+
assert callback_was_executed, message || default_message
|
269
|
+
elsif defined?(::RSpec)
|
270
|
+
expect(callback_was_executed).to be_truthy, message || default_message
|
271
|
+
else
|
272
|
+
raise default_message unless callback_was_executed
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def refute_sm_callback_executed(object, callback_name, message = nil)
|
277
|
+
callbacks_executed = object.instance_variable_get(:@_sm_callbacks_executed) || []
|
278
|
+
callback_was_executed = callbacks_executed.include?(callback_name)
|
279
|
+
default_message = "Expected callback #{callback_name} to not be executed"
|
280
|
+
|
281
|
+
if defined?(::Minitest)
|
282
|
+
refute callback_was_executed, message || default_message
|
283
|
+
elsif defined?(::RSpec)
|
284
|
+
expect(callback_was_executed).to be_falsy, message || default_message
|
285
|
+
elsif callback_was_executed
|
286
|
+
raise default_message
|
287
|
+
end
|
288
|
+
end
|
289
|
+
alias assert_sm_callback_not_executed refute_sm_callback_executed
|
290
|
+
|
291
|
+
def assert_sm_state_persisted(record, expected:, message: nil)
|
292
|
+
record.reload if record.respond_to?(:reload)
|
293
|
+
actual_state = record.state
|
294
|
+
default_message = "Expected persisted state #{expected} but got #{actual_state}"
|
295
|
+
|
296
|
+
if defined?(::Minitest)
|
297
|
+
assert_equal expected, actual_state, message || default_message
|
298
|
+
elsif defined?(::RSpec)
|
299
|
+
expect(actual_state).to eq(expected), message || default_message
|
300
|
+
else
|
301
|
+
raise default_message unless expected == actual_state
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
1
5
|
module StateMachines
|
2
6
|
# Represents a collection of transitions in a state machine
|
3
7
|
class TransitionCollection < Array
|
@@ -28,7 +32,7 @@ module StateMachines
|
|
28
32
|
attributes = map { |transition| transition.attribute }.uniq
|
29
33
|
fail ArgumentError, 'Cannot perform multiple transitions in parallel for the same state machine attribute' if attributes.length != length
|
30
34
|
|
31
|
-
|
35
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :actions, :after, :use_transactions)
|
32
36
|
options = {actions: true, after: true, use_transactions: true}.merge(options)
|
33
37
|
@skip_actions = !options[:actions]
|
34
38
|
@skip_after = !options[:after]
|
data/lib/state_machines.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: state_machines
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.20.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
8
8
|
- Aaron Pfeifer
|
9
|
-
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: bundler
|
@@ -56,7 +55,6 @@ dependencies:
|
|
56
55
|
description: Adds support for creating state machines for attributes on any Ruby class
|
57
56
|
email:
|
58
57
|
- terminale@gmail.com
|
59
|
-
- aaron@pluginaweek.org
|
60
58
|
executables: []
|
61
59
|
extensions: []
|
62
60
|
extra_rdoc_files: []
|
@@ -64,7 +62,6 @@ files:
|
|
64
62
|
- LICENSE.txt
|
65
63
|
- README.md
|
66
64
|
- lib/state_machines.rb
|
67
|
-
- lib/state_machines/assertions.rb
|
68
65
|
- lib/state_machines/branch.rb
|
69
66
|
- lib/state_machines/callback.rb
|
70
67
|
- lib/state_machines/core.rb
|
@@ -79,24 +76,30 @@ files:
|
|
79
76
|
- lib/state_machines/integrations.rb
|
80
77
|
- lib/state_machines/integrations/base.rb
|
81
78
|
- lib/state_machines/machine.rb
|
79
|
+
- lib/state_machines/machine/class_methods.rb
|
82
80
|
- lib/state_machines/machine_collection.rb
|
83
81
|
- lib/state_machines/macro_methods.rb
|
84
82
|
- lib/state_machines/matcher.rb
|
85
83
|
- lib/state_machines/matcher_helpers.rb
|
86
84
|
- lib/state_machines/node_collection.rb
|
85
|
+
- lib/state_machines/options_validator.rb
|
87
86
|
- lib/state_machines/path.rb
|
88
87
|
- lib/state_machines/path_collection.rb
|
89
88
|
- lib/state_machines/state.rb
|
90
89
|
- lib/state_machines/state_collection.rb
|
91
90
|
- lib/state_machines/state_context.rb
|
91
|
+
- lib/state_machines/stdio_renderer.rb
|
92
|
+
- lib/state_machines/test_helper.rb
|
92
93
|
- lib/state_machines/transition.rb
|
93
94
|
- lib/state_machines/transition_collection.rb
|
94
95
|
- lib/state_machines/version.rb
|
95
96
|
homepage: https://github.com/state-machines/state_machines
|
96
97
|
licenses:
|
97
98
|
- MIT
|
98
|
-
metadata:
|
99
|
-
|
99
|
+
metadata:
|
100
|
+
changelog_uri: https://github.com/state-machines/state_machines/blob/master/CHANGELOG.md
|
101
|
+
homepage_uri: https://github.com/state-machines/state_machines
|
102
|
+
source_code_uri: https://github.com/state-machines/state_machines
|
100
103
|
rdoc_options: []
|
101
104
|
require_paths:
|
102
105
|
- lib
|
@@ -111,8 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
111
114
|
- !ruby/object:Gem::Version
|
112
115
|
version: '0'
|
113
116
|
requirements: []
|
114
|
-
rubygems_version: 3.
|
115
|
-
signing_key:
|
117
|
+
rubygems_version: 3.6.7
|
116
118
|
specification_version: 4
|
117
119
|
summary: State machines for attributes
|
118
120
|
test_files: []
|
@@ -1,40 +0,0 @@
|
|
1
|
-
class Hash
|
2
|
-
# Provides a set of helper methods for making assertions about the content
|
3
|
-
# of various objects
|
4
|
-
|
5
|
-
unless respond_to?(:assert_valid_keys)
|
6
|
-
# Validate all keys in a hash match <tt>*valid_keys</tt>, raising ArgumentError
|
7
|
-
# on a mismatch. Note that keys are NOT treated indifferently, meaning if you
|
8
|
-
# use strings for keys but assert symbols as keys, this will fail.
|
9
|
-
#
|
10
|
-
# { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
|
11
|
-
# { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
|
12
|
-
# { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
|
13
|
-
# Code from ActiveSupport
|
14
|
-
def assert_valid_keys(*valid_keys)
|
15
|
-
valid_keys.flatten!
|
16
|
-
each_key do |k|
|
17
|
-
unless valid_keys.include?(k)
|
18
|
-
raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
# Validates that the given hash only includes at *most* one of a set of
|
25
|
-
# exclusive keys. If more than one key is found, an ArgumentError will be
|
26
|
-
# raised.
|
27
|
-
#
|
28
|
-
# == Examples
|
29
|
-
#
|
30
|
-
# options = {:only => :on, :except => :off}
|
31
|
-
# options.assert_exclusive_keys(:only) # => nil
|
32
|
-
# options.assert_exclusive_keys(:except) # => nil
|
33
|
-
# options.assert_exclusive_keys(:only, :except) # => ArgumentError: Conflicting keys: only, except
|
34
|
-
# options.assert_exclusive_keys(:only, :except, :with) # => ArgumentError: Conflicting keys: only, except
|
35
|
-
def assert_exclusive_keys(*exclusive_keys)
|
36
|
-
conflicting_keys = exclusive_keys & keys
|
37
|
-
raise ArgumentError, "Conflicting keys: #{conflicting_keys.join(', ')}" unless conflicting_keys.length <= 1
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|