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.
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class StepOrderingTest < ActiveSupport::TestCase
6
+ include ActiveJob::TestHelper
7
+ include SideEffects::TestHelper
8
+ include StepperMotor::TestHelper
9
+
10
+ test "allows inserting step before another step using string" do
11
+ journey_class = create_journey_subclass do
12
+ step :first do
13
+ # noop
14
+ end
15
+ step :third do
16
+ # noop
17
+ end
18
+ step :second, before_step: "first" do
19
+ # noop
20
+ end
21
+ end
22
+
23
+ assert_equal ["second", "first", "third"], journey_class.step_definitions.map(&:name)
24
+ end
25
+
26
+ test "allows inserting step before another step using symbol" do
27
+ journey_class = create_journey_subclass do
28
+ step :first do
29
+ # noop
30
+ end
31
+ step :third do
32
+ # noop
33
+ end
34
+ step :second, before_step: :first do
35
+ # noop
36
+ end
37
+ end
38
+
39
+ assert_equal ["second", "first", "third"], journey_class.step_definitions.map(&:name)
40
+ end
41
+
42
+ test "allows inserting step after another step using string" do
43
+ journey_class = create_journey_subclass do
44
+ step :first do
45
+ # noop
46
+ end
47
+ step :third do
48
+ # noop
49
+ end
50
+ step :second, after_step: "first" do
51
+ # noop
52
+ end
53
+ end
54
+
55
+ assert_equal ["first", "second", "third"], journey_class.step_definitions.map(&:name)
56
+ end
57
+
58
+ test "allows inserting step after another step using symbol" do
59
+ journey_class = create_journey_subclass do
60
+ step :first do
61
+ # noop
62
+ end
63
+ step :third do
64
+ # noop
65
+ end
66
+ step :second, after_step: :first do
67
+ # noop
68
+ end
69
+ end
70
+
71
+ assert_equal ["first", "second", "third"], journey_class.step_definitions.map(&:name)
72
+ end
73
+
74
+ test "allows inserting step at the beginning using before_step" do
75
+ journey_class = create_journey_subclass do
76
+ step :second do
77
+ # noop
78
+ end
79
+ step :third do
80
+ # noop
81
+ end
82
+ step :first, before_step: "second" do
83
+ # noop
84
+ end
85
+ end
86
+
87
+ assert_equal ["first", "second", "third"], journey_class.step_definitions.map(&:name)
88
+ end
89
+
90
+ test "allows inserting step at the end using after_step" do
91
+ journey_class = create_journey_subclass do
92
+ step :first do
93
+ # noop
94
+ end
95
+ step :second do
96
+ # noop
97
+ end
98
+ step :third, after_step: "second" do
99
+ # noop
100
+ end
101
+ end
102
+
103
+ assert_equal ["first", "second", "third"], journey_class.step_definitions.map(&:name)
104
+ end
105
+
106
+ test "allows complex step ordering with multiple insertions" do
107
+ journey_class = create_journey_subclass do
108
+ step :step_1 do
109
+ # noop
110
+ end
111
+ step :step_4 do
112
+ # noop
113
+ end
114
+ step :step_2, after_step: "step_1" do
115
+ # noop
116
+ end
117
+ step :step_3, before_step: "step_4" do
118
+ # noop
119
+ end
120
+ end
121
+
122
+ assert_equal ["step_1", "step_2", "step_3", "step_4"], journey_class.step_definitions.map(&:name)
123
+ end
124
+
125
+ test "raises error when both before_step and after_step are specified" do
126
+ assert_raises(StepperMotor::StepConfigurationError, "Either before_step: or after_step: can be specified, but not both") do
127
+ create_journey_subclass do
128
+ step :first do
129
+ # noop
130
+ end
131
+ step :second, before_step: "first", after_step: "first" do
132
+ # noop
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ test "raises error when before_step references non-existent step" do
139
+ assert_raises(StepperMotor::StepConfigurationError, "Step named \"nonexistent\" not found for before_step: parameter") do
140
+ create_journey_subclass do
141
+ step :first, before_step: "nonexistent" do
142
+ # noop
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ test "raises error when after_step references non-existent step" do
149
+ assert_raises(StepperMotor::StepConfigurationError, "Step named \"nonexistent\" not found for after_step: parameter") do
150
+ create_journey_subclass do
151
+ step :first, after_step: "nonexistent" do
152
+ # noop
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ test "maintains existing after: timing functionality" do
159
+ journey_class = create_journey_subclass do
160
+ step :first, after: 5.minutes do
161
+ # noop
162
+ end
163
+ step :second, after: 10.minutes do
164
+ # noop
165
+ end
166
+ end
167
+
168
+ assert_equal ["first", "second"], journey_class.step_definitions.map(&:name)
169
+ assert_equal 5.minutes, journey_class.step_definitions[0].wait
170
+ assert_equal 5.minutes, journey_class.step_definitions[1].wait
171
+ end
172
+
173
+ test "allows mixing step ordering with timing" do
174
+ journey_class = create_journey_subclass do
175
+ step :first, wait: 1.minute do
176
+ # noop
177
+ end
178
+ step :third, after_step: "first" do
179
+ # noop
180
+ end
181
+ step :second, before_step: "third", wait: 2.minutes do
182
+ # noop
183
+ end
184
+ end
185
+
186
+ assert_equal ["first", "second", "third"], journey_class.step_definitions.map(&:name)
187
+ assert_equal 1.minute, journey_class.step_definitions[0].wait
188
+ assert_equal 2.minutes, journey_class.step_definitions[1].wait
189
+ assert_equal 0, journey_class.step_definitions[2].wait
190
+ end
191
+
192
+ test "allows inserting step with method name" do
193
+ journey_class = create_journey_subclass do
194
+ step :first do
195
+ # noop
196
+ end
197
+ step :third do
198
+ # noop
199
+ end
200
+ step :second, after_step: "first"
201
+
202
+ def second
203
+ # noop
204
+ end
205
+ end
206
+
207
+ assert_equal ["first", "second", "third"], journey_class.step_definitions.map(&:name)
208
+ end
209
+
210
+ test "allows inserting step with automatic name generation" do
211
+ journey_class = create_journey_subclass do
212
+ step :first do
213
+ # noop
214
+ end
215
+ step :third do
216
+ # noop
217
+ end
218
+ step before_step: "third" do
219
+ # noop
220
+ end
221
+ end
222
+
223
+ assert_equal ["first", "step_3", "third"], journey_class.step_definitions.map(&:name)
224
+ end
225
+
226
+ test "allows inserting step with additional options" do
227
+ journey_class = create_journey_subclass do
228
+ step :first do
229
+ # noop
230
+ end
231
+ step :third do
232
+ # noop
233
+ end
234
+ step :second, after_step: "first", on_exception: :skip! do
235
+ # noop
236
+ end
237
+ end
238
+
239
+ assert_equal ["first", "second", "third"], journey_class.step_definitions.map(&:name)
240
+ assert_equal :skip!, journey_class.step_definitions[1].instance_variable_get(:@on_exception)
241
+ end
242
+
243
+ test "allows inserting steps before or after steps defined in superclass" do
244
+ parent_class = create_journey_subclass do
245
+ step :parent_first do
246
+ # noop
247
+ end
248
+ step :parent_last do
249
+ # noop
250
+ end
251
+ end
252
+
253
+ child_class = create_journey_subclass(parent_class) do
254
+ step :child_before, before_step: "parent_first" do
255
+ # noop
256
+ end
257
+ step :child_after, after_step: "parent_last" do
258
+ # noop
259
+ end
260
+ step :child_middle, after_step: "parent_first" do
261
+ # noop
262
+ end
263
+ end
264
+
265
+ assert_equal ["child_before", "parent_first", "child_middle", "parent_last", "child_after"], child_class.step_definitions.map(&:name)
266
+ end
267
+ end
@@ -22,6 +22,16 @@ class TestHelperTest < ActiveSupport::TestCase
22
22
  end
