stepper_motor 0.1.7 → 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/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 +0 -1
- 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,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class CyclicSchedulerTest < ActiveSupport::TestCase
|
6
|
+
include ActiveJob::TestHelper
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@previous_scheduler = StepperMotor.scheduler
|
10
|
+
StepperMotor::Journey.delete_all
|
11
|
+
end
|
12
|
+
|
13
|
+
teardown do
|
14
|
+
StepperMotor.scheduler = @previous_scheduler
|
15
|
+
end
|
16
|
+
|
17
|
+
def far_future_journey_class
|
18
|
+
@klass ||= create_journey_subclass do
|
19
|
+
step :do_thing, wait: 40.minutes do
|
20
|
+
raise "We do not test this so it should never run"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
test "does not schedule a journey which is too far in the future" do
|
26
|
+
scheduler = StepperMotor::CyclicScheduler.new(cycle_duration: 30.seconds)
|
27
|
+
StepperMotor.scheduler = scheduler
|
28
|
+
|
29
|
+
assert_no_enqueued_jobs do
|
30
|
+
far_future_journey_class.create!
|
31
|
+
end
|
32
|
+
|
33
|
+
assert_no_enqueued_jobs do
|
34
|
+
scheduler.run_scheduling_cycle
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
test "for a job inside the current scheduling cycle, enqueues the job immediately" do
|
39
|
+
scheduler = StepperMotor::CyclicScheduler.new(cycle_duration: 40.minutes)
|
40
|
+
StepperMotor.scheduler = scheduler
|
41
|
+
|
42
|
+
assert_enqueued_jobs 1, only: StepperMotor::PerformStepJobV2 do
|
43
|
+
far_future_journey_class.create!
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
test "also schedules journeys which had to run in the past" do
|
48
|
+
scheduler = StepperMotor::CyclicScheduler.new(cycle_duration: 10.seconds)
|
49
|
+
StepperMotor.scheduler = scheduler
|
50
|
+
|
51
|
+
journey = nil
|
52
|
+
assert_no_enqueued_jobs do
|
53
|
+
journey = far_future_journey_class.create!
|
54
|
+
end
|
55
|
+
journey.update!(next_step_to_be_performed_at: 10.minutes.ago)
|
56
|
+
|
57
|
+
assert_enqueued_jobs 1, only: StepperMotor::PerformStepJobV2 do
|
58
|
+
scheduler.run_scheduling_cycle
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
test "performs the scheduling job without raising exceptions even if the cycling scheduler is not the one active" do
|
63
|
+
StepperMotor.scheduler = StepperMotor::ForwardScheduler.new
|
64
|
+
assert_nothing_raised do
|
65
|
+
StepperMotor::CyclicScheduler::RunSchedulingCycleJob.new.perform
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
test "performs the scheduling job without raising exceptions if the cycling scheduler is the active" do
|
70
|
+
scheduler = StepperMotor::CyclicScheduler.new(cycle_duration: 10.seconds)
|
71
|
+
StepperMotor.scheduler = scheduler
|
72
|
+
|
73
|
+
assert_nothing_raised do
|
74
|
+
StepperMotor::CyclicScheduler::RunSchedulingCycleJob.new.perform
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/{spec/stepper_motor/forward_scheduler_spec.rb → test/stepper_motor/forward_scheduler_test.rb}
RENAMED
@@ -1,16 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "test_helper"
|
4
4
|
|
5
|
-
|
5
|
+
class ForwardSchedulerTest < ActiveSupport::TestCase
|
6
6
|
include ActiveJob::TestHelper
|
7
7
|
|
8
|
-
|
8
|
+
setup do
|
9
9
|
@previous_scheduler = StepperMotor.scheduler
|
10
10
|
StepperMotor::Journey.delete_all
|
11
11
|
end
|
12
12
|
|
13
|
-
|
13
|
+
teardown do
|
14
14
|
StepperMotor.scheduler = @previous_scheduler
|
15
15
|
end
|
16
16
|
|
@@ -22,20 +22,19 @@ RSpec.describe "StepperMotor::ForwardScheduler" do
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
-
|
25
|
+
test "schedules a journey 40 minutes ahead" do
|
26
26
|
scheduler = StepperMotor::ForwardScheduler.new
|
27
27
|
StepperMotor.scheduler = scheduler
|
28
28
|
|
29
|
-
expect(scheduler).to receive(:schedule).with(instance_of(far_future_journey_class)).once.and_call_original
|
30
29
|
_journey = far_future_journey_class.create!
|
31
30
|
|
32
|
-
|
31
|
+
assert_equal 1, enqueued_jobs.size
|
33
32
|
job = enqueued_jobs.first
|
34
33
|
|
35
|
-
|
36
|
-
|
34
|
+
assert_equal "StepperMotor::PerformStepJobV2", job["job_class"]
|
35
|
+
assert_not_nil job["scheduled_at"]
|
37
36
|
|
38
37
|
scheduled_at = Time.parse(job["scheduled_at"])
|
39
|
-
|
38
|
+
assert_in_delta 40.minutes.from_now.to_f, scheduled_at.to_f, 5.seconds.to_f
|
40
39
|
end
|
41
40
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class ExceptionHandlingTest < ActiveSupport::TestCase
|
6
|
+
# See below.
|
7
|
+
self.use_transactional_tests = false
|
8
|
+
|
9
|
+
class CustomEx < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
test "with :reattempt!, reattempts the failing step and bumps the idempotency key" do
|
13
|
+
faulty_journey_class = create_journey_subclass do
|
14
|
+
step on_exception: :reattempt! do
|
15
|
+
raise CustomEx, "Something went wrong"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
faulty_journey = faulty_journey_class.create!
|
20
|
+
assert faulty_journey.ready?
|
21
|
+
ik_before_step = faulty_journey.idempotency_key
|
22
|
+
|
23
|
+
assert_raises(CustomEx) { faulty_journey.perform_next_step! }
|
24
|
+
|
25
|
+
assert faulty_journey.persisted?
|
26
|
+
refute faulty_journey.changed?
|
27
|
+
assert faulty_journey.ready?
|
28
|
+
refute_equal faulty_journey.idempotency_key, ik_before_step
|
29
|
+
end
|
30
|
+
|
31
|
+
test "with :cancel!, cancels at the failig step" do
|
32
|
+
faulty_journey_class = create_journey_subclass do
|
33
|
+
step on_exception: :cancel! do
|
34
|
+
raise CustomEx, "Something went wrong"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
faulty_journey = faulty_journey_class.create!
|
39
|
+
assert faulty_journey.ready?
|
40
|
+
faulty_journey.idempotency_key
|
41
|
+
|
42
|
+
assert_raises(CustomEx) { faulty_journey.perform_next_step! }
|
43
|
+
|
44
|
+
assert faulty_journey.persisted?
|
45
|
+
refute faulty_journey.changed?
|
46
|
+
assert faulty_journey.canceled?
|
47
|
+
end
|
48
|
+
|
49
|
+
test "pauses the journey by default at the failig step" do
|
50
|
+
faulty_journey_class = create_journey_subclass do
|
51
|
+
step do
|
52
|
+
raise CustomEx, "Something went wrong"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
faulty_journey = faulty_journey_class.create!
|
57
|
+
assert faulty_journey.ready?
|
58
|
+
faulty_journey.idempotency_key
|
59
|
+
|
60
|
+
assert_raises(CustomEx) { faulty_journey.perform_next_step! }
|
61
|
+
|
62
|
+
assert faulty_journey.persisted?
|
63
|
+
refute faulty_journey.changed?
|
64
|
+
assert faulty_journey.paused?
|
65
|
+
end
|
66
|
+
|
67
|
+
test "is able to get the journey into reattempt even if the step has caused an invalid transaction" do
|
68
|
+
# We need to test a situation where a Journey causes a database transaction
|
69
|
+
# becoming invalid due to an invalid statement. Since we work with the same database
|
70
|
+
# as the code of the step, we won't be able to perform any SQL statments if the transaction
|
71
|
+
# gets left in the broken state and is not rolled back before we try to persist the failing
|
72
|
+
# Journey.
|
73
|
+
|
74
|
+
faulty_journey_class = create_journey_subclass do
|
75
|
+
step on_exception: :reattempt! do
|
76
|
+
StepperMotor::Journey.connection.execute("KERFUFFLE")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
faulty_journey = faulty_journey_class.create!
|
81
|
+
assert faulty_journey.ready?
|
82
|
+
ik_before_step = faulty_journey.idempotency_key
|
83
|
+
assert_raises(ActiveRecord::StatementInvalid) { faulty_journey.perform_next_step! }
|
84
|
+
|
85
|
+
assert faulty_journey.persisted?
|
86
|
+
refute faulty_journey.changed?
|
87
|
+
refute_equal faulty_journey.idempotency_key, ik_before_step
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class FlowControlTest < ActiveSupport::TestCase
|
6
|
+
include ActiveJob::TestHelper
|
7
|
+
include SideEffects::TestHelper
|
8
|
+
|
9
|
+
test "can pause a Journey during a step" do
|
10
|
+
pausing_journey = create_journey_subclass do
|
11
|
+
step do
|
12
|
+
SideEffects.touch! "before pausing"
|
13
|
+
pause!
|
14
|
+
SideEffects.touch! "after pausing"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
journey = pausing_journey.create!
|
19
|
+
assert journey.ready?
|
20
|
+
|
21
|
+
assert_produced_side_effects("before pausing") do
|
22
|
+
assert_did_not_produce_side_effects("after pausing") do
|
23
|
+
journey.perform_next_step!
|
24
|
+
end
|
25
|
+
end
|
26
|
+
assert journey.paused?
|
27
|
+
end
|
28
|
+
|
29
|
+
test "schedules a job on resume" do
|
30
|
+
pausing_journey = create_journey_subclass do
|
31
|
+
step do
|
32
|
+
SideEffects.touch! "after resume"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
journey = pausing_journey.create!
|
37
|
+
journey.pause!
|
38
|
+
assert journey.paused?
|
39
|
+
|
40
|
+
clear_enqueued_jobs
|
41
|
+
|
42
|
+
assert_produced_side_effects("after resume") do
|
43
|
+
journey.resume!
|
44
|
+
perform_enqueued_jobs
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
test "changes the idempotency key at resume" do
|
49
|
+
pausing_journey = create_journey_subclass do
|
50
|
+
step do
|
51
|
+
SideEffects.touch! "after resume"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
journey = pausing_journey.create!
|
56
|
+
journey.pause!
|
57
|
+
assert journey.paused?
|
58
|
+
ik_before = journey.idempotency_key
|
59
|
+
|
60
|
+
journey.resume!
|
61
|
+
assert journey.persisted?
|
62
|
+
refute_equal ik_before, journey.idempotency_key
|
63
|
+
end
|
64
|
+
|
65
|
+
test "does not perform the step on job that has been paused" do
|
66
|
+
pausing_journey = create_journey_subclass do
|
67
|
+
step do
|
68
|
+
SideEffects.touch! "should not run"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
journey = pausing_journey.create!
|
73
|
+
journey.pause!
|
74
|
+
assert journey.paused?
|
75
|
+
|
76
|
+
assert_no_side_effects { journey.perform_next_step! }
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class JourneyIdempotencyTest < ActiveSupport::TestCase
|
6
|
+
include ActiveJob::TestHelper
|
7
|
+
include SideEffects::TestHelper
|
8
|
+
|
9
|
+
test "does not perform the step if the idempotency key passed does not match the one stored" do
|
10
|
+
journey_class = create_journey_subclass do
|
11
|
+
step do
|
12
|
+
raise "Should not happen"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
journey = journey_class.create!
|
17
|
+
|
18
|
+
assert_predicate journey, :ready?
|
19
|
+
assert journey.idempotency_key
|
20
|
+
assert_nothing_raised do
|
21
|
+
journey.perform_next_step!(idempotency_key: journey.idempotency_key + "n")
|
22
|
+
end
|
23
|
+
assert_predicate journey, :ready?
|
24
|
+
end
|
25
|
+
|
26
|
+
test "does perform the step if the idempotency key is set but not passed to perform_next_step!" do
|
27
|
+
journey_class = create_journey_subclass do
|
28
|
+
step do
|
29
|
+
SideEffects.touch! :with_ik
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
journey = journey_class.create!
|
34
|
+
|
35
|
+
assert_predicate journey, :ready?
|
36
|
+
assert journey.idempotency_key
|
37
|
+
assert_produced_side_effects(:with_ik) do
|
38
|
+
journey.perform_next_step!
|
39
|
+
end
|
40
|
+
assert_predicate journey, :finished?
|
41
|
+
end
|
42
|
+
|
43
|
+
test "updates the idempotency key when a step gets reattempted" do
|
44
|
+
some_journey_class = create_journey_subclass do
|
45
|
+
step :one do
|
46
|
+
reattempt! wait: 2.minutes
|
47
|
+
end
|
48
|
+
end
|
49
|
+
freeze_time
|
50
|
+
|
51
|
+
journey = some_journey_class.create!
|
52
|
+
|
53
|
+
assert_equal "one", journey.next_step_name
|
54
|
+
assert journey.idempotency_key.present? # Since it was scheduled for the initial step
|
55
|
+
previous_idempotency_key = journey.idempotency_key
|
56
|
+
|
57
|
+
perform_enqueued_jobs # Should be reattempted
|
58
|
+
|
59
|
+
journey.reload
|
60
|
+
|
61
|
+
assert_equal "one", journey.next_step_name
|
62
|
+
assert journey.idempotency_key
|
63
|
+
refute_equal journey.idempotency_key, previous_idempotency_key
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
require "minitest/mock"
|
5
|
+
|
6
|
+
class StepDefinitionTest < ActiveSupport::TestCase
|
7
|
+
include ActiveJob::TestHelper
|
8
|
+
include SideEffects::TestHelper
|
9
|
+
|
10
|
+
test "requires either a block or a name" do
|
11
|
+
assert_raises(StepperMotor::StepConfigurationError) do
|
12
|
+
create_journey_subclass do
|
13
|
+
step
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
test "passes any additional options to the step definition" do
|
19
|
+
step_def = StepperMotor::Step.new(name: "a_step", seq: 1, on_exception: :reattempt!)
|
20
|
+
assert_extra_arguments = ->(**options) {
|
21
|
+
assert options.key?(:extra)
|
22
|
+
# Return the original definition
|
23
|
+
step_def
|
24
|
+
}
|
25
|
+
|
26
|
+
StepperMotor::Step.stub :new, assert_extra_arguments do
|
27
|
+
create_journey_subclass do
|
28
|
+
step extra: true do
|
29
|
+
# noop
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
test "returns the created step definition" do
|
36
|
+
test_case = self # To pass it into the class_eval of create_journey_subclass
|
37
|
+
create_journey_subclass do
|
38
|
+
step_def1 = step do
|
39
|
+
# noop
|
40
|
+
end
|
41
|
+
|
42
|
+
step_def2 = step :another_step do
|
43
|
+
# noop
|
44
|
+
end
|
45
|
+
|
46
|
+
test_case.assert_kind_of StepperMotor::Step, step_def1
|
47
|
+
test_case.assert_kind_of StepperMotor::Step, step_def2
|
48
|
+
test_case.assert_equal "another_step", step_def2.name
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
test "allows steps to be defined as instance method names" do
|
53
|
+
journey_class = create_journey_subclass do
|
54
|
+
step :one
|
55
|
+
step :two
|
56
|
+
|
57
|
+
def one
|
58
|
+
SideEffects.touch!("from method one")
|
59
|
+
end
|
60
|
+
|
61
|
+
def two
|
62
|
+
SideEffects.touch!("from method two")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
journey = journey_class.create!
|
67
|
+
|
68
|
+
assert_produced_side_effects("from method one", "from method two") do
|
69
|
+
2.times { journey.perform_next_step! }
|
70
|
+
end
|
71
|
+
|
72
|
+
assert journey.finished?
|
73
|
+
end
|
74
|
+
|
75
|
+
test "raises a custom NoMethodError when a blockless step was defined but no method to carry it" do
|
76
|
+
journey_class = create_journey_subclass do
|
77
|
+
step :one
|
78
|
+
end
|
79
|
+
|
80
|
+
journey = journey_class.create!
|
81
|
+
|
82
|
+
ex = assert_raises(NoMethodError) do
|
83
|
+
journey.perform_next_step!
|
84
|
+
end
|
85
|
+
|
86
|
+
assert_kind_of NoMethodError, ex
|
87
|
+
assert_match(/No block or method/, ex.message)
|
88
|
+
end
|
89
|
+
|
90
|
+
test "allows `step def'" do
|
91
|
+
journey_class = create_journey_subclass do
|
92
|
+
step def one
|
93
|
+
SideEffects.touch!(:woof)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
journey = journey_class.create!
|
98
|
+
assert_produced_side_effects(:woof) do
|
99
|
+
journey.perform_next_step!
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
test "adds steps to step_definitions" do
|
104
|
+
journey_class = create_journey_subclass do
|
105
|
+
step :one do
|
106
|
+
# noop
|
107
|
+
end
|
108
|
+
step :two, wait: 20.minutes do
|
109
|
+
# noop
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
assert_kind_of Array, journey_class.step_definitions
|
114
|
+
assert_equal 2, journey_class.step_definitions.length
|
115
|
+
|
116
|
+
step_one, step_two = *journey_class.step_definitions
|
117
|
+
|
118
|
+
assert_equal "one", step_one.name
|
119
|
+
assert_equal 0, step_one.wait
|
120
|
+
|
121
|
+
assert_equal "two", step_two.name
|
122
|
+
assert_equal 20.minutes, step_two.wait
|
123
|
+
end
|
124
|
+
|
125
|
+
test "gives automatic names to anonymous steps" do
|
126
|
+
journey_class = create_journey_subclass do
|
127
|
+
step :one do
|
128
|
+
# noop
|
129
|
+
end
|
130
|
+
step wait: 20.minutes do
|
131
|
+
# noop
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
assert_kind_of Array, journey_class.step_definitions
|
136
|
+
assert_equal 2, journey_class.step_definitions.length
|
137
|
+
|
138
|
+
step_one, step_two = *journey_class.step_definitions
|
139
|
+
|
140
|
+
assert_equal "one", step_one.name
|
141
|
+
assert_equal "step_2", step_two.name
|
142
|
+
end
|
143
|
+
|
144
|
+
test "does not allow invalid values for after: and wait:" do
|
145
|
+
assert_raises(ArgumentError) do
|
146
|
+
create_journey_subclass do
|
147
|
+
step after: 10.hours do
|
148
|
+
# pass
|
149
|
+
end
|
150
|
+
|
151
|
+
step after: 5.hours do
|
152
|
+
# pass
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
assert_raises(ArgumentError) do
|
158
|
+
create_journey_subclass do
|
159
|
+
step wait: -5.hours do
|
160
|
+
# pass
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
assert_raises(ArgumentError) do
|
166
|
+
create_journey_subclass do
|
167
|
+
step after: 5.hours, wait: 2.seconds do
|
168
|
+
# pass
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
test "forbids multiple steps with the same name within a journey" do
|
175
|
+
assert_raises(ArgumentError) do
|
176
|
+
create_journey_subclass do
|
177
|
+
step :foo do
|
178
|
+
true
|
179
|
+
end
|
180
|
+
|
181
|
+
step "foo" do
|
182
|
+
true
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class UniquenessTest < ActiveSupport::TestCase
|
6
|
+
test "forbids multiple similar journeys for the same hero at the same time unless allow_multiple is set" do
|
7
|
+
actor_class = create_journey_subclass
|
8
|
+
hero = actor_class.create!
|
9
|
+
|
10
|
+
exclusive_journey_class = create_journey_subclass do
|
11
|
+
step do
|
12
|
+
raise "The step should never be entered as we are not testing the step itself here"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
assert_nothing_raised do
|
17
|
+
2.times { exclusive_journey_class.create! }
|
18
|
+
end
|
19
|
+
|
20
|
+
assert_raises(ActiveRecord::RecordNotUnique) do
|
21
|
+
2.times { exclusive_journey_class.create!(hero: hero) }
|
22
|
+
end
|
23
|
+
|
24
|
+
assert_nothing_raised do
|
25
|
+
2.times { exclusive_journey_class.create!(hero: hero, allow_multiple: true) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
test "forbids multiple similar journeys for the same hero even if one of them is paused" do
|
30
|
+
actor_class = create_journey_subclass
|
31
|
+
hero = actor_class.create!
|
32
|
+
|
33
|
+
exclusive_journey_class = create_journey_subclass do
|
34
|
+
step do
|
35
|
+
raise "The step should never be entered as we are not testing the step itself here"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
ready_journey = exclusive_journey_class.create!(hero:)
|
40
|
+
assert ready_journey.ready?
|
41
|
+
ready_journey.pause!
|
42
|
+
assert ready_journey.paused?
|
43
|
+
|
44
|
+
assert_raises(ActiveRecord::RecordNotUnique) {
|
45
|
+
exclusive_journey_class.create!(hero:)
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|