stepper_motor 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +12 -0
- data/.github/workflows/ci.yml +51 -0
- data/CHANGELOG.md +77 -2
- data/Gemfile +11 -0
- data/README.md +13 -374
- data/Rakefile +21 -3
- data/bin/test +5 -0
- data/lib/generators/install_generator.rb +6 -1
- data/lib/generators/stepper_motor_migration_003.rb.erb +6 -0
- data/lib/generators/stepper_motor_migration_004.rb.erb +26 -0
- data/lib/stepper_motor/forward_scheduler.rb +8 -4
- data/lib/stepper_motor/journey/flow_control.rb +58 -0
- data/lib/stepper_motor/journey/recovery.rb +34 -0
- data/lib/stepper_motor/journey.rb +85 -84
- data/lib/stepper_motor/perform_step_job_v2.rb +2 -2
- data/lib/stepper_motor/railtie.rb +1 -1
- data/lib/stepper_motor/recover_stuck_journeys_job_v1.rb +3 -1
- data/lib/stepper_motor/step.rb +70 -5
- data/lib/stepper_motor/version.rb +1 -1
- data/lib/stepper_motor.rb +1 -2
- data/lib/tasks/stepper_motor_tasks.rake +8 -0
- data/manual/MANUAL.md +538 -0
- data/rbi/stepper_motor.rbi +459 -0
- data/sig/stepper_motor.rbs +406 -3
- data/stepper_motor.gemspec +49 -0
- data/test/dummy/Rakefile +8 -0
- data/test/dummy/app/assets/stylesheets/application.css +1 -0
- data/test/dummy/app/controllers/application_controller.rb +6 -0
- data/test/dummy/app/helpers/application_helper.rb +4 -0
- data/test/dummy/app/jobs/application_job.rb +9 -0
- data/test/dummy/app/mailers/application_mailer.rb +6 -0
- data/test/dummy/app/models/application_record.rb +5 -0
- data/test/dummy/app/views/layouts/application.html.erb +27 -0
- data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/test/dummy/app/views/pwa/manifest.json.erb +22 -0
- data/test/dummy/app/views/pwa/service-worker.js +26 -0
- data/test/dummy/bin/dev +2 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +34 -0
- data/test/dummy/config/application.rb +28 -0
- data/test/dummy/config/boot.rb +7 -0
- data/test/dummy/config/cable.yml +10 -0
- data/test/dummy/config/database.yml +32 -0
- data/test/dummy/config/environment.rb +7 -0
- data/test/dummy/config/environments/development.rb +71 -0
- data/test/dummy/config/environments/production.rb +91 -0
- data/test/dummy/config/environments/test.rb +55 -0
- data/test/dummy/config/initializers/content_security_policy.rb +27 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +10 -0
- data/test/dummy/config/initializers/inflections.rb +18 -0
- data/test/dummy/config/initializers/stepper_motor.rb +3 -0
- data/test/dummy/config/locales/en.yml +31 -0
- data/test/dummy/config/puma.rb +40 -0
- data/test/dummy/config/routes.rb +16 -0
- data/test/dummy/config/storage.yml +34 -0
- data/test/dummy/config.ru +8 -0
- data/test/dummy/db/migrate/20250520094921_stepper_motor_migration_001.rb +38 -0
- data/test/dummy/db/migrate/20250520094922_stepper_motor_migration_002.rb +8 -0
- data/test/dummy/db/migrate/20250522212312_stepper_motor_migration_003.rb +7 -0
- data/test/dummy/db/migrate/20250525110812_stepper_motor_migration_004.rb +28 -0
- data/test/dummy/db/schema.rb +37 -0
- data/test/dummy/public/400.html +114 -0
- data/test/dummy/public/404.html +114 -0
- data/test/dummy/public/406-unsupported-browser.html +114 -0
- data/test/dummy/public/422.html +114 -0
- data/test/dummy/public/500.html +114 -0
- data/test/dummy/public/icon.png +0 -0
- data/test/dummy/public/icon.svg +3 -0
- data/test/side_effects_helper.rb +67 -0
- data/test/stepper_motor/cyclic_scheduler_test.rb +77 -0
- data/{spec/stepper_motor/forward_scheduler_spec.rb → test/stepper_motor/forward_scheduler_test.rb} +9 -10
- data/test/stepper_motor/journey/exception_handling_test.rb +89 -0
- data/test/stepper_motor/journey/flow_control_test.rb +78 -0
- data/test/stepper_motor/journey/idempotency_test.rb +65 -0
- data/test/stepper_motor/journey/step_definition_test.rb +187 -0
- data/test/stepper_motor/journey/uniqueness_test.rb +48 -0
- data/test/stepper_motor/journey_test.rb +352 -0
- data/{spec/stepper_motor/recover_stuck_journeys_job_spec.rb → test/stepper_motor/recover_stuck_journeys_job_test.rb} +14 -14
- data/{spec/stepper_motor/recovery_spec.rb → test/stepper_motor/recovery_test.rb} +27 -27
- data/test/stepper_motor/test_helper_test.rb +44 -0
- data/test/stepper_motor_test.rb +9 -0
- data/test/test_helper.rb +46 -0
- metadata +120 -24
- data/.rspec +0 -3
- data/.ruby-version +0 -1
- data/.standard.yml +0 -4
- data/.yardopts +0 -1
- data/spec/helpers/side_effects.rb +0 -85
- data/spec/spec_helper.rb +0 -90
- data/spec/stepper_motor/cyclic_scheduler_spec.rb +0 -68
- data/spec/stepper_motor/generator_spec.rb +0 -16
- data/spec/stepper_motor/journey_spec.rb +0 -401
- data/spec/stepper_motor/test_helper_spec.rb +0 -48
- data/spec/stepper_motor_spec.rb +0 -7
@@ -0,0 +1,352 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class JourneyTest < ActiveSupport::TestCase
|
6
|
+
include ActiveJob::TestHelper
|
7
|
+
include SideEffects::TestHelper
|
8
|
+
|
9
|
+
test "allows an empty journey to be defined and performed to completion" do
|
10
|
+
pointless_class = create_journey_subclass
|
11
|
+
journey = pointless_class.create!
|
12
|
+
journey.perform_next_step!
|
13
|
+
assert journey.finished?
|
14
|
+
end
|
15
|
+
|
16
|
+
test "allows a journey consisting of one step to be defined and performed to completion" do
|
17
|
+
single_step_class = create_journey_subclass do
|
18
|
+
step :do_thing do
|
19
|
+
SideEffects.touch!("do_thing")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
journey = single_step_class.create!
|
24
|
+
assert_not_nil journey.next_step_to_be_performed_at
|
25
|
+
journey.perform_next_step!
|
26
|
+
assert journey.finished?
|
27
|
+
assert SideEffects.produced?("do_thing")
|
28
|
+
end
|
29
|
+
|
30
|
+
test "allows a journey consisting of multiple named steps to be defined and performed to completion" do
|
31
|
+
multi_step_journey_class = create_journey_subclass do
|
32
|
+
[:step1, :step2, :step3].each do |step_name|
|
33
|
+
step step_name do
|
34
|
+
SideEffects.touch!("from_#{step_name}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
journey = multi_step_journey_class.create!
|
40
|
+
assert_equal "step1", journey.next_step_name
|
41
|
+
|
42
|
+
journey.perform_next_step!
|
43
|
+
assert_equal "step2", journey.next_step_name
|
44
|
+
assert_equal "step1", journey.previous_step_name
|
45
|
+
|
46
|
+
journey.perform_next_step!
|
47
|
+
assert_equal "step3", journey.next_step_name
|
48
|
+
assert_equal "step2", journey.previous_step_name
|
49
|
+
|
50
|
+
journey.perform_next_step!
|
51
|
+
assert journey.finished?
|
52
|
+
assert_nil journey.next_step_name
|
53
|
+
assert_equal "step3", journey.previous_step_name
|
54
|
+
|
55
|
+
assert SideEffects.produced?("from_step1")
|
56
|
+
assert SideEffects.produced?("from_step2")
|
57
|
+
assert SideEffects.produced?("from_step3")
|
58
|
+
end
|
59
|
+
|
60
|
+
test "allows a journey consisting of multiple anonymous steps to be defined and performed to completion" do
|
61
|
+
anonymous_steps_class = create_journey_subclass do
|
62
|
+
3.times do |n|
|
63
|
+
step do
|
64
|
+
SideEffects.touch!("sidefx_#{n}")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
journey = anonymous_steps_class.create!
|
70
|
+
assert_equal "step_1", journey.next_step_name
|
71
|
+
|
72
|
+
journey.perform_next_step!
|
73
|
+
assert_equal "step_2", journey.next_step_name
|
74
|
+
assert_equal "step_1", journey.previous_step_name
|
75
|
+
|
76
|
+
journey.perform_next_step!
|
77
|
+
assert_equal "step_3", journey.next_step_name
|
78
|
+
assert_equal "step_2", journey.previous_step_name
|
79
|
+
|
80
|
+
journey.perform_next_step!
|
81
|
+
assert journey.finished?
|
82
|
+
assert_nil journey.next_step_name
|
83
|
+
assert_equal "step_3", journey.previous_step_name
|
84
|
+
|
85
|
+
assert SideEffects.produced?("sidefx_0")
|
86
|
+
assert SideEffects.produced?("sidefx_1")
|
87
|
+
assert SideEffects.produced?("sidefx_2")
|
88
|
+
end
|
89
|
+
|
90
|
+
test "allows an arbitrary ActiveRecord to be attached as the hero" do
|
91
|
+
carried_journey_class = create_journey_subclass
|
92
|
+
carrier_journey_class = create_journey_subclass do
|
93
|
+
step :only do
|
94
|
+
raise "Incorrect" unless hero.instance_of?(carried_journey_class)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
hero = carried_journey_class.create!
|
99
|
+
journey = carrier_journey_class.create!(hero: hero)
|
100
|
+
assert_nothing_raised do
|
101
|
+
journey.perform_next_step!
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
test "allows a journey where steps are delayed in time using wait:" do
|
106
|
+
timely_journey_class = create_journey_subclass do
|
107
|
+
step wait: 10.hours do
|
108
|
+
SideEffects.touch! "after_10_hours.txt"
|
109
|
+
end
|
110
|
+
|
111
|
+
step wait: 5.minutes do
|
112
|
+
SideEffects.touch! "after_5_minutes.txt"
|
113
|
+
end
|
114
|
+
|
115
|
+
step do
|
116
|
+
SideEffects.touch! "final_nowait.txt"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
freeze_time
|
121
|
+
timely_journey_class.create!
|
122
|
+
|
123
|
+
assert_no_side_effects do
|
124
|
+
perform_enqueued_jobs
|
125
|
+
end
|
126
|
+
|
127
|
+
travel 10.hours
|
128
|
+
assert_produced_side_effects "after_10_hours.txt" do
|
129
|
+
perform_enqueued_jobs
|
130
|
+
end
|
131
|
+
|
132
|
+
travel 4.minutes
|
133
|
+
assert_no_side_effects do
|
134
|
+
perform_enqueued_jobs
|
135
|
+
end
|
136
|
+
|
137
|
+
travel 1.minutes
|
138
|
+
assert_produced_side_effects "after_5_minutes.txt" do
|
139
|
+
perform_enqueued_jobs
|
140
|
+
end
|
141
|
+
|
142
|
+
assert_produced_side_effects "final_nowait.txt" do
|
143
|
+
perform_enqueued_jobs
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
test "allows a journey where steps are delayed in time using after:" do
|
148
|
+
journey_class = create_journey_subclass do
|
149
|
+
step after: 10.hours do
|
150
|
+
SideEffects.touch! "step1"
|
151
|
+
end
|
152
|
+
|
153
|
+
step after: 605.minutes do
|
154
|
+
SideEffects.touch! "step2"
|
155
|
+
end
|
156
|
+
|
157
|
+
step do
|
158
|
+
SideEffects.touch! "step3"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
timely_journey = journey_class.create!
|
163
|
+
freeze_time
|
164
|
+
|
165
|
+
assert_no_side_effects do
|
166
|
+
perform_enqueued_jobs
|
167
|
+
end
|
168
|
+
|
169
|
+
travel_to(timely_journey.next_step_to_be_performed_at + 1.second)
|
170
|
+
assert_produced_side_effects "step1" do
|
171
|
+
perform_enqueued_jobs
|
172
|
+
end
|
173
|
+
|
174
|
+
travel(4.minutes)
|
175
|
+
assert_no_side_effects do
|
176
|
+
perform_enqueued_jobs
|
177
|
+
end
|
178
|
+
|
179
|
+
travel(1.minutes + 1.second)
|
180
|
+
assert_produced_side_effects "step2" do
|
181
|
+
perform_enqueued_jobs
|
182
|
+
end
|
183
|
+
assert_produced_side_effects "step3" do
|
184
|
+
perform_enqueued_jobs
|
185
|
+
end
|
186
|
+
assert_empty enqueued_jobs # Journey ended
|
187
|
+
end
|
188
|
+
|
189
|
+
test "tracks steps entered and completed using counters" do
|
190
|
+
failing = create_journey_subclass do
|
191
|
+
step do
|
192
|
+
raise "oops"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
not_failing = create_journey_subclass do
|
197
|
+
step do
|
198
|
+
true # no-op
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
failing_journey = failing.create!
|
203
|
+
assert_raises(RuntimeError) { failing_journey.perform_next_step! }
|
204
|
+
assert_equal 1, failing_journey.steps_entered
|
205
|
+
assert_equal 0, failing_journey.steps_completed
|
206
|
+
|
207
|
+
failing_journey.ready!
|
208
|
+
assert_raises(RuntimeError) { failing_journey.perform_next_step! }
|
209
|
+
assert_equal 2, failing_journey.steps_entered
|
210
|
+
assert_equal 0, failing_journey.steps_completed
|
211
|
+
|
212
|
+
non_failing_journey = not_failing.create!
|
213
|
+
non_failing_journey.perform_next_step!
|
214
|
+
assert_equal 1, non_failing_journey.steps_entered
|
215
|
+
assert_equal 1, non_failing_journey.steps_completed
|
216
|
+
end
|
217
|
+
|
218
|
+
test "allows a step to reattempt itself" do
|
219
|
+
deferring = create_journey_subclass do
|
220
|
+
step do
|
221
|
+
reattempt! wait: 5.minutes
|
222
|
+
raise "Should never be reached"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
journey = deferring.create!
|
227
|
+
perform_enqueued_jobs
|
228
|
+
|
229
|
+
journey.reload
|
230
|
+
assert_equal "step_1", journey.previous_step_name
|
231
|
+
assert_equal "step_1", journey.next_step_name
|
232
|
+
assert_in_delta Time.current + 5.minutes, journey.next_step_to_be_performed_at, 1.second
|
233
|
+
|
234
|
+
travel 5.minutes + 1.second
|
235
|
+
perform_enqueued_jobs
|
236
|
+
|
237
|
+
journey.reload
|
238
|
+
assert_equal "step_1", journey.previous_step_name
|
239
|
+
assert_equal "step_1", journey.next_step_name
|
240
|
+
assert_in_delta Time.current + 5.minutes, journey.next_step_to_be_performed_at, 1.second
|
241
|
+
end
|
242
|
+
|
243
|
+
test "allows a journey consisting of multiple steps where the first step bails out to be defined and performed to the point of cancellation" do
|
244
|
+
interrupting = create_journey_subclass do
|
245
|
+
step :step1 do
|
246
|
+
SideEffects.touch!("step1_before_cancel")
|
247
|
+
cancel!
|
248
|
+
SideEffects.touch!("step1_after_cancel")
|
249
|
+
end
|
250
|
+
|
251
|
+
step :step2 do
|
252
|
+
raise "Should never be reached"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
journey = interrupting.create!
|
257
|
+
assert_equal "step1", journey.next_step_name
|
258
|
+
|
259
|
+
perform_enqueued_jobs
|
260
|
+
assert SideEffects.produced?("step1_before_cancel")
|
261
|
+
assert_not SideEffects.produced?("step1_after_cancel")
|
262
|
+
assert_canceled_or_finished(journey)
|
263
|
+
end
|
264
|
+
|
265
|
+
test "finishes the journey after perform_next_step" do
|
266
|
+
rapid = create_journey_subclass do
|
267
|
+
step :one do
|
268
|
+
true # no-op
|
269
|
+
end
|
270
|
+
step :two do
|
271
|
+
true # no-op
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
journey = rapid.create!
|
276
|
+
assert journey.ready?
|
277
|
+
journey.perform_next_step!
|
278
|
+
assert journey.ready?
|
279
|
+
journey.perform_next_step!
|
280
|
+
assert journey.finished?
|
281
|
+
end
|
282
|
+
|
283
|
+
test "does not enter next step on a finished journey" do
|
284
|
+
near_instant = create_journey_subclass do
|
285
|
+
step :one do
|
286
|
+
finished!
|
287
|
+
end
|
288
|
+
|
289
|
+
step :two do
|
290
|
+
raise "Should never be reached"
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
journey = near_instant.create!
|
295
|
+
assert journey.ready?
|
296
|
+
journey.perform_next_step!
|
297
|
+
assert journey.finished?
|
298
|
+
|
299
|
+
assert_nothing_raised do
|
300
|
+
journey.perform_next_step!
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
test "raises an exception if a step changes the journey but does not save it" do
|
305
|
+
mutating = create_journey_subclass do
|
306
|
+
step :one do
|
307
|
+
self.state = "canceled"
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
journey = mutating.create!
|
312
|
+
assert_raises(StepperMotor::JourneyNotPersisted) do
|
313
|
+
journey.perform_next_step!
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
test "resets the instance variables after performing a step" do
|
318
|
+
self_resetting = create_journey_subclass do
|
319
|
+
step :one do
|
320
|
+
raise unless @current_step_definition
|
321
|
+
end
|
322
|
+
|
323
|
+
step :two do
|
324
|
+
@reattempt_after = 2.minutes
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
journey = self_resetting.create!
|
329
|
+
assert_nothing_raised do
|
330
|
+
journey.perform_next_step!
|
331
|
+
end
|
332
|
+
assert_nil journey.instance_variable_get(:@current_step_definition)
|
333
|
+
|
334
|
+
assert_nothing_raised do
|
335
|
+
journey.perform_next_step!
|
336
|
+
end
|
337
|
+
assert_nil journey.instance_variable_get(:@current_step_definition)
|
338
|
+
assert_nil journey.instance_variable_get(:@reattempt_after)
|
339
|
+
end
|
340
|
+
|
341
|
+
private
|
342
|
+
|
343
|
+
def assert_canceled_or_finished(model)
|
344
|
+
model.reload
|
345
|
+
assert_includes ["canceled", "finished"], model.state
|
346
|
+
end
|
347
|
+
|
348
|
+
def assert_produced_side_effects(name)
|
349
|
+
yield
|
350
|
+
assert SideEffects.produced?(name)
|
351
|
+
end
|
352
|
+
end
|
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "test_helper"
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
class RecoverStuckJourneysJobTest < ActiveSupport::TestCase
|
6
|
+
setup do
|
7
7
|
StepperMotor::Journey.delete_all
|
8
8
|
end
|
9
9
|
|
10
|
-
|
10
|
+
test "handles recovery from a background job" do
|
11
11
|
stuck_journey_class1 = create_journey_subclass do
|
12
12
|
self.when_stuck = :cancel
|
13
13
|
|
@@ -49,21 +49,21 @@ RSpec.describe "RecoveryStuckJourneysJobV1" do
|
|
49
49
|
journey_to_reattempt.perform_next_step!
|
50
50
|
end.resume
|
51
51
|
|
52
|
-
|
53
|
-
|
52
|
+
assert journey_to_cancel.reload.performing?
|
53
|
+
assert journey_to_reattempt.reload.performing?
|
54
54
|
|
55
55
|
StepperMotor::RecoverStuckJourneysJobV1.perform_now(stuck_for: 2.days)
|
56
|
-
|
57
|
-
|
56
|
+
assert journey_to_cancel.reload.performing?
|
57
|
+
assert journey_to_reattempt.reload.performing?
|
58
58
|
|
59
59
|
travel_to Time.now + 2.days + 1.second
|
60
60
|
StepperMotor::RecoverStuckJourneysJobV1.perform_now(stuck_for: 2.days)
|
61
61
|
|
62
|
-
|
63
|
-
|
62
|
+
assert journey_to_cancel.reload.canceled?
|
63
|
+
assert journey_to_reattempt.reload.ready?
|
64
64
|
end
|
65
65
|
|
66
|
-
|
66
|
+
test "does not raise when the class of the journey is no longer present" do
|
67
67
|
stuck_journey_class1 = create_journey_subclass do
|
68
68
|
self.when_stuck = :cancel
|
69
69
|
|
@@ -78,12 +78,12 @@ RSpec.describe "RecoveryStuckJourneysJobV1" do
|
|
78
78
|
Fiber.new do
|
79
79
|
journey_to_cancel.perform_next_step!
|
80
80
|
end.resume
|
81
|
-
|
81
|
+
assert journey_to_cancel.reload.performing?
|
82
82
|
|
83
83
|
journey_to_cancel.class.update_all(type: "UnknownJourneySubclass")
|
84
84
|
|
85
|
-
|
85
|
+
assert_nothing_raised do
|
86
86
|
StepperMotor::RecoverStuckJourneysJobV1.perform_now(stuck_for: 2.days)
|
87
|
-
|
87
|
+
end
|
88
88
|
end
|
89
89
|
end
|
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "test_helper"
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
class RecoveryTest < ActiveSupport::TestCase
|
6
|
+
setup do
|
7
7
|
StepperMotor::Journey.delete_all
|
8
8
|
end
|
9
9
|
|
10
|
-
|
10
|
+
test "recovers a journey by reattempting it" do
|
11
11
|
stuck_journey_class = create_journey_subclass do
|
12
12
|
step :first do
|
13
13
|
end
|
@@ -24,12 +24,12 @@ RSpec.describe "Recovery of stuck journeys" do
|
|
24
24
|
journey = stuck_journey_class.create!
|
25
25
|
|
26
26
|
journey.perform_next_step!
|
27
|
-
|
27
|
+
assert_equal "second", journey.next_step_name
|
28
28
|
|
29
29
|
travel_to Time.now + 5.days
|
30
30
|
|
31
|
-
|
32
|
-
|
31
|
+
assert_equal :reattempt, stuck_journey_class.when_stuck
|
32
|
+
assert_equal :reattempt, journey.when_stuck
|
33
33
|
|
34
34
|
# Hang the journey in "performing"
|
35
35
|
stuck_fiber = Fiber.new do
|
@@ -37,26 +37,26 @@ RSpec.describe "Recovery of stuck journeys" do
|
|
37
37
|
end
|
38
38
|
stuck_fiber.resume
|
39
39
|
|
40
|
-
|
41
|
-
|
42
|
-
|
40
|
+
assert journey.persisted?
|
41
|
+
assert journey.performing?
|
42
|
+
assert_equal Time.now, journey.updated_at
|
43
43
|
|
44
|
-
|
44
|
+
assert_not_includes StepperMotor::Journey.stuck(1.days.ago), journey
|
45
45
|
|
46
46
|
travel_to Time.now + 2.days
|
47
|
-
|
47
|
+
assert_includes StepperMotor::Journey.stuck(2.days.ago), journey
|
48
48
|
|
49
49
|
perform_at_before_recovery = journey.next_step_to_be_performed_at
|
50
|
-
|
50
|
+
assert_nothing_raised do
|
51
51
|
journey.reload.recover!
|
52
|
-
|
52
|
+
end
|
53
53
|
|
54
54
|
journey.reload
|
55
|
-
|
56
|
-
|
55
|
+
assert_equal perform_at_before_recovery, journey.next_step_to_be_performed_at
|
56
|
+
assert_equal "second", journey.next_step_name
|
57
57
|
end
|
58
58
|
|
59
|
-
|
59
|
+
test "recovers a journey by canceling it" do
|
60
60
|
stuck_journey_class = create_journey_subclass do
|
61
61
|
self.when_stuck = :cancel
|
62
62
|
|
@@ -75,12 +75,12 @@ RSpec.describe "Recovery of stuck journeys" do
|
|
75
75
|
journey = stuck_journey_class.create!
|
76
76
|
|
77
77
|
journey.perform_next_step!
|
78
|
-
|
78
|
+
assert_equal "second", journey.next_step_name
|
79
79
|
|
80
80
|
travel_to Time.now + 5.days
|
81
81
|
|
82
|
-
|
83
|
-
|
82
|
+
assert_equal :cancel, stuck_journey_class.when_stuck
|
83
|
+
assert_equal :cancel, journey.when_stuck
|
84
84
|
|
85
85
|
# Hang the journey in "performing"
|
86
86
|
stuck_fiber = Fiber.new do
|
@@ -88,18 +88,18 @@ RSpec.describe "Recovery of stuck journeys" do
|
|
88
88
|
end
|
89
89
|
stuck_fiber.resume
|
90
90
|
|
91
|
-
|
92
|
-
|
93
|
-
|
91
|
+
assert journey.persisted?
|
92
|
+
assert journey.performing?
|
93
|
+
assert_equal Time.now, journey.updated_at
|
94
94
|
|
95
95
|
travel_to Time.now + 2.days
|
96
|
-
|
96
|
+
assert_includes StepperMotor::Journey.stuck(2.days.ago), journey
|
97
97
|
|
98
|
-
|
98
|
+
assert_nothing_raised do
|
99
99
|
journey.reload.recover!
|
100
|
-
|
100
|
+
end
|
101
101
|
|
102
102
|
journey.reload
|
103
|
-
|
103
|
+
assert journey.canceled?
|
104
104
|
end
|
105
105
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class TestHelperTest < ActiveSupport::TestCase
|
6
|
+
include SideEffects::TestHelper
|
7
|
+
include StepperMotor::TestHelper
|
8
|
+
|
9
|
+
def speedy_journey_class
|
10
|
+
create_journey_subclass do
|
11
|
+
step :step_1, wait: 40.minutes do
|
12
|
+
SideEffects.touch!("step_1")
|
13
|
+
end
|
14
|
+
|
15
|
+
step :step_2, wait: 2.days do
|
16
|
+
SideEffects.touch!("step_2")
|
17
|
+
end
|
18
|
+
|
19
|
+
step do
|
20
|
+
SideEffects.touch!("step_3")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
test "speedruns the journey despite waits being configured" do
|
26
|
+
journey = speedy_journey_class.create!
|
27
|
+
assert journey.ready?
|
28
|
+
|
29
|
+
SideEffects.clear!
|
30
|
+
speedrun_journey(journey)
|
31
|
+
assert SideEffects.produced?("step_1")
|
32
|
+
assert SideEffects.produced?("step_2")
|
33
|
+
assert SideEffects.produced?("step_3")
|
34
|
+
end
|
35
|
+
|
36
|
+
test "is able to perform a single step forcibly" do
|
37
|
+
journey = speedy_journey_class.create!
|
38
|
+
assert journey.ready?
|
39
|
+
|
40
|
+
SideEffects.clear!
|
41
|
+
immediately_perform_single_step(journey, :step_2)
|
42
|
+
assert SideEffects.produced?("step_2")
|
43
|
+
end
|
44
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Configure Rails Environment
|
4
|
+
ENV["RAILS_ENV"] = "test"
|
5
|
+
|
6
|
+
require_relative "../test/dummy/config/environment"
|
7
|
+
require_relative "side_effects_helper"
|
8
|
+
|
9
|
+
ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
|
10
|
+
require "rails/test_help"
|
11
|
+
|
12
|
+
# Load fixtures from the engine
|
13
|
+
if ActiveSupport::TestCase.respond_to?(:fixture_paths=)
|
14
|
+
ActiveSupport::TestCase.fixture_paths = [File.expand_path("fixtures", __dir__)]
|
15
|
+
ActionDispatch::IntegrationTest.fixture_paths = ActiveSupport::TestCase.fixture_paths
|
16
|
+
ActiveSupport::TestCase.file_fixture_path = File.expand_path("fixtures", __dir__) + "/files"
|
17
|
+
ActiveSupport::TestCase.fixtures :all
|
18
|
+
end
|
19
|
+
|
20
|
+
module JourneyDefinitionHelper
|
21
|
+
def setup
|
22
|
+
@class_names_rng = Random.new(Minitest.seed)
|
23
|
+
@dynamic_class_names = Set.new
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def teardown
|
28
|
+
@dynamic_class_names.each do |name|
|
29
|
+
Object.send(:remove_const, name)
|
30
|
+
end
|
31
|
+
@dynamic_class_names.clear
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_journey_subclass(&blk)
|
36
|
+
# https://stackoverflow.com/questions/4113479/dynamic-class-definition-with-a-class-name
|
37
|
+
random_component = @class_names_rng.hex(8)
|
38
|
+
random_name = "JourneySubclass_#{random_component}"
|
39
|
+
klass = Class.new(StepperMotor::Journey, &blk)
|
40
|
+
Object.const_set(random_name, klass)
|
41
|
+
@dynamic_class_names << random_name
|
42
|
+
klass
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
ActiveSupport::TestCase.include(JourneyDefinitionHelper)
|