stepper_motor 0.1.12 → 0.1.16
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/workflows/ci.yml +50 -8
- data/CHANGELOG.md +17 -0
- data/lib/generators/stepper_motor_migration_005.rb.erb +58 -0
- data/lib/stepper_motor/journey/flow_control.rb +49 -0
- data/lib/stepper_motor/journey.rb +53 -10
- data/lib/stepper_motor/step.rb +38 -8
- data/lib/stepper_motor/version.rb +1 -1
- data/manual/MANUAL.md +352 -90
- data/rbi/stepper_motor.rbi +48 -6
- data/sig/stepper_motor.rbs +44 -5
- data/stepper_motor.gemspec +2 -0
- data/test/dummy/config/database.mysql2.yml +14 -0
- data/test/dummy/config/database.postgres.yml +14 -0
- data/test/dummy/config/database.sqlite3.yml +32 -0
- data/test/dummy/config/initializers/stepper_motor.rb +1 -1
- data/test/dummy/db/migrate/20250609221201_stepper_motor_migration_005.rb +58 -0
- data/test/dummy/db/schema.rb +7 -6
- data/test/stepper_motor/journey/exception_handling_test.rb +57 -0
- data/test/stepper_motor/journey/flow_control_test.rb +164 -0
- data/test/stepper_motor/journey/if_condition_test.rb +355 -0
- data/test/stepper_motor/journey/step_definition_test.rb +1 -0
- metadata +37 -6
data/sig/stepper_motor.rbs
CHANGED
@@ -30,14 +30,24 @@ module StepperMotor
|
|
30
30
|
#
|
31
31
|
# _@param_ `wait` — the amount of time to wait before entering the step
|
32
32
|
#
|
33
|
-
# _@param_ `on_exception` — the action to take if an exception occurs when performing the step. The possible values are: * `:cancel!` - cancels the Journey and re-raises the exception. The Journey will be persisted before re-raising. * `:reattempt!` - reattempts the Journey and re-raises the exception. The Journey will be persisted before re-raising.
|
33
|
+
# _@param_ `on_exception` — the action to take if an exception occurs when performing the step. The possible values are: * `:cancel!` - cancels the Journey and re-raises the exception. The Journey will be persisted before re-raising. * `:reattempt!` - reattempts the Journey and re-raises the exception. The Journey will be persisted before re-raising. * `:pause!` - pauses the Journey and re-raises the exception. The Journey will be persisted before re-raising. * `:skip!` - skips the current step and proceeds to the next step, or finishes the journey if it's the last step.
|
34
|
+
#
|
35
|
+
# _@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.
|
34
36
|
def initialize: (
|
35
37
|
name: (String | Symbol),
|
36
38
|
seq: untyped,
|
37
|
-
on_exception: Symbol,
|
38
|
-
?wait: (Numeric | ActiveSupport::Duration)
|
39
|
+
?on_exception: Symbol,
|
40
|
+
?wait: (Numeric | ActiveSupport::Duration),
|
41
|
+
?skip_if: (TrueClass | FalseClass | NilClass | Symbol | Proc)
|
39
42
|
) -> void
|
40
43
|
|
44
|
+
# Checks if the step should be skipped based on the skip_if condition
|
45
|
+
#
|
46
|
+
# _@param_ `journey` — the journey to check the condition for
|
47
|
+
#
|
48
|
+
# _@return_ — true if the step should be skipped, false otherwise
|
49
|
+
def should_skip?: (StepperMotor::Journey journey) -> bool
|
50
|
+
|
41
51
|
# Performs the step on the passed Journey, wrapping the step with the required context.
|
42
52
|
#
|
43
53
|
# _@param_ `journey` — the journey to perform the step in. If a `step_block` is passed in, it is going to be executed in the context of the journey using `instance_exec`. If only the name of the step has been provided, an accordingly named public method on the journey will be called
|
@@ -117,6 +127,10 @@ module StepperMotor
|
|
117
127
|
#
|
118
128
|
# _@param_ `on_exception` — See {StepperMotor::Step#on_exception}
|
119
129
|
#
|
130
|
+
# _@param_ `skip_if` — condition to check before performing the step. 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 be skipped if the condition returns a truthy value.
|
131
|
+
#
|
132
|
+
# _@param_ `if` — condition to check before performing the step. 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 be performed if the condition returns a truthy value. and skipped otherwise. Inverse of `skip_if`.
|
133
|
+
#
|
120
134
|
# _@param_ `additional_step_definition_options` — Any remaining options get passed to `StepperMotor::Step.new` as keyword arguments.
|
121
135
|
#
|
122
136
|
# _@return_ — the step definition that has been created
|
@@ -124,8 +138,7 @@ module StepperMotor
|
|
124
138
|
?String? name,
|
125
139
|
?wait: (Float | untyped | ActiveSupport::Duration)?,
|
126
140
|
?after: (Float | untyped | ActiveSupport::Duration)?,
|
127
|
-
|
128
|
-
**untyped additional_step_definition_options
|
141
|
+
**::Hash[untyped, untyped] additional_step_definition_options
|
129
142
|
) -> StepperMotor::Step
|
130
143
|
|
131
144
|
# sord warn - "StepperMotor::Step?" does not appear to be a type
|
@@ -205,6 +218,19 @@ module StepperMotor
|
|
205
218
|
# _@return_ — void
|
206
219
|
def reattempt!: (?wait: untyped) -> untyped
|
207
220
|
|
221
|
+
# Is used to skip the current step and proceed to the next step in the journey. This is useful when you want to
|
222
|
+
# conditionally skip a step based on some business logic without canceling the entire journey. For example,
|
223
|
+
# you might want to skip a reminder email step if the user has already taken the required action.
|
224
|
+
#
|
225
|
+
# If there are more steps after the current step, `skip!` will schedule the next step to be performed.
|
226
|
+
# If the current step is the last step in the journey, `skip!` will finish the journey.
|
227
|
+
#
|
228
|
+
# `skip!` may be called within a step or outside of a step for journeys in the `ready` state.
|
229
|
+
# When called outside of a step, it will skip the next scheduled step and proceed to the following step.
|
230
|
+
#
|
231
|
+
# _@return_ — void
|
232
|
+
def skip!: () -> untyped
|
233
|
+
|
208
234
|
# Is used to pause a Journey at any point. The "paused" state is similar to the "ready" state, except that "perform_next_step!" on the
|
209
235
|
# journey will do nothing - even if it is scheduled to be performed. Pausing a Journey can be useful in the following situations:
|
210
236
|
#
|
@@ -251,6 +277,19 @@ module StepperMotor
|
|
251
277
|
# _@return_ — void
|
252
278
|
def reattempt!: (?wait: untyped) -> untyped
|
253
279
|
|
280
|
+
# Is used to skip the current step and proceed to the next step in the journey. This is useful when you want to
|
281
|
+
# conditionally skip a step based on some business logic without canceling the entire journey. For example,
|
282
|
+
# you might want to skip a reminder email step if the user has already taken the required action.
|
283
|
+
#
|
284
|
+
# If there are more steps after the current step, `skip!` will schedule the next step to be performed.
|
285
|
+
# If the current step is the last step in the journey, `skip!` will finish the journey.
|
286
|
+
#
|
287
|
+
# `skip!` may be called within a step or outside of a step for journeys in the `ready` state.
|
288
|
+
# When called outside of a step, it will skip the next scheduled step and proceed to the following step.
|
289
|
+
#
|
290
|
+
# _@return_ — void
|
291
|
+
def skip!: () -> untyped
|
292
|
+
|
254
293
|
# Is used to pause a Journey at any point. The "paused" state is similar to the "ready" state, except that "perform_next_step!" on the
|
255
294
|
# journey will do nothing - even if it is scheduled to be performed. Pausing a Journey can be useful in the following situations:
|
256
295
|
#
|
data/stepper_motor.gemspec
CHANGED
@@ -37,6 +37,8 @@ Gem::Specification.new do |spec|
|
|
37
37
|
spec.add_development_dependency "minitest"
|
38
38
|
spec.add_development_dependency "rails", "~> 7.0"
|
39
39
|
spec.add_development_dependency "sqlite3"
|
40
|
+
spec.add_development_dependency "mysql2"
|
41
|
+
spec.add_development_dependency "pg"
|
40
42
|
spec.add_development_dependency "rake", "~> 13.0"
|
41
43
|
spec.add_development_dependency "standard", "~> 1.50.0", "< 2.0"
|
42
44
|
spec.add_development_dependency "magic_frozen_string_literal"
|
@@ -0,0 +1,14 @@
|
|
1
|
+
default: &default
|
2
|
+
adapter: mysql2
|
3
|
+
database: stepper_motor_dummy_<%= Rails.env %>
|
4
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
5
|
+
username: root
|
6
|
+
password: constabulary
|
7
|
+
timeout: 5000
|
8
|
+
|
9
|
+
development:
|
10
|
+
<<: *default
|
11
|
+
test:
|
12
|
+
<<: *default
|
13
|
+
production:
|
14
|
+
<<: *default
|
@@ -0,0 +1,14 @@
|
|
1
|
+
default: &default
|
2
|
+
adapter: postgres
|
3
|
+
database: stepper_motor_dummy_<%= Rails.env %>
|
4
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
5
|
+
username: root
|
6
|
+
password: constabulary
|
7
|
+
timeout: 5000
|
8
|
+
|
9
|
+
development:
|
10
|
+
<<: *default
|
11
|
+
test:
|
12
|
+
<<: *default
|
13
|
+
production:
|
14
|
+
<<: *default
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# SQLite. Versions 3.8.0 and up are supported.
|
2
|
+
# gem install sqlite3
|
3
|
+
#
|
4
|
+
# Ensure the SQLite 3 gem is defined in your Gemfile
|
5
|
+
# gem "sqlite3"
|
6
|
+
#
|
7
|
+
default: &default
|
8
|
+
adapter: sqlite3
|
9
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
10
|
+
timeout: 5000
|
11
|
+
|
12
|
+
development:
|
13
|
+
<<: *default
|
14
|
+
database: storage/development.sqlite3
|
15
|
+
|
16
|
+
# Warning: The database defined as "test" will be erased and
|
17
|
+
# re-generated from your development database when you run "rake".
|
18
|
+
# Do not set this db to the same as development or production.
|
19
|
+
test:
|
20
|
+
<<: *default
|
21
|
+
database: storage/test.sqlite3
|
22
|
+
|
23
|
+
|
24
|
+
# SQLite3 write its data on the local filesystem, as such it requires
|
25
|
+
# persistent disks. If you are deploying to a managed service, you should
|
26
|
+
# make sure it provides disk persistence, as many don't.
|
27
|
+
#
|
28
|
+
# Similarly, if you deploy your application as a Docker container, you must
|
29
|
+
# ensure the database is located in a persisted volume.
|
30
|
+
production:
|
31
|
+
<<: *default
|
32
|
+
# database: path/to/persistent/storage/production.sqlite3
|
@@ -9,7 +9,7 @@
|
|
9
9
|
#
|
10
10
|
# and add its cycle job into your recurring jobs table. For example, for solid_queue:
|
11
11
|
#
|
12
|
-
#
|
12
|
+
# run_stepper_motor_scheduling_cycle:
|
13
13
|
# schedule: "*/30 * * * *" # Every 30 minutes
|
14
14
|
# class: "StepperMotor::CyclicScheduler::RunSchedulingCycleJob"
|
15
15
|
#
|
@@ -0,0 +1,58 @@
|
|
1
|
+
class StepperMotorMigration005 < ActiveRecord::Migration[7.2]
|
2
|
+
def up
|
3
|
+
unless mysql?
|
4
|
+
say "Skipping migration as it is only used with MySQL (mysql2 or trilogy)"
|
5
|
+
return
|
6
|
+
end
|
7
|
+
|
8
|
+
# Add generated column that combines the state, type, hero_id, and hero_type
|
9
|
+
# The column will be NULL if any of the components is NULL
|
10
|
+
execute <<-SQL
|
11
|
+
ALTER TABLE stepper_motor_journeys
|
12
|
+
ADD COLUMN journey_uniq_col_generated VARCHAR(255) GENERATED ALWAYS AS (
|
13
|
+
CASE
|
14
|
+
WHEN state IN ('ready', 'performing', 'paused')
|
15
|
+
AND allow_multiple = 0
|
16
|
+
AND type IS NOT NULL
|
17
|
+
AND hero_id IS NOT NULL
|
18
|
+
AND hero_type IS NOT NULL
|
19
|
+
THEN CONCAT(type, ':', hero_id, ':', hero_type)
|
20
|
+
ELSE NULL
|
21
|
+
END
|
22
|
+
) STORED
|
23
|
+
SQL
|
24
|
+
|
25
|
+
# Add unique index on the generated column with MySQL-specific name
|
26
|
+
add_index :stepper_motor_journeys, :journey_uniq_col_generated,
|
27
|
+
unique: true,
|
28
|
+
name: :idx_journeys_one_per_hero_mysql_generated
|
29
|
+
|
30
|
+
# Remove old indexes that include 'ready', 'performing', and 'paused' states
|
31
|
+
remove_index :stepper_motor_journeys, name: :idx_journeys_one_per_hero_with_paused
|
32
|
+
end
|
33
|
+
|
34
|
+
def down
|
35
|
+
unless mysql?
|
36
|
+
say "Skipping migration as it is only used with MySQL (mysql2 or trilogy)"
|
37
|
+
return
|
38
|
+
end
|
39
|
+
|
40
|
+
# Remove the generated column and its index
|
41
|
+
remove_index :stepper_motor_journeys, name: :idx_journeys_one_per_hero_mysql_generated
|
42
|
+
remove_column :stepper_motor_journeys, :journey_uniq_col_generated
|
43
|
+
|
44
|
+
# Recreate old indexes
|
45
|
+
quoted_false = connection.quote(false)
|
46
|
+
add_index :stepper_motor_journeys, [:type, :hero_id, :hero_type],
|
47
|
+
where: "allow_multiple = '#{quoted_false}' AND state IN ('ready', 'performing', 'paused')",
|
48
|
+
unique: true,
|
49
|
+
name: :idx_journeys_one_per_hero_with_paused
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def mysql?
|
55
|
+
adapter = connection.adapter_name.downcase
|
56
|
+
adapter == "mysql2" || adapter == "trilogy"
|
57
|
+
end
|
58
|
+
end
|
data/test/dummy/db/schema.rb
CHANGED
@@ -10,8 +10,8 @@
|
|
10
10
|
#
|
11
11
|
# It's strongly recommended that you check this file into your version control system.
|
12
12
|
|
13
|
-
ActiveRecord::Schema[7.2].define(version:
|
14
|
-
create_table "stepper_motor_journeys", force: :cascade do |t|
|
13
|
+
ActiveRecord::Schema[7.2].define(version: 2025_06_09_221201) do
|
14
|
+
create_table "stepper_motor_journeys", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
15
15
|
t.string "type", null: false
|
16
16
|
t.string "state", default: "ready"
|
17
17
|
t.string "hero_type"
|
@@ -25,12 +25,13 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_28_141038) do
|
|
25
25
|
t.datetime "created_at", null: false
|
26
26
|
t.datetime "updated_at", null: false
|
27
27
|
t.string "idempotency_key"
|
28
|
+
t.virtual "journey_uniq_col_generated", type: :string, as: "case when `state` in ('ready','performing','paused') and `allow_multiple` = 0 and `type` is not null and `hero_id` is not null and `hero_type` is not null then concat(`type`,':',`hero_id`,':',`hero_type`) else NULL end", stored: true
|
28
29
|
t.index ["hero_type", "hero_id"], name: "index_stepper_motor_journeys_on_hero_type_and_hero_id"
|
29
|
-
t.index ["
|
30
|
-
t.index ["
|
30
|
+
t.index ["journey_uniq_col_generated"], name: "idx_journeys_one_per_hero_mysql_generated", unique: true
|
31
|
+
t.index ["next_step_to_be_performed_at"], name: "index_stepper_motor_journeys_on_next_step_to_be_performed_at"
|
31
32
|
t.index ["type", "hero_type", "hero_id"], name: "index_stepper_motor_journeys_on_type_and_hero_type_and_hero_id"
|
32
33
|
t.index ["type"], name: "index_stepper_motor_journeys_on_type"
|
33
|
-
t.index ["updated_at"], name: "index_stepper_motor_journeys_on_updated_at"
|
34
|
-
t.index ["updated_at"], name: "stuck_journeys_index"
|
34
|
+
t.index ["updated_at"], name: "index_stepper_motor_journeys_on_updated_at"
|
35
|
+
t.index ["updated_at"], name: "stuck_journeys_index"
|
35
36
|
end
|
36
37
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
require "test_helper"
|
4
4
|
|
5
5
|
class ExceptionHandlingTest < ActiveSupport::TestCase
|
6
|
+
include SideEffects::TestHelper
|
7
|
+
|
6
8
|
# See below.
|
7
9
|
self.use_transactional_tests = false
|
8
10
|
|
@@ -46,6 +48,61 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
46
48
|
assert faulty_journey.canceled?
|
47
49
|
end
|
48
50
|
|
51
|
+
test "with :skip!, skips the failing step and continues to next step" do
|
52
|
+
faulty_journey_class = create_journey_subclass do
|
53
|
+
step on_exception: :skip! do
|
54
|
+
raise CustomEx, "Something went wrong"
|
55
|
+
end
|
56
|
+
|
57
|
+
step do
|
58
|
+
SideEffects.touch! "second step"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
faulty_journey = faulty_journey_class.create!
|
63
|
+
assert faulty_journey.ready?
|
64
|
+
|
65
|
+
assert_raises(CustomEx) { faulty_journey.perform_next_step! }
|
66
|
+
|
67
|
+
assert faulty_journey.persisted?
|
68
|
+
refute faulty_journey.changed?
|
69
|
+
assert faulty_journey.ready?
|
70
|
+
|
71
|
+
# The second step should now be scheduled
|
72
|
+
assert_produced_side_effects("second step") do
|
73
|
+
faulty_journey.perform_next_step!
|
74
|
+
end
|
75
|
+
assert faulty_journey.finished?
|
76
|
+
end
|
77
|
+
|
78
|
+
test "with :skip! on last step, skips the failing step and finishes the journey" do
|
79
|
+
faulty_journey_class = create_journey_subclass do
|
80
|
+
step do
|
81
|
+
SideEffects.touch! "first step"
|
82
|
+
end
|
83
|
+
|
84
|
+
step on_exception: :skip! do
|
85
|
+
raise CustomEx, "Something went wrong"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
faulty_journey = faulty_journey_class.create!
|
90
|
+
assert faulty_journey.ready?
|
91
|
+
|
92
|
+
# Perform first step
|
93
|
+
assert_produced_side_effects("first step") do
|
94
|
+
faulty_journey.perform_next_step!
|
95
|
+
end
|
96
|
+
assert faulty_journey.ready?
|
97
|
+
|
98
|
+
# The second step should be skipped due to exception
|
99
|
+
assert_raises(CustomEx) { faulty_journey.perform_next_step! }
|
100
|
+
|
101
|
+
assert faulty_journey.persisted?
|
102
|
+
refute faulty_journey.changed?
|
103
|
+
assert faulty_journey.finished?
|
104
|
+
end
|
105
|
+
|
49
106
|
test "pauses the journey by default at the failig step" do
|
50
107
|
faulty_journey_class = create_journey_subclass do
|
51
108
|
step do
|
@@ -75,4 +75,168 @@ class FlowControlTest < ActiveSupport::TestCase
|
|
75
75
|
|
76
76
|
assert_no_side_effects { journey.perform_next_step! }
|
77
77
|
end
|
78
|
+
|
79
|
+
test "can skip a step and continue to next step" do
|
80
|
+
skipping_journey = create_journey_subclass do
|
81
|
+
step do
|
82
|
+
SideEffects.touch! "before skipping"
|
83
|
+
skip!
|
84
|
+
SideEffects.touch! "after skipping"
|
85
|
+
end
|
86
|
+
|
87
|
+
step do
|
88
|
+
SideEffects.touch! "second step"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
journey = skipping_journey.create!
|
93
|
+
assert journey.ready?
|
94
|
+
|
95
|
+
# First step should be skipped
|
96
|
+
assert_produced_side_effects("before skipping") do
|
97
|
+
assert_did_not_produce_side_effects("after skipping") do
|
98
|
+
journey.perform_next_step!
|
99
|
+
end
|
100
|
+
end
|
101
|
+
assert journey.ready?
|
102
|
+
|
103
|
+
# Second step should be performed
|
104
|
+
assert_produced_side_effects("second step") do
|
105
|
+
journey.perform_next_step!
|
106
|
+
end
|
107
|
+
assert journey.finished?
|
108
|
+
end
|
109
|
+
|
110
|
+
test "can skip the last step and finish the journey" do
|
111
|
+
skipping_journey = create_journey_subclass do
|
112
|
+
step do
|
113
|
+
SideEffects.touch! "first step"
|
114
|
+
end
|
115
|
+
|
116
|
+
step do
|
117
|
+
SideEffects.touch! "before skipping last"
|
118
|
+
skip!
|
119
|
+
SideEffects.touch! "after skipping last"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
journey = skipping_journey.create!
|
124
|
+
assert journey.ready?
|
125
|
+
|
126
|
+
# First step should be performed normally
|
127
|
+
assert_produced_side_effects("first step") do
|
128
|
+
journey.perform_next_step!
|
129
|
+
end
|
130
|
+
assert journey.ready?
|
131
|
+
|
132
|
+
# Last step should be skipped and journey should finish
|
133
|
+
assert_produced_side_effects("before skipping last") do
|
134
|
+
assert_did_not_produce_side_effects("after skipping last") do
|
135
|
+
journey.perform_next_step!
|
136
|
+
end
|
137
|
+
end
|
138
|
+
assert journey.finished?
|
139
|
+
end
|
140
|
+
|
141
|
+
test "skip! can be called outside of a step for ready journeys" do
|
142
|
+
skipping_journey = create_journey_subclass do
|
143
|
+
step do
|
144
|
+
SideEffects.touch! "first step"
|
145
|
+
end
|
146
|
+
|
147
|
+
step do
|
148
|
+
SideEffects.touch! "second step"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
journey = skipping_journey.create!
|
153
|
+
assert journey.ready?
|
154
|
+
|
155
|
+
# Skip the first step from outside
|
156
|
+
journey.skip!
|
157
|
+
assert journey.ready?
|
158
|
+
|
159
|
+
# The second step should now be scheduled
|
160
|
+
assert_produced_side_effects("second step") do
|
161
|
+
journey.perform_next_step!
|
162
|
+
end
|
163
|
+
assert journey.finished?
|
164
|
+
end
|
165
|
+
|
166
|
+
test "skip! outside of step raises error for non-ready journeys" do
|
167
|
+
skipping_journey = create_journey_subclass do
|
168
|
+
step do
|
169
|
+
SideEffects.touch! "step completed"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
journey = skipping_journey.create!
|
174
|
+
journey.pause!
|
175
|
+
assert journey.paused?
|
176
|
+
|
177
|
+
assert_raises(RuntimeError, "skip! can only be used on journeys in the `ready` state") do
|
178
|
+
journey.skip!
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
test "skip! outside of step can finish journey when skipping last step" do
|
183
|
+
skipping_journey = create_journey_subclass do
|
184
|
+
step do
|
185
|
+
SideEffects.touch! "first step"
|
186
|
+
end
|
187
|
+
|
188
|
+
step do
|
189
|
+
SideEffects.touch! "last step"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
journey = skipping_journey.create!
|
194
|
+
assert journey.ready?
|
195
|
+
|
196
|
+
# Perform first step
|
197
|
+
assert_produced_side_effects("first step") do
|
198
|
+
journey.perform_next_step!
|
199
|
+
end
|
200
|
+
assert journey.ready?
|
201
|
+
|
202
|
+
# Skip the last step from outside
|
203
|
+
journey.skip!
|
204
|
+
assert journey.finished?
|
205
|
+
end
|
206
|
+
|
207
|
+
test "skip! outside of step handles missing step definitions gracefully" do
|
208
|
+
skipping_journey = create_journey_subclass do
|
209
|
+
step do
|
210
|
+
SideEffects.touch! "step completed"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
journey = skipping_journey.create!
|
215
|
+
assert journey.ready?
|
216
|
+
|
217
|
+
# Manually set a non-existent next step
|
218
|
+
journey.update!(next_step_name: "non_existent_step")
|
219
|
+
|
220
|
+
# Skip should handle this gracefully and finish the journey
|
221
|
+
journey.skip!
|
222
|
+
assert journey.finished?
|
223
|
+
end
|
224
|
+
|
225
|
+
test "skip! aborts the current step execution" do
|
226
|
+
skipping_journey = create_journey_subclass do
|
227
|
+
step do
|
228
|
+
SideEffects.touch! "before skip"
|
229
|
+
skip!
|
230
|
+
SideEffects.touch! "after skip"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
journey = skipping_journey.create!
|
235
|
+
|
236
|
+
assert_produced_side_effects("before skip") do
|
237
|
+
assert_did_not_produce_side_effects("after skip") do
|
238
|
+
journey.perform_next_step!
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
78
242
|
end
|