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.
@@ -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.17", T.untyped)
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:, seq:, on_exception: :pause!, wait: 0, skip_if: false, &step_block); end
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
  #
@@ -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", seq: 1, on_exception: :reattempt!)
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", seq: 1, on_exception: :reattempt!)
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