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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +12 -0
  3. data/.github/workflows/ci.yml +51 -0
  4. data/CHANGELOG.md +77 -2
  5. data/Gemfile +11 -0
  6. data/README.md +13 -374
  7. data/Rakefile +21 -3
  8. data/bin/test +5 -0
  9. data/lib/generators/install_generator.rb +6 -1
  10. data/lib/generators/stepper_motor_migration_003.rb.erb +6 -0
  11. data/lib/generators/stepper_motor_migration_004.rb.erb +26 -0
  12. data/lib/stepper_motor/forward_scheduler.rb +8 -4
  13. data/lib/stepper_motor/journey/flow_control.rb +58 -0
  14. data/lib/stepper_motor/journey/recovery.rb +34 -0
  15. data/lib/stepper_motor/journey.rb +85 -84
  16. data/lib/stepper_motor/perform_step_job_v2.rb +2 -2
  17. data/lib/stepper_motor/railtie.rb +1 -1
  18. data/lib/stepper_motor/recover_stuck_journeys_job_v1.rb +3 -1
  19. data/lib/stepper_motor/step.rb +70 -5
  20. data/lib/stepper_motor/version.rb +1 -1
  21. data/lib/stepper_motor.rb +1 -2
  22. data/lib/tasks/stepper_motor_tasks.rake +8 -0
  23. data/manual/MANUAL.md +538 -0
  24. data/rbi/stepper_motor.rbi +459 -0
  25. data/sig/stepper_motor.rbs +406 -3
  26. data/stepper_motor.gemspec +49 -0
  27. data/test/dummy/Rakefile +8 -0
  28. data/test/dummy/app/assets/stylesheets/application.css +1 -0
  29. data/test/dummy/app/controllers/application_controller.rb +6 -0
  30. data/test/dummy/app/helpers/application_helper.rb +4 -0
  31. data/test/dummy/app/jobs/application_job.rb +9 -0
  32. data/test/dummy/app/mailers/application_mailer.rb +6 -0
  33. data/test/dummy/app/models/application_record.rb +5 -0
  34. data/test/dummy/app/views/layouts/application.html.erb +27 -0
  35. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  36. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  37. data/test/dummy/app/views/pwa/manifest.json.erb +22 -0
  38. data/test/dummy/app/views/pwa/service-worker.js +26 -0
  39. data/test/dummy/bin/dev +2 -0
  40. data/test/dummy/bin/rails +4 -0
  41. data/test/dummy/bin/rake +4 -0
  42. data/test/dummy/bin/setup +34 -0
  43. data/test/dummy/config/application.rb +28 -0
  44. data/test/dummy/config/boot.rb +7 -0
  45. data/test/dummy/config/cable.yml +10 -0
  46. data/test/dummy/config/database.yml +32 -0
  47. data/test/dummy/config/environment.rb +7 -0
  48. data/test/dummy/config/environments/development.rb +71 -0
  49. data/test/dummy/config/environments/production.rb +91 -0
  50. data/test/dummy/config/environments/test.rb +55 -0
  51. data/test/dummy/config/initializers/content_security_policy.rb +27 -0
  52. data/test/dummy/config/initializers/filter_parameter_logging.rb +10 -0
  53. data/test/dummy/config/initializers/inflections.rb +18 -0
  54. data/test/dummy/config/initializers/stepper_motor.rb +3 -0
  55. data/test/dummy/config/locales/en.yml +31 -0
  56. data/test/dummy/config/puma.rb +40 -0
  57. data/test/dummy/config/routes.rb +16 -0
  58. data/test/dummy/config/storage.yml +34 -0
  59. data/test/dummy/config.ru +8 -0
  60. data/test/dummy/db/migrate/20250520094921_stepper_motor_migration_001.rb +38 -0
  61. data/test/dummy/db/migrate/20250520094922_stepper_motor_migration_002.rb +8 -0
  62. data/test/dummy/db/migrate/20250522212312_stepper_motor_migration_003.rb +7 -0
  63. data/test/dummy/db/migrate/20250525110812_stepper_motor_migration_004.rb +28 -0
  64. data/test/dummy/db/schema.rb +37 -0
  65. data/test/dummy/public/400.html +114 -0
  66. data/test/dummy/public/404.html +114 -0
  67. data/test/dummy/public/406-unsupported-browser.html +114 -0
  68. data/test/dummy/public/422.html +114 -0
  69. data/test/dummy/public/500.html +114 -0
  70. data/test/dummy/public/icon.png +0 -0
  71. data/test/dummy/public/icon.svg +3 -0
  72. data/test/side_effects_helper.rb +67 -0
  73. data/test/stepper_motor/cyclic_scheduler_test.rb +77 -0
  74. data/{spec/stepper_motor/forward_scheduler_spec.rb → test/stepper_motor/forward_scheduler_test.rb} +9 -10
  75. data/test/stepper_motor/journey/exception_handling_test.rb +89 -0
  76. data/test/stepper_motor/journey/flow_control_test.rb +78 -0
  77. data/test/stepper_motor/journey/idempotency_test.rb +65 -0
  78. data/test/stepper_motor/journey/step_definition_test.rb +187 -0
  79. data/test/stepper_motor/journey/uniqueness_test.rb +48 -0
  80. data/test/stepper_motor/journey_test.rb +352 -0
  81. data/{spec/stepper_motor/recover_stuck_journeys_job_spec.rb → test/stepper_motor/recover_stuck_journeys_job_test.rb} +14 -14
  82. data/{spec/stepper_motor/recovery_spec.rb → test/stepper_motor/recovery_test.rb} +27 -27
  83. data/test/stepper_motor/test_helper_test.rb +44 -0
  84. data/test/stepper_motor_test.rb +9 -0
  85. data/test/test_helper.rb +46 -0
  86. metadata +120 -24
  87. data/.rspec +0 -3
  88. data/.ruby-version +0 -1
  89. data/.standard.yml +0 -4
  90. data/.yardopts +0 -1
  91. data/spec/helpers/side_effects.rb +0 -85
  92. data/spec/spec_helper.rb +0 -90
  93. data/spec/stepper_motor/cyclic_scheduler_spec.rb +0 -68
  94. data/spec/stepper_motor/generator_spec.rb +0 -16
  95. data/spec/stepper_motor/journey_spec.rb +0 -401
  96. data/spec/stepper_motor/test_helper_spec.rb +0 -48
  97. 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