23
23
  end
24
24
 
25
+ def infinite_journey_class
26
+ create_journey_subclass do
27
+ step :step_1 do
28
+ SideEffects.touch!("step_1")
29
+ # This step never finishes, causing infinite loop
30
+ reattempt!
31
+ end
32
+ end
33
+ end
34
+
25
35
  test "speedruns the journey despite waits being configured" do
26
36
  journey = speedy_journey_class.create!
27
37
  assert journey.ready?
@@ -33,6 +43,36 @@ class TestHelperTest < ActiveSupport::TestCase
33
43
  assert SideEffects.produced?("step_3")
34
44
  end
35
45
 
46
+ test "speedruns the journey with time travel by default" do
47
+ journey = speedy_journey_class.create!
48
+ assert journey.ready?
49
+
50
+ original_time = Time.current
51
+ SideEffects.clear!
52
+ speedrun_journey(journey)
53
+ assert SideEffects.produced?("step_1")
54
+ assert SideEffects.produced?("step_2")
55
+ assert SideEffects.produced?("step_3")
56
+
57
+ # Calculate expected time difference: 40 minutes + 2 days + 1 second buffer per step
58
+ expected_time_difference = 40.minutes + 2.days + 3.seconds
59
+
60
+ # Verify that time has traveled forward by approximately the expected amount
61
+ # (allowing for small execution time differences)
62
+ assert_in_delta expected_time_difference, Time.current - original_time, 1.second
63
+ end
64
+
65
+ test "speedruns the journey without time travel when specified" do
66
+ journey = speedy_journey_class.create!
67
+ assert journey.ready?
68
+
69
+ SideEffects.clear!
70
+ speedrun_journey(journey, time_travel: false)
71
+ assert SideEffects.produced?("step_1")
72
+ assert SideEffects.produced?("step_2")
73
+ assert SideEffects.produced?("step_3")
74
+ end
75
+
36
76
  test "is able to perform a single step forcibly" do
