stepper_motor 0.1.17 → 0.1.19
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/CHANGELOG.md +12 -0
- data/lib/stepper_motor/conditional.rb +42 -0
- data/lib/stepper_motor/journey/flow_control.rb +1 -2
- data/lib/stepper_motor/journey.rb +110 -26
- data/lib/stepper_motor/step.rb +3 -19
- data/lib/stepper_motor/test_helper.rb +28 -3
- data/lib/stepper_motor/version.rb +1 -1
- data/lib/stepper_motor.rb +1 -0
- data/manual/MANUAL.md +182 -13
- data/rbi/stepper_motor.rbi +59 -12
- data/sig/stepper_motor.rbs +49 -7
- data/test/stepper_motor/journey/cancel_if_test.rb +293 -0
- data/test/stepper_motor/journey/if_condition_test.rb +1 -1
- data/test/stepper_motor/journey/step_definition_test.rb +1 -1
- data/test/stepper_motor/journey/step_ordering_test.rb +267 -0
- data/test/stepper_motor/test_helper_test.rb +59 -0
- data/test/stepper_motor/wrap_conditional_test.rb +190 -0
- data/test/test_helper.rb +3 -2
- metadata +10 -3
data/rbi/stepper_motor.rbi
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
# StepperMotor is a module for building multi-step flows where steps are sequential and only
|
3
3
|
# ever progress forward. The building block of StepperMotor is StepperMotor::Journey
|
4
4
|
module StepperMotor
|
5
|
-
VERSION = T.let("0.1.
|
5
|
+
VERSION = T.let("0.1.19", T.untyped)
|
6
6
|
PerformStepJobV2 = T.let(StepperMotor::PerformStepJob, T.untyped)
|
7
7
|
RecoverStuckJourneysJobV1 = T.let(StepperMotor::RecoverStuckJourneysJob, T.untyped)
|
8
8
|
|
@@ -24,7 +24,6 @@ module StepperMotor
|
|
24
24
|
# array of the Journey subclass. When the step gets performed, the block passed to the
|
25
25
|
# constructor will be instance_exec'd with the Journey model being the context
|
26
26
|
class Step
|
27
|
-
# sord omit - no YARD type given for "seq:", using untyped
|
28
27
|
# sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
|
29
28
|
# Creates a new step definition
|
30
29
|
#
|
@@ -38,14 +37,13 @@ module StepperMotor
|
|
38
37
|
sig do
|
39
38
|
params(
|
40
39
|
name: T.any(String, Symbol),
|
41
|
-
seq: T.untyped,
|
42
40
|
on_exception: Symbol,
|
43
41
|
wait: T.any(Numeric, ActiveSupport::Duration),
|
44
42
|
skip_if: T.any(TrueClass, FalseClass, NilClass, Symbol, Proc),
|
45
43
|
step_block: T.untyped
|
46
44
|
).void
|
47
45
|
end
|
48
|
-
def initialize(name:,
|
46
|
+
def initialize(name:, on_exception: :pause!, wait: 0, skip_if: false, &step_block); end
|
49
47
|
|
50
48
|
# Checks if the step should be skipped based on the skip_if condition
|
51
49
|
#
|
@@ -72,10 +70,6 @@ module StepperMotor
|
|
72
70
|
sig { returns(T.any(Numeric, ActiveSupport::Duration)) }
|
73
71
|
attr_reader :wait
|
74
72
|
|
75
|
-
# sord omit - no YARD type given for :seq, using untyped
|
76
|
-
sig { returns(T.untyped) }
|
77
|
-
attr_reader :seq
|
78
|
-
|
79
73
|
class MissingDefinition < NoMethodError
|
80
74
|
end
|
81
75
|
end
|
@@ -124,6 +118,10 @@ module StepperMotor
|
|
124
118
|
sig { returns(T.untyped) }
|
125
119
|
def step_definitions; end
|
126
120
|
|
121
|
+
# _@return_ — the cancel_if conditions defined for this journey class
|
122
|
+
sig { returns(T::Array[StepperMotor::Conditional]) }
|
123
|
+
def cancel_if_conditions; end
|
124
|
+
|
127
125
|
# sord duck - #to_f looks like a duck type, replacing with untyped
|
128
126
|
# sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
|
129
127
|
# sord duck - #to_f looks like a duck type, replacing with untyped
|
@@ -135,7 +133,11 @@ module StepperMotor
|
|
135
133
|
#
|
136
134
|
# _@param_ `wait` — the amount of time this step should wait before getting performed. When the journey gets scheduled, the triggering job is going to be delayed by this amount of time, and the `next_step_to_be_performed_at` attribute will be set to the current time plus the wait duration. Mutually exclusive with `after:`
|
137
135
|
#
|
138
|
-
# _@param_ `after` — the amount of time this step should wait before getting performed including all the previous waits. This allows you to set the wait time based on the time after the journey started, as opposed to when the previous step has completed. When the journey gets scheduled, the triggering job is going to be delayed by this amount of time _minus the `wait` values of the preceding steps, and the `next_step_to_be_performed_at` attribute will be set to the current time. The `after` value gets converted into the `wait` value and passed to the step definition. Mutually exclusive with `wait
|
136
|
+
# _@param_ `after` — the amount of time this step should wait before getting performed including all the previous waits. This allows you to set the wait time based on the time after the journey started, as opposed to when the previous step has completed. When the journey gets scheduled, the triggering job is going to be delayed by this amount of time _minus the `wait` values of the preceding steps, and the `next_step_to_be_performed_at` attribute will be set to the current time. The `after` value gets converted into the `wait` value and passed to the step definition. Mutually exclusive with `wait:`.
|
137
|
+
#
|
138
|
+
# _@param_ `before_step` — the name of the step before which this step should be inserted. This allows you to control the order of steps by inserting a step before a specific existing step. The step name can be provided as a string or symbol. Mutually exclusive with `after_step:`.
|
139
|
+
#
|
140
|
+
# _@param_ `after_step` — the name of the step after which this step should be inserted. This allows you to control the order of steps by inserting a step after a specific existing step. The step name can be provided as a string or symbol. Mutually exclusive with `before_step:`.
|
139
141
|
#
|
140
142
|
# _@param_ `on_exception` — See {StepperMotor::Step#on_exception}
|
141
143
|
#
|
@@ -151,11 +153,13 @@ module StepperMotor
|
|
151
153
|
name: T.nilable(String),
|
152
154
|
wait: T.nilable(T.any(Float, T.untyped, ActiveSupport::Duration)),
|
153
155
|
after: T.nilable(T.any(Float, T.untyped, ActiveSupport::Duration)),
|
156
|
+
before_step: T.nilable(T.any(String, Symbol)),
|
157
|
+
after_step: T.nilable(T.any(String, Symbol)),
|
154
158
|
additional_step_definition_options: T::Hash[T.untyped, T.untyped],
|
155
159
|
blk: T.untyped
|
156
160
|
).returns(StepperMotor::Step)
|
157
161
|
end
|
158
|
-
def self.step(name = nil, wait: nil, after: nil, **additional_step_definition_options, &blk); end
|
162
|
+
def self.step(name = nil, wait: nil, after: nil, before_step: nil, after_step: nil, **additional_step_definition_options, &blk); end
|
159
163
|
|
160
164
|
# sord warn - "StepperMotor::Step?" does not appear to be a type
|
161
165
|
# Returns the `Step` object for a named step. This is used when performing a step, but can also
|
@@ -165,6 +169,14 @@ module StepperMotor
|
|
165
169
|
sig { params(by_step_name: T.any(Symbol, String)).returns(SORD_ERROR_StepperMotorStep) }
|
166
170
|
def self.lookup_step_definition(by_step_name); end
|
167
171
|
|
172
|
+
# Returns all step definitions that follow the given step in the journey
|
173
|
+
#
|
174
|
+
# _@param_ `step_definition` — the step to find the following steps for
|
175
|
+
#
|
176
|
+
# _@return_ — the following steps, or empty array if this is the last step
|
177
|
+
sig { params(step_definition: StepperMotor::Step).returns(T::Array[StepperMotor::Step]) }
|
178
|
+
def self.step_definitions_following(step_definition); end
|
179
|
+
|
168
180
|
# sord omit - no YARD type given for "by_step_name", using untyped
|
169
181
|
# sord omit - no YARD return type given, using untyped
|
170
182
|
# Alias for the class method, for brevity
|
@@ -173,6 +185,18 @@ module StepperMotor
|
|
173
185
|
sig { params(by_step_name: T.untyped).returns(T.untyped) }
|
174
186
|
def lookup_step_definition(by_step_name); end
|
175
187
|
|
188
|
+
# Defines a condition that will cause the journey to cancel if satisfied.
|
189
|
+
# This works like Rails' `etag` - it's class-inheritable and appendable.
|
190
|
+
# Multiple `cancel_if` calls can be made to a Journey definition.
|
191
|
+
# All conditions are evaluated after setting the state to `performing`.
|
192
|
+
# If any condition is satisfied, the journey will cancel.
|
193
|
+
#
|
194
|
+
# _@param_ `condition_arg` — the condition to check
|
195
|
+
#
|
196
|
+
# _@param_ `condition_blk` — a block that will be evaluated as a condition
|
197
|
+
sig { params(condition_arg: T.any(TrueClass, FalseClass, Symbol, Proc, T::Array[T.untyped], Conditional), condition_blk: T.untyped).void }
|
198
|
+
def self.cancel_if(condition_arg = :__no_argument_given__, &condition_blk); end
|
199
|
+
|
176
200
|
# Performs the next step in the journey. Will check whether any other process has performed the step already
|
177
201
|
# and whether the record is unchanged, and will then lock it and set the state to 'performimg'.
|
178
202
|
#
|
@@ -359,6 +383,25 @@ module StepperMotor
|
|
359
383
|
class BaseJob < ActiveJob::Base
|
360
384
|
end
|
361
385
|
|
386
|
+
# A wrapper for conditional logic that can be evaluated against an object.
|
387
|
+
# This class encapsulates different types of conditions (booleans, symbols, callables, arrays)
|
388
|
+
# and provides a unified interface for checking if a condition is satisfied by a given object.
|
389
|
+
# It handles negation and ensures proper context when evaluating conditions.
|
390
|
+
class Conditional
|
391
|
+
# sord omit - no YARD type given for "condition", using untyped
|
392
|
+
# sord omit - no YARD type given for "negate:", using untyped
|
393
|
+
sig { params(condition: T.untyped, negate: T.untyped).void }
|
394
|
+
def initialize(condition, negate: false); end
|
395
|
+
|
396
|
+
# sord omit - no YARD type given for "object", using untyped
|
397
|
+
sig { params(object: T.untyped).returns(T::Boolean) }
|
398
|
+
def satisfied_by?(object); end
|
399
|
+
|
400
|
+
# sord omit - no YARD return type given, using untyped
|
401
|
+
sig { returns(T.untyped) }
|
402
|
+
def validate_condition; end
|
403
|
+
end
|
404
|
+
|
362
405
|
module TestHelper
|
363
406
|
# Allows running a given Journey to completion, skipping across the waiting periods.
|
364
407
|
# This is useful to evaluate all side effects of a Journey. The helper will ensure
|
@@ -368,9 +411,13 @@ module StepperMotor
|
|
368
411
|
#
|
369
412
|
# _@param_ `journey` — the journey to speedrun
|
370
413
|
#
|
414
|
+
# _@param_ `time_travel` — whether to use ActiveSupport time travel (default: true) Note: When time_travel is true, this method will permanently travel time forward and will not reset it back to the original time when the method exits.
|
415
|
+
#
|
416
|
+
# _@param_ `maximum_steps` — how many steps can we take until we assume the journey has hung and fail the test. Default value is :reasonable, which is 10x the number of steps. :unlimited allows "a ton", but can make your test hang if your logic lets a step reattempt indefinitely
|
417
|
+
#
|
371
418
|
# _@return_ — void
|
372
|
-
sig { params(journey: StepperMotor::Journey).returns(T.untyped) }
|
373
|
-
def speedrun_journey(journey); end
|
419
|
+
sig { params(journey: StepperMotor::Journey, time_travel: T::Boolean, maximum_steps: T.any(Symbol, Integer)).returns(T.untyped) }
|
420
|
+
def speedrun_journey(journey, time_travel: true, maximum_steps: :reasonable); end
|
374
421
|
|
375
422
|
# Performs the named step of the journey without waiting for the time to perform the step.
|
376
423
|
#
|
data/sig/stepper_motor.rbs
CHANGED
@@ -22,7 +22,6 @@ module StepperMotor
|
|
22
22
|
# array of the Journey subclass. When the step gets performed, the block passed to the
|
23
23
|
# constructor will be instance_exec'd with the Journey model being the context
|
24
24
|
class Step
|
25
|
-
# sord omit - no YARD type given for "seq:", using untyped
|
26
25
|
# sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
|
27
26
|
# Creates a new step definition
|
28
27
|
#
|
@@ -35,7 +34,6 @@ module StepperMotor
|
|
35
34
|
# _@param_ `skip_if` — condition to check before performing the step. If a boolean is provided, it will be used directly. If nil is provided, it will be treated as false. If a symbol is provided, it will call the method on the Journey. If a block is provided, it will be executed with the Journey as context. The step will only be performed if the condition returns a truthy value.
|
36
35
|
def initialize: (
|
37
36
|
name: (String | Symbol),
|
38
|
-
seq: untyped,
|
39
37
|
?on_exception: Symbol,
|
40
38
|
?wait: (Numeric | ActiveSupport::Duration),
|
41
39
|
?skip_if: (TrueClass | FalseClass | NilClass | Symbol | Proc)
|
@@ -62,9 +60,6 @@ module StepperMotor
|
|
62
60
|
# _@return_ — how long to wait before performing the step
|
63
61
|
attr_reader wait: (Numeric | ActiveSupport::Duration)
|
64
62
|
|
65
|
-
# sord omit - no YARD type given for :seq, using untyped
|
66
|
-
attr_reader seq: untyped
|
67
|
-
|
68
63
|
class MissingDefinition < NoMethodError
|
69
64
|
end
|
70
65
|
end
|
@@ -112,6 +107,9 @@ module StepperMotor
|
|
112
107
|
# _@see_ `Journey.step_definitions`
|
113
108
|
def step_definitions: () -> untyped
|
114
109
|
|
110
|
+
# _@return_ — the cancel_if conditions defined for this journey class
|
111
|
+
def cancel_if_conditions: () -> ::Array[StepperMotor::Conditional]
|
112
|
+
|
115
113
|
# sord duck - #to_f looks like a duck type, replacing with untyped
|
116
114
|
# sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
|
117
115
|
# sord duck - #to_f looks like a duck type, replacing with untyped
|
@@ -123,7 +121,11 @@ module StepperMotor
|
|
123
121
|
#
|
124
122
|
# _@param_ `wait` — the amount of time this step should wait before getting performed. When the journey gets scheduled, the triggering job is going to be delayed by this amount of time, and the `next_step_to_be_performed_at` attribute will be set to the current time plus the wait duration. Mutually exclusive with `after:`
|
125
123
|
#
|
126
|
-
# _@param_ `after` — the amount of time this step should wait before getting performed including all the previous waits. This allows you to set the wait time based on the time after the journey started, as opposed to when the previous step has completed. When the journey gets scheduled, the triggering job is going to be delayed by this amount of time _minus the `wait` values of the preceding steps, and the `next_step_to_be_performed_at` attribute will be set to the current time. The `after` value gets converted into the `wait` value and passed to the step definition. Mutually exclusive with `wait
|
124
|
+
# _@param_ `after` — the amount of time this step should wait before getting performed including all the previous waits. This allows you to set the wait time based on the time after the journey started, as opposed to when the previous step has completed. When the journey gets scheduled, the triggering job is going to be delayed by this amount of time _minus the `wait` values of the preceding steps, and the `next_step_to_be_performed_at` attribute will be set to the current time. The `after` value gets converted into the `wait` value and passed to the step definition. Mutually exclusive with `wait:`.
|
125
|
+
#
|
126
|
+
# _@param_ `before_step` — the name of the step before which this step should be inserted. This allows you to control the order of steps by inserting a step before a specific existing step. The step name can be provided as a string or symbol. Mutually exclusive with `after_step:`.
|
127
|
+
#
|
128
|
+
# _@param_ `after_step` — the name of the step after which this step should be inserted. This allows you to control the order of steps by inserting a step after a specific existing step. The step name can be provided as a string or symbol. Mutually exclusive with `before_step:`.
|
127
129
|
#
|
128
130
|
# _@param_ `on_exception` — See {StepperMotor::Step#on_exception}
|
129
131
|
#
|
@@ -138,6 +140,8 @@ module StepperMotor
|
|
138
140
|
?String? name,
|
139
141
|
?wait: (Float | untyped | ActiveSupport::Duration)?,
|
140
142
|
?after: (Float | untyped | ActiveSupport::Duration)?,
|
143
|
+
?before_step: (String | Symbol)?,
|
144
|
+
?after_step: (String | Symbol)?,
|
141
145
|
**::Hash[untyped, untyped] additional_step_definition_options
|
142
146
|
) -> StepperMotor::Step
|
143
147
|
|
@@ -148,6 +152,13 @@ module StepperMotor
|
|
148
152
|
# _@param_ `by_step_name` — the name of the step to find
|
149
153
|
def self.lookup_step_definition: ((Symbol | String) by_step_name) -> SORD_ERROR_StepperMotorStep
|
150
154
|
|
155
|
+
# Returns all step definitions that follow the given step in the journey
|
156
|
+
#
|
157
|
+
# _@param_ `step_definition` — the step to find the following steps for
|
158
|
+
#
|
159
|
+
# _@return_ — the following steps, or empty array if this is the last step
|
160
|
+
def self.step_definitions_following: (StepperMotor::Step step_definition) -> ::Array[StepperMotor::Step]
|
161
|
+
|
151
162
|
# sord omit - no YARD type given for "by_step_name", using untyped
|
152
163
|
# sord omit - no YARD return type given, using untyped
|
153
164
|
# Alias for the class method, for brevity
|
@@ -155,6 +166,17 @@ module StepperMotor
|
|
155
166
|
# _@see_ `Journey.lookup_step_definition`
|
156
167
|
def lookup_step_definition: (untyped by_step_name) -> untyped
|
157
168
|
|
169
|
+
# Defines a condition that will cause the journey to cancel if satisfied.
|
170
|
+
# This works like Rails' `etag` - it's class-inheritable and appendable.
|
171
|
+
# Multiple `cancel_if` calls can be made to a Journey definition.
|
172
|
+
# All conditions are evaluated after setting the state to `performing`.
|
173
|
+
# If any condition is satisfied, the journey will cancel.
|
174
|
+
#
|
175
|
+
# _@param_ `condition_arg` — the condition to check
|
176
|
+
#
|
177
|
+
# _@param_ `condition_blk` — a block that will be evaluated as a condition
|
178
|
+
def self.cancel_if: (?(TrueClass | FalseClass | Symbol | Proc | ::Array[untyped] | Conditional) condition_arg) -> void
|
179
|
+
|
158
180
|
# Performs the next step in the journey. Will check whether any other process has performed the step already
|
159
181
|
# and whether the record is unchanged, and will then lock it and set the state to 'performimg'.
|
160
182
|
#
|
@@ -320,6 +342,22 @@ module StepperMotor
|
|
320
342
|
class BaseJob < ActiveJob::Base
|
321
343
|
end
|
322
344
|
|
345
|
+
# A wrapper for conditional logic that can be evaluated against an object.
|
346
|
+
# This class encapsulates different types of conditions (booleans, symbols, callables, arrays)
|
347
|
+
# and provides a unified interface for checking if a condition is satisfied by a given object.
|
348
|
+
# It handles negation and ensures proper context when evaluating conditions.
|
349
|
+
class Conditional
|
350
|
+
# sord omit - no YARD type given for "condition", using untyped
|
351
|
+
# sord omit - no YARD type given for "negate:", using untyped
|
352
|
+
def initialize: (untyped condition, ?negate: untyped) -> void
|
353
|
+
|
354
|
+
# sord omit - no YARD type given for "object", using untyped
|
355
|
+
def satisfied_by?: (untyped object) -> bool
|
356
|
+
|
357
|
+
# sord omit - no YARD return type given, using untyped
|
358
|
+
def validate_condition: () -> untyped
|
359
|
+
end
|
360
|
+
|
323
361
|
module TestHelper
|
324
362
|
# Allows running a given Journey to completion, skipping across the waiting periods.
|
325
363
|
# This is useful to evaluate all side effects of a Journey. The helper will ensure
|
@@ -329,8 +367,12 @@ module StepperMotor
|
|
329
367
|
#
|
330
368
|
# _@param_ `journey` — the journey to speedrun
|
331
369
|
#
|
370
|
+
# _@param_ `time_travel` — whether to use ActiveSupport time travel (default: true) Note: When time_travel is true, this method will permanently travel time forward and will not reset it back to the original time when the method exits.
|
371
|
+
#
|
372
|
+
# _@param_ `maximum_steps` — how many steps can we take until we assume the journey has hung and fail the test. Default value is :reasonable, which is 10x the number of steps. :unlimited allows "a ton", but can make your test hang if your logic lets a step reattempt indefinitely
|
373
|
+
#
|
332
374
|
# _@return_ — void
|
333
|
-
def speedrun_journey: (StepperMotor::Journey journey) -> untyped
|
375
|
+
def speedrun_journey: (StepperMotor::Journey journey, ?time_travel: bool, ?maximum_steps: (Symbol | Integer)) -> untyped
|
334
376
|
|
335
377
|
# Performs the named step of the journey without waiting for the time to perform the step.
|
336
378
|
#
|
@@ -0,0 +1,293 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class CancelIfTest < ActiveSupport::TestCase
|
6
|
+
include ActiveJob::TestHelper
|
7
|
+
include SideEffects::TestHelper
|
8
|
+
|
9
|
+
test "cancel_if with block condition cancels journey when true" do
|
10
|
+
canceling_journey = create_journey_subclass do
|
11
|
+
cancel_if { true }
|
12
|
+
|
13
|
+
step do
|
14
|
+
SideEffects.touch! "should not run"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
journey = canceling_journey.create!
|
19
|
+
assert journey.ready?
|
20
|
+
|
21
|
+
assert_no_side_effects { journey.perform_next_step! }
|
22
|
+
assert journey.canceled?
|
23
|
+
end
|
24
|
+
|
25
|
+
test "cancel_if with block condition does not cancel journey when false" do
|
26
|
+
canceling_journey = create_journey_subclass do
|
27
|
+
cancel_if { false }
|
28
|
+
|
29
|
+
step do
|
30
|
+
SideEffects.touch! "should run"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
journey = canceling_journey.create!
|
35
|
+
assert journey.ready?
|
36
|
+
|
37
|
+
assert_produced_side_effects("should run") do
|
38
|
+
journey.perform_next_step!
|
39
|
+
end
|
40
|
+
assert journey.finished?
|
41
|
+
end
|
42
|
+
|
43
|
+
test "cancel_if with boolean argument cancels journey when true" do
|
44
|
+
canceling_journey = create_journey_subclass do
|
45
|
+
cancel_if true
|
46
|
+
|
47
|
+
step do
|
48
|
+
SideEffects.touch! "should not run"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
journey = canceling_journey.create!
|
53
|
+
assert journey.ready?
|
54
|
+
|
55
|
+
assert_no_side_effects { journey.perform_next_step! }
|
56
|
+
assert journey.canceled?
|
57
|
+
end
|
58
|
+
|
59
|
+
test "cancel_if with boolean argument does not cancel journey when false" do
|
60
|
+
canceling_journey = create_journey_subclass do
|
61
|
+
cancel_if false
|
62
|
+
|
63
|
+
step do
|
64
|
+
SideEffects.touch! "should run"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
journey = canceling_journey.create!
|
69
|
+
assert journey.ready?
|
70
|
+
|
71
|
+
assert_produced_side_effects("should run") do
|
72
|
+
journey.perform_next_step!
|
73
|
+
end
|
74
|
+
assert journey.finished?
|
75
|
+
end
|
76
|
+
|
77
|
+
test "cancel_if with symbol argument calls method on journey" do
|
78
|
+
canceling_journey = create_journey_subclass do
|
79
|
+
cancel_if :should_cancel?
|
80
|
+
|
81
|
+
step do
|
82
|
+
SideEffects.touch! "should not run"
|
83
|
+
end
|
84
|
+
|
85
|
+
def should_cancel?
|
86
|
+
true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
journey = canceling_journey.create!
|
91
|
+
assert journey.ready?
|
92
|
+
|
93
|
+
assert_no_side_effects { journey.perform_next_step! }
|
94
|
+
assert journey.canceled?
|
95
|
+
end
|
96
|
+
|
97
|
+
test "cancel_if with symbol method call in block" do
|
98
|
+
canceling_journey = create_journey_subclass do
|
99
|
+
cancel_if { should_cancel? }
|
100
|
+
|
101
|
+
step do
|
102
|
+
SideEffects.touch! "should not run"
|
103
|
+
end
|
104
|
+
|
105
|
+
def should_cancel?
|
106
|
+
true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
journey = canceling_journey.create!
|
111
|
+
assert journey.ready?
|
112
|
+
|
113
|
+
assert_no_side_effects { journey.perform_next_step! }
|
114
|
+
assert journey.canceled?
|
115
|
+
end
|
116
|
+
|
117
|
+
test "cancel_if with array argument cancels when all conditions are true" do
|
118
|
+
canceling_journey = create_journey_subclass do
|
119
|
+
cancel_if [true, true]
|
120
|
+
|
121
|
+
step do
|
122
|
+
SideEffects.touch! "should not run"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
journey = canceling_journey.create!
|
127
|
+
assert journey.ready?
|
128
|
+
|
129
|
+
assert_no_side_effects { journey.perform_next_step! }
|
130
|
+
assert journey.canceled?
|
131
|
+
end
|
132
|
+
|
133
|
+
test "cancel_if with array argument does not cancel when any condition is false" do
|
134
|
+
canceling_journey = create_journey_subclass do
|
135
|
+
cancel_if [true, false]
|
136
|
+
|
137
|
+
step do
|
138
|
+
SideEffects.touch! "should run"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
journey = canceling_journey.create!
|
143
|
+
assert journey.ready?
|
144
|
+
|
145
|
+
assert_produced_side_effects("should run") do
|
146
|
+
journey.perform_next_step!
|
147
|
+
end
|
148
|
+
assert journey.finished?
|
149
|
+
end
|
150
|
+
|
151
|
+
test "cancel_if with proc argument evaluates proc in journey context" do
|
152
|
+
proc_condition = -> { true }
|
153
|
+
|
154
|
+
canceling_journey = create_journey_subclass do
|
155
|
+
cancel_if proc_condition
|
156
|
+
|
157
|
+
step do
|
158
|
+
SideEffects.touch! "should not run"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
journey = canceling_journey.create!
|
163
|
+
assert journey.ready?
|
164
|
+
|
165
|
+
assert_no_side_effects { journey.perform_next_step! }
|
166
|
+
assert journey.canceled?
|
167
|
+
end
|
168
|
+
|
169
|
+
test "cancel_if with conditional object works correctly" do
|
170
|
+
conditional = StepperMotor::Conditional.new(true)
|
171
|
+
|
172
|
+
canceling_journey = create_journey_subclass do
|
173
|
+
cancel_if conditional
|
174
|
+
|
175
|
+
step do
|
176
|
+
SideEffects.touch! "should not run"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
journey = canceling_journey.create!
|
181
|
+
assert journey.ready?
|
182
|
+
|
183
|
+
assert_no_side_effects { journey.perform_next_step! }
|
184
|
+
assert journey.canceled?
|
185
|
+
end
|
186
|
+
|
187
|
+
test "cancel_if with nil argument does not cancel journey" do
|
188
|
+
canceling_journey = create_journey_subclass do
|
189
|
+
cancel_if nil
|
190
|
+
|
191
|
+
step do
|
192
|
+
SideEffects.touch! "should run"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
journey = canceling_journey.create!
|
197
|
+
assert journey.ready?
|
198
|
+
|
199
|
+
assert_produced_side_effects("should run") do
|
200
|
+
journey.perform_next_step!
|
201
|
+
end
|
202
|
+
assert journey.finished?
|
203
|
+
end
|
204
|
+
|
205
|
+
test "multiple cancel_if calls are all evaluated" do
|
206
|
+
canceling_journey = create_journey_subclass do
|
207
|
+
cancel_if false
|
208
|
+
cancel_if true
|
209
|
+
|
210
|
+
step do
|
211
|
+
SideEffects.touch! "should not run"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
journey = canceling_journey.create!
|
216
|
+
assert journey.ready?
|
217
|
+
|
218
|
+
assert_no_side_effects { journey.perform_next_step! }
|
219
|
+
assert journey.canceled?
|
220
|
+
end
|
221
|
+
|
222
|
+
test "cancel_if conditions are evaluated after setting state to performing" do
|
223
|
+
canceling_journey = create_journey_subclass do
|
224
|
+
cancel_if { performing? }
|
225
|
+
|
226
|
+
step do
|
227
|
+
SideEffects.touch! "should not run"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
journey = canceling_journey.create!
|
232
|
+
assert journey.ready?
|
233
|
+
|
234
|
+
assert_no_side_effects { journey.perform_next_step! }
|
235
|
+
assert journey.canceled?
|
236
|
+
end
|
237
|
+
|
238
|
+
test "cancel_if with no arguments and no block raises error" do
|
239
|
+
assert_raises(ArgumentError, "cancel_if requires either a condition argument or a block") do
|
240
|
+
create_journey_subclass do
|
241
|
+
cancel_if
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
test "cancel_if with both argument and block raises error" do
|
247
|
+
assert_raises(ArgumentError, "cancel_if accepts either a condition argument or a block, but not both") do
|
248
|
+
create_journey_subclass do
|
249
|
+
cancel_if true do
|
250
|
+
false
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
test "cancel_if conditions are class-inheritable" do
|
257
|
+
parent_journey = create_journey_subclass do
|
258
|
+
cancel_if true
|
259
|
+
end
|
260
|
+
|
261
|
+
child_journey = create_journey_subclass(parent_journey) do
|
262
|
+
step do
|
263
|
+
SideEffects.touch! "should not run"
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
journey = child_journey.create!
|
268
|
+
assert journey.ready?
|
269
|
+
|
270
|
+
assert_no_side_effects { journey.perform_next_step! }
|
271
|
+
assert journey.canceled?
|
272
|
+
end
|
273
|
+
|
274
|
+
test "cancel_if conditions are appendable in subclasses" do
|
275
|
+
parent_journey = create_journey_subclass do
|
276
|
+
cancel_if false
|
277
|
+
end
|
278
|
+
|
279
|
+
child_journey = create_journey_subclass(parent_journey) do
|
280
|
+
cancel_if true
|
281
|
+
|
282
|
+
step do
|
283
|
+
SideEffects.touch! "should not run"
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
journey = child_journey.create!
|
288
|
+
assert journey.ready?
|
289
|
+
|
290
|
+
assert_no_side_effects { journey.perform_next_step! }
|
291
|
+
assert journey.canceled?
|
292
|
+
end
|
293
|
+
end
|
@@ -255,7 +255,7 @@ class IfConditionTest < ActiveSupport::TestCase
|
|
255
255
|
end
|
256
256
|
|
257
257
|
test "passes skip_if: parameter to step definition" do
|
258
|
-
step_def = StepperMotor::Step.new(name: "a_step",
|
258
|
+
step_def = StepperMotor::Step.new(name: "a_step", on_exception: :reattempt!)
|
259
259
|
assert_skip_if_parameter = ->(**options) {
|
260
260
|
assert options.key?(:skip_if)
|
261
261
|
assert_equal :test_condition, options[:skip_if]
|
@@ -17,7 +17,7 @@ class StepDefinitionTest < ActiveSupport::TestCase
|
|
17
17
|
end
|
18
18
|
|
19
19
|
test "passes any additional options to the step definition" do
|
20
|
-
step_def = StepperMotor::Step.new(name: "a_step",
|
20
|
+
step_def = StepperMotor::Step.new(name: "a_step", on_exception: :reattempt!)
|
21
21
|
assert_extra_arguments = ->(**options) {
|
22
22
|
assert options.key?(:extra)
|
23
23
|
# Return the original definition
|