- require_relative "../spec_helper"
3
+ require "test_helper"
4
4
 
5
- RSpec.describe "RecoveryStuckJourneysJobV1" do
6
- before do
5
+ class RecoverStuckJourneysJobTest < ActiveSupport::TestCase
6
+ setup do
7
7
  StepperMotor::Journey.delete_all
8
8
  end
9
9
 
10
- it "handles recovery from a background job" do
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
- expect(journey_to_cancel.reload).to be_performing
53
- expect(journey_to_reattempt.reload).to be_performing
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
- expect(journey_to_cancel.reload).to be_performing
57
- expect(journey_to_reattempt.reload).to be_performing
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
- expect(journey_to_cancel.reload).to be_canceled
63
- expect(journey_to_reattempt.reload).to be_ready
62
+ assert journey_to_cancel.reload.canceled?
63
+ assert journey_to_reattempt.reload.ready?
64
64
  end
65
65
 
66
- it "does not raise when the class of the journey is no longer present" do
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
- expect(journey_to_cancel.reload).to be_performing
81
+ assert journey_to_cancel.reload.performing?
82
82
 
83
83
  journey_to_cancel.class.update_all(type: "UnknownJourneySubclass")
84
84
 
85
- expect {
85
+ assert_nothing_raised do
86
86
  StepperMotor::RecoverStuckJourneysJobV1.perform_now(stuck_for: 2.days)
87
- }.not_to raise_error
87
+ end
88
88
  end
89
89
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../spec_helper"
3
+ require "test_helper"
4
4
 
5
- RSpec.describe "Recovery of stuck journeys" do
6
- before do
5
+ class RecoveryTest < ActiveSupport::TestCase
6
+ setup do
7
7
  StepperMotor::Journey.delete_all
8
8
  end
9
9
 
10
- it "recovers a journey by reattempting it" do
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
- expect(journey.next_step_name).to eq("second")
27
+ assert_equal "second", journey.next_step_name
28
28
 
29
29
  travel_to Time.now + 5.days
30
30
 
31
- expect(stuck_journey_class.when_stuck).to eq(:reattempt)
32
- expect(journey.when_stuck).to eq(:reattempt)
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
- expect(journey).to be_persisted
41
- expect(journey).to be_performing
42
- expect(journey.updated_at).to eq(Time.now)
40
+ assert journey.persisted?
41
+ assert journey.performing?
42
+ assert_equal Time.now, journey.updated_at
43
43
 
44
- expect(StepperMotor::Journey.stuck(1.days.ago)).not_to include(journey)
44
+ assert_not_includes StepperMotor::Journey.stuck(1.days.ago), journey
45
45
 
46
46
  travel_to Time.now + 2.days
47
- expect(StepperMotor::Journey.stuck(2.days.ago)).to include(journey)
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
- expect {
50
+ assert_nothing_raised do
51
51
  journey.reload.recover!
52
- }.not_to raise_error
52
+ end
53
53
 
54
54
  journey.reload
55
- expect(journey.next_step_to_be_performed_at).to eq(perform_at_before_recovery)
56
- expect(journey.next_step_name).to eq("second")
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
- it "recovers a journey by canceling it" do
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
- expect(journey.next_step_name).to eq("second")
78
+ assert_equal "second", journey.next_step_name
79
79
 
80
80
  travel_to Time.now + 5.days
81
81
 
82
- expect(stuck_journey_class.when_stuck).to eq(:cancel)
83
- expect(journey.when_stuck).to eq(:cancel)
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
- expect(journey).to be_persisted
92
- expect(journey).to be_performing
93
- expect(journey.updated_at).to eq(Time.now)
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
- expect(StepperMotor::Journey.stuck(2.days.ago)).to include(journey)
96
+ assert_includes StepperMotor::Journey.stuck(2.days.ago), journey
97
97
 
98
- expect {
98
+ assert_nothing_raised do
99
99
  journey.reload.recover!
100
- }.not_to raise_error
100
+ end
101
101
 
102
102
  journey.reload
103
- expect(journey).to be_canceled
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class StepperMotorTest < ActiveSupport::TestCase
6
+ test "it has a version number" do
7
+ assert StepperMotor::VERSION
8
+ end
9
+ end
@@ -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)