37
77
  journey = speedy_journey_class.create!
38
78
  assert journey.ready?
@@ -41,4 +81,23 @@ class TestHelperTest < ActiveSupport::TestCase
41
81
  immediately_perform_single_step(journey, :step_2)
42
82
  assert SideEffects.produced?("step_2")
43
83
  end
84
+
85
+ test "fails when maximum_steps limit is exceeded" do
86
+ journey = infinite_journey_class.create!
87
+ assert journey.ready?
88
+
89
+ SideEffects.clear!
90
+
91
+ # This should raise an exception because the journey will try to perform more than 2 steps
92
+ # but the infinite loop in step_1 will never finish
93
+ error = assert_raises(RuntimeError) do
94
+ speedrun_journey(journey, maximum_steps: 2)
95
+ end
96
+
97
+ # Verify the error message indicates the journey didn't finish after the maximum steps
98
+ assert_match(/did not finish or cancel after performing 2 steps/, error.message)
99
+
100
+ # Verify that the step was executed (at least once)
101
+ assert SideEffects.produced?("step_1")
102
+ end
44
103
  end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class WrapConditionalTest < ActiveSupport::TestCase
6
+ # Validate the skip_if condition
7
+ # if ![true, false, nil].include?(@skip_if_condition) && !@skip_if_condition.is_a?(Symbol) && !@skip_if_condition.respond_to?(:call)
8
+ # raise ArgumentError, "skip_if: condition must be a boolean, nil, Symbol or a callable object, but was a #{@skip_if_condition.inspect}"
9
+ # end
10
+
11
+ test "wraps true without negate" do
12
+ conditional = StepperMotor::Conditional.new(true)
13
+ assert_equal true, conditional.satisfied_by?(nil)
14
+ assert_equal true, conditional.satisfied_by?("something")
15
+ end
16
+
17
+ test "wraps true with negate" do
18
+ conditional = StepperMotor::Conditional.new(true, negate: true)
19
+ assert_equal false, conditional.satisfied_by?(nil)
20
+ assert_equal false, conditional.satisfied_by?("something")
21
+ end
22
+
23
+ test "wraps false without negate" do
24
+ conditional = StepperMotor::Conditional.new(false)
25
+ assert_equal false, conditional.satisfied_by?(nil)
26
+ assert_equal false, conditional.satisfied_by?("something")
27
+ end
28
+
29
+ test "wraps false with negate" do
30
+ conditional = StepperMotor::Conditional.new(false, negate: true)
31
+ assert_equal true, conditional.satisfied_by?(nil)
32
+ assert_equal true, conditional.satisfied_by?("something")
33
+ end
34
+
35
+ test "wraps nil without negate" do
36
+ conditional = StepperMotor::Conditional.new(nil)
37
+ assert_equal false, conditional.satisfied_by?(nil)
38
+ assert_equal false, conditional.satisfied_by?("something")
39
+ end
40
+
41
+ test "wraps nil with negate" do
42
+ conditional = StepperMotor::Conditional.new(nil, negate: true)
43
+ assert_equal true, conditional.satisfied_by?(nil)
44
+ assert_equal true, conditional.satisfied_by?("something")
45
+ end
46
+
47
+ test "wraps multiple without negate" do
48
+ conditional = StepperMotor::Conditional.new([true, true])
49
+ assert_equal true, conditional.satisfied_by?(nil)
50
+
51
+ conditional = StepperMotor::Conditional.new([true, false])
52
+ assert_equal false, conditional.satisfied_by?(nil)
53
+
54
+ conditional = StepperMotor::Conditional.new([false, false])
55
+ assert_equal false, conditional.satisfied_by?(nil)
56
+ end
57
+
58
+ test "wraps multiple with proc" do
59
+ conditional = StepperMotor::Conditional.new([true, -> { true }])
60
+ assert_equal true, conditional.satisfied_by?(nil)
61
+
62
+ conditional = StepperMotor::Conditional.new([true, -> { false }])
63
+ assert_equal false, conditional.satisfied_by?(nil)
64
+ end
65
+
66
+ test "wraps multiple with negate" do
67
+ conditional = StepperMotor::Conditional.new([true, true], negate: true)
68
+ assert_equal false, conditional.satisfied_by?(nil)
69
+
70
+ conditional = StepperMotor::Conditional.new([true, false], negate: true)
71
+ assert_equal true, conditional.satisfied_by?(nil)
72
+
73
+ conditional = StepperMotor::Conditional.new([false, false], negate: true)
74
+ assert_equal true, conditional.satisfied_by?(nil)
75
+ end
76
+
77
+ test "wraps callable without negate" do
78
+ conditional = StepperMotor::Conditional.new(-> { :foo })
79
+ assert_equal true, conditional.satisfied_by?(nil)
80
+
81
+ conditional = StepperMotor::Conditional.new(-> {})
82
+ assert_equal false, conditional.satisfied_by?(nil)
83
+
84
+ conditional = StepperMotor::Conditional.new(-> { false })
85
+ assert_equal false, conditional.satisfied_by?(nil)
86
+
87
+ conditional = StepperMotor::Conditional.new(-> { true })
88
+ assert_equal true, conditional.satisfied_by?(nil)
89
+ end
90
+
91
+ test "wraps callable with negate" do
92
+ conditional = StepperMotor::Conditional.new(-> { :foo }, negate: true)
93
+ assert_equal false, conditional.satisfied_by?(nil)
94
+
95
+ conditional = StepperMotor::Conditional.new(-> {}, negate: true)
96
+ assert_equal true, conditional.satisfied_by?(nil)
97
+
98
+ conditional = StepperMotor::Conditional.new(-> { false }, negate: true)
99
+ assert_equal true, conditional.satisfied_by?(nil)
100
+
101
+ conditional = StepperMotor::Conditional.new(-> { true }, negate: true)
102
+ assert_equal false, conditional.satisfied_by?(nil)
103
+ end
104
+
105
+ class Doer
106
+ def should_do?
107
+ true
108
+ end
109
+ end
110
+
111
+ class NotDoer
112
+ def should_do?
113
+ false
114
+ end
115
+ end
116
+
117
+ test "wraps method without negate" do
118
+ doer = Doer.new
119
+
120
+ conditional = StepperMotor::Conditional.new(:should_do?)
121
+ assert_equal true, conditional.satisfied_by?(doer)
122
+
123
+ not_doer = NotDoer.new
124
+ assert_equal false, conditional.satisfied_by?(not_doer)
125
+ end
126
+
127
+ test "wraps method with negate" do
128
+ doer = Doer.new
129
+
130
+ conditional = StepperMotor::Conditional.new(:should_do?, negate: true)
131
+ assert_equal false, conditional.satisfied_by?(doer)
132
+
133
+ not_doer = NotDoer.new
134
+ assert_equal true, conditional.satisfied_by?(not_doer)
135
+ end
136
+
137
+ test "wraps conditional without negate" do
138
+ inner_conditional = StepperMotor::Conditional.new(true)
139
+ outer_conditional = StepperMotor::Conditional.new(inner_conditional)
140
+
141
+ assert_equal true, outer_conditional.satisfied_by?(nil)
142
+ assert_equal true, outer_conditional.satisfied_by?("something")
143
+ end
144
+
145
+ test "wraps conditional with negate" do
146
+ inner_conditional = StepperMotor::Conditional.new(true)
147
+ outer_conditional = StepperMotor::Conditional.new(inner_conditional, negate: true)
148
+
149
+ assert_equal false, outer_conditional.satisfied_by?(nil)
150
+ assert_equal false, outer_conditional.satisfied_by?("something")
151
+ end
152
+
153
+ test "wraps negated conditional without negate" do
154
+ inner_conditional = StepperMotor::Conditional.new(true, negate: true)
155
+ outer_conditional = StepperMotor::Conditional.new(inner_conditional)
156
+
157
+ assert_equal false, outer_conditional.satisfied_by?(nil)
158
+ assert_equal false, outer_conditional.satisfied_by?("something")
159
+ end
160
+
161
+ test "wraps negated conditional with negate" do
162
+ inner_conditional = StepperMotor::Conditional.new(true, negate: true)
163
+ outer_conditional = StepperMotor::Conditional.new(inner_conditional, negate: true)
164
+
165
+ assert_equal true, outer_conditional.satisfied_by?(nil)
166
+ assert_equal true, outer_conditional.satisfied_by?("something")
167
+ end
168
+
169
+ test "wraps conditional with method" do
170
+ doer = Doer.new
171
+ inner_conditional = StepperMotor::Conditional.new(:should_do?)
172
+ outer_conditional = StepperMotor::Conditional.new(inner_conditional)
173
+
174
+ assert_equal true, outer_conditional.satisfied_by?(doer)
175
+
176
+ not_doer = NotDoer.new
177
+ assert_equal false, outer_conditional.satisfied_by?(not_doer)
178
+ end
179
+
180
+ test "wraps conditional with method and negate" do
181
+ doer = Doer.new
182
+ inner_conditional = StepperMotor::Conditional.new(:should_do?)
183
+ outer_conditional = StepperMotor::Conditional.new(inner_conditional, negate: true)
184
+
185
+ assert_equal false, outer_conditional.satisfied_by?(doer)
186
+
187
+ not_doer = NotDoer.new
188
+ assert_equal true, outer_conditional.satisfied_by?(not_doer)
189
+ end
190
+ end
data/test/test_helper.rb CHANGED
@@ -32,11 +32,12 @@ module JourneyDefinitionHelper
32
32
  super
33
33
  end
34
34
 
35
- def create_journey_subclass(&blk)
35
+ def create_journey_subclass(parent_class = nil, &blk)
36
36
  # https://stackoverflow.com/questions/4113479/dynamic-class-definition-with-a-class-name
37
37
  random_component = @class_names_rng.hex(8)
38
38
  random_name = "JourneySubclass_#{random_component}"
39
- klass = Class.new(StepperMotor::Journey, &blk)
39
+ parent_class ||= StepperMotor::Journey
40
+ klass = Class.new(parent_class, &blk)
40
41
  Object.const_set(random_name, klass)
41
42
  @dynamic_class_names << random_name
42
43
  klass
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stepper_motor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.17
4
+ version: 0.1.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 2025-06-20 00:00:00.000000000 Z
11
+ date: 2025-06-25 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: activerecord
@@ -236,6 +237,7 @@ files:
236
237
  - lib/generators/stepper_motor_migration_005.rb.erb
237
238
  - lib/stepper_motor.rb
238
239
  - lib/stepper_motor/base_job.rb
240
+ - lib/stepper_motor/conditional.rb
239
241
  - lib/stepper_motor/cyclic_scheduler.rb
240
242
  - lib/stepper_motor/delete_completed_journeys_job.rb
241
243
  - lib/stepper_motor/forward_scheduler.rb
@@ -309,17 +311,20 @@ files:
309
311
  - test/stepper_motor/cyclic_scheduler_test.rb
310
312
  - test/stepper_motor/forward_scheduler_test.rb
311
313
  - test/stepper_motor/housekeeping_job_test.rb
314
+ - test/stepper_motor/journey/cancel_if_test.rb
312
315
  - test/stepper_motor/journey/exception_handling_test.rb
313
316
  - test/stepper_motor/journey/flow_control_test.rb
314
317
  - test/stepper_motor/journey/idempotency_test.rb
315
318
  - test/stepper_motor/journey/if_condition_test.rb
316
319
  - test/stepper_motor/journey/step_definition_test.rb
320
+ - test/stepper_motor/journey/step_ordering_test.rb
317
321
  - test/stepper_motor/journey/uniqueness_test.rb
318
322
  - test/stepper_motor/journey_test.rb
319
323
  - test/stepper_motor/perform_step_job_test.rb
320
324
  - test/stepper_motor/recover_stuck_journeys_job_test.rb
321
325
  - test/stepper_motor/recovery_test.rb
322
326
  - test/stepper_motor/test_helper_test.rb
327
+ - test/stepper_motor/wrap_conditional_test.rb
323
328
  - test/stepper_motor_test.rb
324
329
  - test/test_helper.rb
325
330
  homepage: https://steppermotor.dev
@@ -330,6 +335,7 @@ metadata:
330
335
  homepage_uri: https://steppermotor.dev
331
336
  source_code_uri: https://github.com/stepper-motor/stepper_motor
332
337
  changelog_uri: https://github.com/stepper-motor/stepper_motor/blob/main/CHANGELOG.md
338
+ post_install_message:
333
339
  rdoc_options: []
334
340
  require_paths:
335
341
  - lib
@@ -344,7 +350,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
344
350
  - !ruby/object:Gem::Version
345
351
  version: '0'
346
352
  requirements: []
347
- rubygems_version: 3.6.6
353
+ rubygems_version: 3.4.10
354
+ signing_key:
348
355
  specification_version: 4
349
356
  summary: Effortless step workflows that embed nicely inside Rails
350
357
  test_files: []