stepper_motor 0.1.2 → 0.1.3
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/README.md +5 -5
- data/lib/generators/stepper_motor_migration_001.rb.erb +2 -1
- data/lib/stepper_motor/forward_scheduler.rb +1 -3
- data/lib/stepper_motor/journey.rb +1 -1
- data/lib/stepper_motor/version.rb +1 -1
- data/spec/helpers/side_effects.rb +7 -0
- data/spec/spec_helper.rb +36 -0
- data/spec/stepper_motor/cyclic_scheduler_spec.rb +16 -15
- data/spec/stepper_motor/journey_spec.rb +53 -78
- data/spec/stepper_motor/test_helper_spec.rb +15 -13
- metadata +36 -8
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 2496495ef6f6748068d2a726432ef1e23c8c17880b464da61530457396342701
         | 
| 4 | 
            +
              data.tar.gz: 003f7998b41ccf526038de029a0e839ebc9a413ddc7cf1fed47c2f2ec07e96e5
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: d522012c349187573966dacf65f90ecc476ccc1f34e20d818d7c4619b6804881491b9058361f16cf2eeb020a15b837645c76e2a951f8c5620dd3c9d6ba79e42f
         | 
| 7 | 
            +
              data.tar.gz: 97a05abbd99ef244d5de94f37f50d10338e4618dd4b103a7efeffa5de4957e3e0dce854ebd8122f51b6e8c15d7a8735c108950df5f24adf6456f6136f767128e
         | 
    
        data/README.md
    CHANGED
    
    | @@ -16,7 +16,7 @@ class SignupJourney < StepperMotor::Journey | |
| 16 16 | 
             
                ServiceUpdateMailer.two_days_spent_email(hero).deliver_later
         | 
| 17 17 | 
             
              end
         | 
| 18 18 |  | 
| 19 | 
            -
              step : | 
| 19 | 
            +
              step :onboarding_complete, wait: 15.days do
         | 
| 20 20 | 
             
                OnboardingCompleteMailer.onboarding_complete_email(hero).deliver_later
         | 
| 21 21 | 
             
              end
         | 
| 22 22 | 
             
            end
         | 
| @@ -358,16 +358,16 @@ This creates a large number of jobs on your queue, but will be easier to manage. | |
| 358 358 | 
             
            ```ruby
         | 
| 359 359 | 
             
            StepperMotor.configure do |c|
         | 
| 360 360 | 
             
              # Use jobs per journey step and enqueue them early
         | 
| 361 | 
            -
              c. | 
| 361 | 
            +
              c.scheduler = StepperMotor::ForwardScheduler.new
         | 
| 362 362 | 
             
            end
         | 
| 363 363 | 
             
            ```
         | 
| 364 364 |  | 
| 365 | 
            -
            or, for  | 
| 365 | 
            +
            or, for cyclic scheduling (less jobs on the queue, but you need a decent scheduler for your background jobs to be present:
         | 
| 366 366 |  | 
| 367 367 | 
             
            ```ruby
         | 
| 368 368 | 
             
            StepperMotor.configure do |c|
         | 
| 369 | 
            -
              #  | 
| 370 | 
            -
              c. | 
| 369 | 
            +
              # Check for jobs to be created every 5 minutes
         | 
| 370 | 
            +
              c.scheduler = StepperMotor::CyclicScheduler.new((cycle_duration: 5.minutes)
         | 
| 371 371 | 
             
            end
         | 
| 372 372 | 
             
            ```
         | 
| 373 373 |  | 
| @@ -29,7 +29,8 @@ class StepperMotorMigration001 < ActiveRecord::Migration[<%= migration_version % | |
| 29 29 | 
             
                add_index :stepper_motor_journeys, [:type, :hero_type, :hero_id]
         | 
| 30 30 |  | 
| 31 31 | 
             
                # A unique index prevents multiple journeys of the same type from being created for a particular hero
         | 
| 32 | 
            -
                 | 
| 32 | 
            +
                quoted_false = connection.quote(false)
         | 
| 33 | 
            +
                add_index :stepper_motor_journeys, [:type, :hero_id, :hero_type], where: "allow_multiple = '#{quoted_false}' AND state IN ('ready', 'performing')", unique: true, name: :one_per_hero_index
         | 
| 33 34 |  | 
| 34 35 | 
             
                # An index is also needed for cleaning up finished and canceled journeys quickly
         | 
| 35 36 | 
             
                # for a specific hero of a specific class
         | 
| @@ -13,8 +13,6 @@ | |
| 13 13 | 
             
            # this scheduler is not a good fit for you, and you will need to use the {CyclicScheduler} instead.
         | 
| 14 14 | 
             
            class StepperMotor::ForwardScheduler
         | 
| 15 15 | 
             
              def schedule(journey)
         | 
| 16 | 
            -
                 | 
| 17 | 
            -
                wait = 0 if wait.negative?
         | 
| 18 | 
            -
                StepperMotor::PerformStepJob.set(wait: wait).perform_later(journey.to_global_id.to_s)
         | 
| 16 | 
            +
                StepperMotor::PerformStepJob.set(scheduled_at: journey.next_step_to_be_performed_at).perform_later(journey.to_global_id.to_s)
         | 
| 19 17 | 
             
              end
         | 
| 20 18 | 
             
            end
         | 
| @@ -44,7 +44,7 @@ module StepperMotor | |
| 44 44 | 
             
                belongs_to :hero, polymorphic: true, optional: true
         | 
| 45 45 |  | 
| 46 46 | 
             
                STATES = %w[ready performing canceled finished]
         | 
| 47 | 
            -
                enum state | 
| 47 | 
            +
                enum :state, STATES.zip(STATES).to_h, default: "ready"
         | 
| 48 48 |  | 
| 49 49 | 
             
                # Allows querying for journeys for this specific hero. This uses a scope for convenience as the hero
         | 
| 50 50 | 
             
                # is referenced using it's global ID (same ID that ActiveJob uses for serialization)
         | 
| @@ -24,6 +24,13 @@ module SideEffects | |
| 24 24 | 
             
              end
         | 
| 25 25 |  | 
| 26 26 | 
             
              def self.touch!(name)
         | 
| 27 | 
            +
                if Thread.current[:side_effects].nil?
         | 
| 28 | 
            +
                  raise <<~ERROR
         | 
| 29 | 
            +
                    The current thread locals do not contain :side_effects, which means that your job
         | 
| 30 | 
            +
                    is running on a different thread than the specs. This is probably due to bad configuration
         | 
| 31 | 
            +
                    of the ActiveJob test adapter.
         | 
| 32 | 
            +
                  ERROR
         | 
| 33 | 
            +
                end
         | 
| 27 34 | 
             
                Thread.current[:side_effects][name.to_s] = true
         | 
| 28 35 | 
             
              end
         | 
| 29 36 |  | 
    
        data/spec/spec_helper.rb
    CHANGED
    
    | @@ -6,6 +6,7 @@ require "active_job" | |
| 6 6 | 
             
            require "active_record"
         | 
| 7 7 | 
             
            require "globalid"
         | 
| 8 8 | 
             
            require_relative "helpers/side_effects"
         | 
| 9 | 
            +
            require "fileutils"
         | 
| 9 10 |  | 
| 10 11 | 
             
            module StepperMotorRailtieTestHelpers
         | 
| 11 12 | 
             
              def establish_test_connection
         | 
| @@ -34,6 +35,33 @@ module StepperMotorRailtieTestHelpers | |
| 34 35 | 
             
                ActiveRecord::Tasks::DatabaseTasks.root = fake_app_root
         | 
| 35 36 | 
             
                ActiveRecord::Tasks::DatabaseTasks.migrate
         | 
| 36 37 | 
             
              end
         | 
| 38 | 
            +
              extend self
         | 
| 39 | 
            +
            end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
            module ActiveSupportTestCaseMethodsStub
         | 
| 42 | 
            +
              def self.included(into)
         | 
| 43 | 
            +
                into.before(:each) { before_setup } 
         | 
| 44 | 
            +
                into.after(:each) { after_teardown }
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              def before_setup
         | 
| 48 | 
            +
                # Blank implementation as TestCase modules super() into it
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
              def after_teardown
         | 
| 52 | 
            +
                # Blank implementation as TestCase modules super() into it
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
            end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
            module JourneyDefinitionHelper
         | 
| 57 | 
            +
              def create_journey_subclass(&blk)
         | 
| 58 | 
            +
                # https://stackoverflow.com/questions/4113479/dynamic-class-definition-with-a-class-name
         | 
| 59 | 
            +
                random_component = Random.hex(8)
         | 
| 60 | 
            +
                random_name = "JourneySubclass#{random_component}"
         | 
| 61 | 
            +
                klass = Class.new(StepperMotor::Journey, &blk)
         | 
| 62 | 
            +
                Object.const_set(random_name, klass)
         | 
| 63 | 
            +
                klass
         | 
| 64 | 
            +
              end
         | 
| 37 65 | 
             
            end
         | 
| 38 66 |  | 
| 39 67 | 
             
            RSpec.configure do |config|
         | 
| @@ -50,4 +78,12 @@ RSpec.configure do |config| | |
| 50 78 | 
             
              config.include ActiveSupport::Testing::TimeHelpers
         | 
| 51 79 | 
             
              config.include StepperMotorRailtieTestHelpers
         | 
| 52 80 | 
             
              config.include SideEffects::SpecHelper
         | 
| 81 | 
            +
              config.include ActiveSupportTestCaseMethodsStub
         | 
| 82 | 
            +
              config.include JourneyDefinitionHelper
         | 
| 83 | 
            +
             | 
| 84 | 
            +
              config.before :suite do
         | 
| 85 | 
            +
                StepperMotorRailtieTestHelpers.establish_test_connection
         | 
| 86 | 
            +
                StepperMotorRailtieTestHelpers.run_generator
         | 
| 87 | 
            +
                StepperMotorRailtieTestHelpers.run_migrations
         | 
| 88 | 
            +
              end
         | 
| 53 89 | 
             
            end
         | 
| @@ -3,17 +3,8 @@ require_relative "../spec_helper" | |
| 3 3 | 
             
            RSpec.describe "StepperMotor::CyclicScheduler" do
         | 
| 4 4 | 
             
              include ActiveJob::TestHelper
         | 
| 5 5 |  | 
| 6 | 
            -
              class FarFutureJourney < StepperMotor::Journey
         | 
| 7 | 
            -
                step :do_thing, wait: 40.minutes do
         | 
| 8 | 
            -
                  raise "We do not test this so it should never run"
         | 
| 9 | 
            -
                end
         | 
| 10 | 
            -
              end
         | 
| 11 | 
            -
             | 
| 12 6 | 
             
              before do
         | 
| 13 7 | 
             
                @previous_scheduler = StepperMotor.scheduler
         | 
| 14 | 
            -
                establish_test_connection
         | 
| 15 | 
            -
                run_generator
         | 
| 16 | 
            -
                run_migrations
         | 
| 17 8 | 
             
                StepperMotor::Journey.delete_all
         | 
| 18 9 | 
             
              end
         | 
| 19 10 |  | 
| @@ -21,12 +12,22 @@ RSpec.describe "StepperMotor::CyclicScheduler" do | |
| 21 12 | 
             
                StepperMotor.scheduler = @previous_scheduler
         | 
| 22 13 | 
             
              end
         | 
| 23 14 |  | 
| 15 | 
            +
              def far_future_journey_class
         | 
| 16 | 
            +
                @klass ||= begin
         | 
| 17 | 
            +
                  create_journey_subclass do
         | 
| 18 | 
            +
                    step :do_thing, wait: 40.minutes do
         | 
| 19 | 
            +
                      raise "We do not test this so it should never run"
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
             | 
| 24 25 | 
             
              it "does not schedule a journey which is too far in the future" do
         | 
| 25 26 | 
             
                scheduler = StepperMotor::CyclicScheduler.new(cycle_duration: 30.seconds)
         | 
| 26 27 | 
             
                StepperMotor.scheduler = scheduler
         | 
| 27 28 |  | 
| 28 | 
            -
                expect(scheduler).to receive(:schedule).with(instance_of( | 
| 29 | 
            -
                _journey =  | 
| 29 | 
            +
                expect(scheduler).to receive(:schedule).with(instance_of(far_future_journey_class)).once.and_call_original
         | 
| 30 | 
            +
                _journey = far_future_journey_class.create!
         | 
| 30 31 |  | 
| 31 32 | 
             
                expect(scheduler).not_to receive(:schedule)
         | 
| 32 33 | 
             
                scheduler.run_scheduling_cycle
         | 
| @@ -36,8 +37,8 @@ RSpec.describe "StepperMotor::CyclicScheduler" do | |
| 36 37 | 
             
                scheduler = StepperMotor::CyclicScheduler.new(cycle_duration: 40.minutes)
         | 
| 37 38 | 
             
                StepperMotor.scheduler = scheduler
         | 
| 38 39 |  | 
| 39 | 
            -
                expect(scheduler).to receive(:schedule).with(instance_of( | 
| 40 | 
            -
                journey =  | 
| 40 | 
            +
                expect(scheduler).to receive(:schedule).with(instance_of(far_future_journey_class)).once.and_call_original
         | 
| 41 | 
            +
                journey = far_future_journey_class.create!
         | 
| 41 42 |  | 
| 42 43 | 
             
                expect(scheduler).to receive(:schedule).with(journey).and_call_original
         | 
| 43 44 | 
             
                scheduler.run_scheduling_cycle
         | 
| @@ -47,8 +48,8 @@ RSpec.describe "StepperMotor::CyclicScheduler" do | |
| 47 48 | 
             
                scheduler = StepperMotor::CyclicScheduler.new(cycle_duration: 10.seconds)
         | 
| 48 49 | 
             
                StepperMotor.scheduler = scheduler
         | 
| 49 50 |  | 
| 50 | 
            -
                expect(scheduler).to receive(:schedule).with(instance_of( | 
| 51 | 
            -
                journey =  | 
| 51 | 
            +
                expect(scheduler).to receive(:schedule).with(instance_of(far_future_journey_class)).once.and_call_original
         | 
| 52 | 
            +
                journey = far_future_journey_class.create!
         | 
| 52 53 | 
             
                journey.update!(next_step_to_be_performed_at: 10.minutes.ago)
         | 
| 53 54 |  | 
| 54 55 | 
             
                expect(scheduler).to receive(:schedule).with(journey).and_call_original
         | 
| @@ -4,44 +4,21 @@ require_relative "../spec_helper" | |
| 4 4 | 
             
            RSpec.describe "StepperMotor::Journey" do
         | 
| 5 5 | 
             
              include ActiveJob::TestHelper
         | 
| 6 6 |  | 
| 7 | 
            -
              before :all do
         | 
| 8 | 
            -
                establish_test_connection
         | 
| 9 | 
            -
                run_generator
         | 
| 10 | 
            -
                run_migrations
         | 
| 11 | 
            -
                ActiveJob::Base.queue_adapter = :test
         | 
| 12 | 
            -
                ActiveJob::Base.logger = Logger.new(nil)
         | 
| 13 | 
            -
              end
         | 
| 14 | 
            -
             | 
| 15 | 
            -
              after :all do
         | 
| 16 | 
            -
                FileUtils.rm_rf(fake_app_root)
         | 
| 17 | 
            -
              end
         | 
| 18 | 
            -
             | 
| 19 | 
            -
              before :each do
         | 
| 20 | 
            -
                Thread.current[:stepper_motor_side_effects] = {}
         | 
| 21 | 
            -
              end
         | 
| 22 | 
            -
             | 
| 23 | 
            -
              after :each do
         | 
| 24 | 
            -
                # Remove all jobs that remain in the queue
         | 
| 25 | 
            -
                ActiveJob::Base.queue_adapter.enqueued_jobs.clear
         | 
| 26 | 
            -
              end
         | 
| 27 | 
            -
             | 
| 28 7 | 
             
              it "allows an empty journey to be defined and performed to completion" do
         | 
| 29 | 
            -
                 | 
| 30 | 
            -
                 | 
| 31 | 
            -
             | 
| 32 | 
            -
                journey = PointlessJourney.create!
         | 
| 8 | 
            +
                pointless_class = create_journey_subclass
         | 
| 9 | 
            +
                journey = pointless_class.create!
         | 
| 33 10 | 
             
                journey.perform_next_step!
         | 
| 34 11 | 
             
                expect(journey).to be_finished
         | 
| 35 12 | 
             
              end
         | 
| 36 13 |  | 
| 37 14 | 
             
              it "allows a journey consisting of one step to be defined and performed to completion" do
         | 
| 38 | 
            -
                 | 
| 15 | 
            +
                single_step_class = create_journey_subclass do
         | 
| 39 16 | 
             
                  step :do_thing do
         | 
| 40 17 | 
             
                    SideEffects.touch!("do_thing")
         | 
| 41 18 | 
             
                  end
         | 
| 42 19 | 
             
                end
         | 
| 43 20 |  | 
| 44 | 
            -
                journey =  | 
| 21 | 
            +
                journey = single_step_class.create!
         | 
| 45 22 | 
             
                expect(journey.next_step_to_be_performed_at).not_to be_nil
         | 
| 46 23 | 
             
                journey.perform_next_step!
         | 
| 47 24 | 
             
                expect(journey).to be_finished
         | 
| @@ -51,16 +28,15 @@ RSpec.describe "StepperMotor::Journey" do | |
| 51 28 | 
             
              it "allows a journey consisting of multiple named steps to be defined and performed to completion" do
         | 
| 52 29 | 
             
                step_names = [:step1, :step2, :step3]
         | 
| 53 30 |  | 
| 54 | 
            -
                 | 
| 55 | 
            -
             | 
| 56 | 
            -
                  step_names.each do |step_name|
         | 
| 31 | 
            +
                multi_step_journey_class = create_journey_subclass do
         | 
| 32 | 
            +
                  [:step1, :step2, :step3].each do |step_name|
         | 
| 57 33 | 
             
                    step step_name do
         | 
| 58 34 | 
             
                      SideEffects.touch!("from_#{step_name}")
         | 
| 59 35 | 
             
                    end
         | 
| 60 36 | 
             
                  end
         | 
| 61 37 | 
             
                end
         | 
| 62 38 |  | 
| 63 | 
            -
                journey =  | 
| 39 | 
            +
                journey = multi_step_journey_class.create!
         | 
| 64 40 | 
             
                expect(journey.next_step_name).to eq("step1")
         | 
| 65 41 |  | 
| 66 42 | 
             
                journey.perform_next_step!
         | 
| @@ -82,7 +58,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 82 58 | 
             
              end
         | 
| 83 59 |  | 
| 84 60 | 
             
              it "allows a journey consisting of multiple anonymous steps to be defined and performed to completion" do
         | 
| 85 | 
            -
                 | 
| 61 | 
            +
                anonymous_steps_class = create_journey_subclass do
         | 
| 86 62 | 
             
                  3.times do |n|
         | 
| 87 63 | 
             
                    step do
         | 
| 88 64 | 
             
                      SideEffects.touch!("sidefx_#{n}")
         | 
| @@ -90,7 +66,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 90 66 | 
             
                  end
         | 
| 91 67 | 
             
                end
         | 
| 92 68 |  | 
| 93 | 
            -
                journey =  | 
| 69 | 
            +
                journey = anonymous_steps_class.create!
         | 
| 94 70 | 
             
                expect(journey.next_step_name).to eq("step_1")
         | 
| 95 71 |  | 
| 96 72 | 
             
                journey.perform_next_step!
         | 
| @@ -112,27 +88,22 @@ RSpec.describe "StepperMotor::Journey" do | |
| 112 88 | 
             
              end
         | 
| 113 89 |  | 
| 114 90 | 
             
              it "allows an arbitrary ActiveRecord to be attached as the hero" do
         | 
| 115 | 
            -
                 | 
| 116 | 
            -
             | 
| 117 | 
            -
                    # nothing, but we need to have a step so that the journey doesn't get destroyed immediately after creation
         | 
| 118 | 
            -
                  end
         | 
| 119 | 
            -
                end
         | 
| 120 | 
            -
             | 
| 121 | 
            -
                class CarryingJourney < StepperMotor::Journey
         | 
| 91 | 
            +
                carried_journey_class = create_journey_subclass
         | 
| 92 | 
            +
                carrier_journey_class = create_journey_subclass do
         | 
| 122 93 | 
             
                  step :only do
         | 
| 123 | 
            -
                    raise "Incorrect" unless hero.instance_of?( | 
| 94 | 
            +
                    raise "Incorrect" unless hero.instance_of?(carried_journey_class)
         | 
| 124 95 | 
             
                  end
         | 
| 125 96 | 
             
                end
         | 
| 126 97 |  | 
| 127 | 
            -
                hero =  | 
| 128 | 
            -
                journey =  | 
| 98 | 
            +
                hero = carried_journey_class.create!
         | 
| 99 | 
            +
                journey = carrier_journey_class.create!(hero: hero)
         | 
| 129 100 | 
             
                expect {
         | 
| 130 101 | 
             
                  journey.perform_next_step!
         | 
| 131 102 | 
             
                }.not_to raise_error
         | 
| 132 103 | 
             
              end
         | 
| 133 104 |  | 
| 134 105 | 
             
              it "allows a journey where steps are delayed in time using wait:" do
         | 
| 135 | 
            -
                 | 
| 106 | 
            +
                timely_journey_class = carrier_journey_class = create_journey_subclass do
         | 
| 136 107 | 
             
                  step wait: 10.hours do
         | 
| 137 108 | 
             
                    SideEffects.touch! "after_10_hours.txt"
         | 
| 138 109 | 
             
                  end
         | 
| @@ -147,7 +118,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 147 118 | 
             
                end
         | 
| 148 119 |  | 
| 149 120 | 
             
                freeze_time
         | 
| 150 | 
            -
                 | 
| 121 | 
            +
                timely_journey_class.create!
         | 
| 151 122 |  | 
| 152 123 | 
             
                expect {
         | 
| 153 124 | 
             
                  perform_enqueued_jobs
         | 
| @@ -174,7 +145,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 174 145 | 
             
              end
         | 
| 175 146 |  | 
| 176 147 | 
             
              it "allows a journey where steps are delayed in time using after:" do
         | 
| 177 | 
            -
                 | 
| 148 | 
            +
                journey_class = create_journey_subclass do
         | 
| 178 149 | 
             
                  step after: 10.hours do
         | 
| 179 150 | 
             
                    SideEffects.touch! "step1"
         | 
| 180 151 | 
             
                  end
         | 
| @@ -188,36 +159,41 @@ RSpec.describe "StepperMotor::Journey" do | |
| 188 159 | 
             
                  end
         | 
| 189 160 | 
             
                end
         | 
| 190 161 |  | 
| 191 | 
            -
                 | 
| 162 | 
            +
                timely_journey = journey_class.create!
         | 
| 192 163 | 
             
                freeze_time
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                # Note that the "perform_enqueued_jobs" helper method performs the job even if
         | 
| 166 | 
            +
                # its "scheduled_at" lies in the future. Presumably this is done so that testing is
         | 
| 167 | 
            +
                # easier to do, but we check the time the journey was set to perform the next step at
         | 
| 168 | 
            +
                # - and therefore a job which runs too early will produce another job that replaces it.
         | 
| 193 169 | 
             
                expect { perform_enqueued_jobs }.to not_have_produced_any_side_effects
         | 
| 194 170 |  | 
| 195 | 
            -
                 | 
| 196 | 
            -
                perform_enqueued_jobs
         | 
| 171 | 
            +
                travel_to(timely_journey.next_step_to_be_performed_at + 1.second)
         | 
| 197 172 | 
             
                expect { perform_enqueued_jobs }.to have_produced_side_effects_named("step1")
         | 
| 198 173 |  | 
| 199 | 
            -
                travel | 
| 174 | 
            +
                travel(4.minutes)
         | 
| 200 175 | 
             
                expect { perform_enqueued_jobs }.to not_have_produced_any_side_effects
         | 
| 201 176 |  | 
| 202 | 
            -
                travel 1. | 
| 177 | 
            +
                travel(1.minutes + 1.second)
         | 
| 203 178 | 
             
                expect { perform_enqueued_jobs }.to have_produced_side_effects_named("step2")
         | 
| 204 179 | 
             
                expect { perform_enqueued_jobs }.to have_produced_side_effects_named("step3")
         | 
| 180 | 
            +
                expect(enqueued_jobs).to be_empty # Journey ended
         | 
| 205 181 | 
             
              end
         | 
| 206 182 |  | 
| 207 183 | 
             
              it "tracks steps entered and completed using counters" do
         | 
| 208 | 
            -
                 | 
| 184 | 
            +
                failing = create_journey_subclass do
         | 
| 209 185 | 
             
                  step do
         | 
| 210 186 | 
             
                    raise "oops"
         | 
| 211 187 | 
             
                  end
         | 
| 212 188 | 
             
                end
         | 
| 213 189 |  | 
| 214 | 
            -
                 | 
| 190 | 
            +
                not_failing = create_journey_subclass do
         | 
| 215 191 | 
             
                  step do
         | 
| 216 192 | 
             
                    true # no-op
         | 
| 217 193 | 
             
                  end
         | 
| 218 194 | 
             
                end
         | 
| 219 195 |  | 
| 220 | 
            -
                failing_journey =  | 
| 196 | 
            +
                failing_journey = failing.create!
         | 
| 221 197 | 
             
                expect { failing_journey.perform_next_step! }.to raise_error(/oops/)
         | 
| 222 198 | 
             
                expect(failing_journey.steps_entered).to eq(1)
         | 
| 223 199 | 
             
                expect(failing_journey.steps_completed).to eq(0)
         | 
| @@ -227,7 +203,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 227 203 | 
             
                expect(failing_journey.steps_entered).to eq(2)
         | 
| 228 204 | 
             
                expect(failing_journey.steps_completed).to eq(0)
         | 
| 229 205 |  | 
| 230 | 
            -
                non_failing_journey =  | 
| 206 | 
            +
                non_failing_journey = not_failing.create!
         | 
| 231 207 | 
             
                non_failing_journey.perform_next_step!
         | 
| 232 208 | 
             
                expect(non_failing_journey.steps_entered).to eq(1)
         | 
| 233 209 | 
             
                expect(non_failing_journey.steps_completed).to eq(1)
         | 
| @@ -235,7 +211,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 235 211 |  | 
| 236 212 | 
             
              it "does not allow invalid values for after: and wait:" do
         | 
| 237 213 | 
             
                expect {
         | 
| 238 | 
            -
                   | 
| 214 | 
            +
                  create_journey_subclass do
         | 
| 239 215 | 
             
                    step after: 10.hours do
         | 
| 240 216 | 
             
                      # pass
         | 
| 241 217 | 
             
                    end
         | 
| @@ -247,7 +223,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 247 223 | 
             
                }.to raise_error(ArgumentError)
         | 
| 248 224 |  | 
| 249 225 | 
             
                expect {
         | 
| 250 | 
            -
                   | 
| 226 | 
            +
                  create_journey_subclass do
         | 
| 251 227 | 
             
                    step wait: -5.hours do
         | 
| 252 228 | 
             
                      # pass
         | 
| 253 229 | 
             
                    end
         | 
| @@ -255,7 +231,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 255 231 | 
             
                }.to raise_error(ArgumentError)
         | 
| 256 232 |  | 
| 257 233 | 
             
                expect {
         | 
| 258 | 
            -
                   | 
| 234 | 
            +
                  create_journey_subclass do
         | 
| 259 235 | 
             
                    step after: 5.hours, wait: 2.seconds do
         | 
| 260 236 | 
             
                      # pass
         | 
| 261 237 | 
             
                    end
         | 
| @@ -264,14 +240,14 @@ RSpec.describe "StepperMotor::Journey" do | |
| 264 240 | 
             
              end
         | 
| 265 241 |  | 
| 266 242 | 
             
              it "allows a step to reattempt itself" do
         | 
| 267 | 
            -
                 | 
| 243 | 
            +
                deferring = create_journey_subclass do
         | 
| 268 244 | 
             
                  step do
         | 
| 269 245 | 
             
                    reattempt! wait: 5.minutes
         | 
| 270 246 | 
             
                    raise "Should never be reached"
         | 
| 271 247 | 
             
                  end
         | 
| 272 248 | 
             
                end
         | 
| 273 249 |  | 
| 274 | 
            -
                journey =  | 
| 250 | 
            +
                journey = deferring.create!
         | 
| 275 251 | 
             
                perform_enqueued_jobs
         | 
| 276 252 |  | 
| 277 253 | 
             
                journey.reload
         | 
| @@ -289,7 +265,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 289 265 | 
             
              end
         | 
| 290 266 |  | 
| 291 267 | 
             
              it "allows a journey consisting of multiple steps where the first step bails out to be defined and performed to the point of cancellation" do
         | 
| 292 | 
            -
                 | 
| 268 | 
            +
                interrupting = create_journey_subclass do
         | 
| 293 269 | 
             
                  step :step1 do
         | 
| 294 270 | 
             
                    SideEffects.touch!("step1_before_cancel")
         | 
| 295 271 | 
             
                    cancel!
         | 
| @@ -301,7 +277,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 301 277 | 
             
                  end
         | 
| 302 278 | 
             
                end
         | 
| 303 279 |  | 
| 304 | 
            -
                journey =  | 
| 280 | 
            +
                journey = interrupting.create!
         | 
| 305 281 | 
             
                expect(journey.next_step_name).to eq("step1")
         | 
| 306 282 |  | 
| 307 283 | 
             
                perform_enqueued_jobs
         | 
| @@ -311,32 +287,31 @@ RSpec.describe "StepperMotor::Journey" do | |
| 311 287 | 
             
              end
         | 
| 312 288 |  | 
| 313 289 | 
             
              it "forbids multiple similar journeys for the same hero at the same time unless allow_multiple is set" do
         | 
| 314 | 
            -
                 | 
| 315 | 
            -
                 | 
| 316 | 
            -
                hero = SomeActor.create!
         | 
| 290 | 
            +
                actor_class = create_journey_subclass
         | 
| 291 | 
            +
                hero = actor_class.create!
         | 
| 317 292 |  | 
| 318 | 
            -
                 | 
| 293 | 
            +
                exclusive_journey_class = create_journey_subclass do
         | 
| 319 294 | 
             
                  step do
         | 
| 320 295 | 
             
                    raise "The step should never be entered as we are not testing the step itself here"
         | 
| 321 296 | 
             
                  end
         | 
| 322 297 | 
             
                end
         | 
| 323 298 |  | 
| 324 299 | 
             
                expect {
         | 
| 325 | 
            -
                  2.times {  | 
| 300 | 
            +
                  2.times { exclusive_journey_class.create! }
         | 
| 326 301 | 
             
                }.not_to raise_error
         | 
| 327 302 |  | 
| 328 303 | 
             
                expect {
         | 
| 329 | 
            -
                  2.times {  | 
| 304 | 
            +
                  2.times { exclusive_journey_class.create!(hero: hero) }
         | 
| 330 305 | 
             
                }.to raise_error(ActiveRecord::RecordNotUnique)
         | 
| 331 306 |  | 
| 332 307 | 
             
                expect {
         | 
| 333 | 
            -
                  2.times {  | 
| 308 | 
            +
                  2.times { exclusive_journey_class.create!(hero: hero, allow_multiple: true) }
         | 
| 334 309 | 
             
                }.not_to raise_error
         | 
| 335 310 | 
             
              end
         | 
| 336 311 |  | 
| 337 312 | 
             
              it "forbids multiple steps with the same name within a journey" do
         | 
| 338 313 | 
             
                expect {
         | 
| 339 | 
            -
                   | 
| 314 | 
            +
                  create_journey_subclass do
         | 
| 340 315 | 
             
                    step :foo do
         | 
| 341 316 | 
             
                      true
         | 
| 342 317 | 
             
                    end
         | 
| @@ -349,7 +324,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 349 324 | 
             
              end
         | 
| 350 325 |  | 
| 351 326 | 
             
              it "finishes the journey after perform_next_step" do
         | 
| 352 | 
            -
                 | 
| 327 | 
            +
                rapid = create_journey_subclass do
         | 
| 353 328 | 
             
                  step :one do
         | 
| 354 329 | 
             
                    true # no-op
         | 
| 355 330 | 
             
                  end
         | 
| @@ -358,7 +333,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 358 333 | 
             
                  end
         | 
| 359 334 | 
             
                end
         | 
| 360 335 |  | 
| 361 | 
            -
                journey =  | 
| 336 | 
            +
                journey = rapid.create!
         | 
| 362 337 | 
             
                expect(journey).to be_ready
         | 
| 363 338 | 
             
                journey.perform_next_step!
         | 
| 364 339 | 
             
                expect(journey).to be_ready
         | 
| @@ -367,7 +342,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 367 342 | 
             
              end
         | 
| 368 343 |  | 
| 369 344 | 
             
              it "does not enter next step on a finished journey" do
         | 
| 370 | 
            -
                 | 
| 345 | 
            +
                near_instant = create_journey_subclass do
         | 
| 371 346 | 
             
                  step :one do
         | 
| 372 347 | 
             
                    finished!
         | 
| 373 348 | 
             
                  end
         | 
| @@ -377,7 +352,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 377 352 | 
             
                  end
         | 
| 378 353 | 
             
                end
         | 
| 379 354 |  | 
| 380 | 
            -
                journey =  | 
| 355 | 
            +
                journey = near_instant.create!
         | 
| 381 356 | 
             
                expect(journey).to be_ready
         | 
| 382 357 | 
             
                journey.perform_next_step!
         | 
| 383 358 | 
             
                expect(journey).to be_finished
         | 
| @@ -386,20 +361,20 @@ RSpec.describe "StepperMotor::Journey" do | |
| 386 361 | 
             
              end
         | 
| 387 362 |  | 
| 388 363 | 
             
              it "raises an exception if a step changes the journey but does not save it" do
         | 
| 389 | 
            -
                 | 
| 364 | 
            +
                mutating = create_journey_subclass do
         | 
| 390 365 | 
             
                  step :one do
         | 
| 391 366 | 
             
                    self.state = "canceled"
         | 
| 392 367 | 
             
                  end
         | 
| 393 368 | 
             
                end
         | 
| 394 369 |  | 
| 395 | 
            -
                journey =  | 
| 370 | 
            +
                journey = mutating.create!
         | 
| 396 371 | 
             
                expect {
         | 
| 397 372 | 
             
                  journey.perform_next_step!
         | 
| 398 373 | 
             
                }.to raise_error(StepperMotor::JourneyNotPersisted)
         | 
| 399 374 | 
             
              end
         | 
| 400 375 |  | 
| 401 376 | 
             
              it "resets the instance variables after performing a step" do
         | 
| 402 | 
            -
                 | 
| 377 | 
            +
                self_resetting = create_journey_subclass do
         | 
| 403 378 | 
             
                  step :one do
         | 
| 404 379 | 
             
                    raise unless @current_step_definition
         | 
| 405 380 | 
             
                  end
         | 
| @@ -409,7 +384,7 @@ RSpec.describe "StepperMotor::Journey" do | |
| 409 384 | 
             
                  end
         | 
| 410 385 | 
             
                end
         | 
| 411 386 |  | 
| 412 | 
            -
                journey =  | 
| 387 | 
            +
                journey = self_resetting.create!
         | 
| 413 388 | 
             
                expect { journey.perform_next_step! }.not_to raise_error
         | 
| 414 389 | 
             
                expect(journey.instance_variable_get(:@current_step_definition)).to be_nil
         | 
| 415 390 |  | 
| @@ -10,22 +10,24 @@ RSpec.describe "StepperMotor::TestHelper" do | |
| 10 10 | 
             
                run_migrations
         | 
| 11 11 | 
             
              end
         | 
| 12 12 |  | 
| 13 | 
            -
               | 
| 14 | 
            -
                 | 
| 15 | 
            -
                   | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
                   | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
                   | 
| 13 | 
            +
              def speedy_journey_class
         | 
| 14 | 
            +
                create_journey_subclass do
         | 
| 15 | 
            +
                  step :step_1, wait: 40.minutes do
         | 
| 16 | 
            +
                    SideEffects.touch!("step_1")
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  step :step_2, wait: 2.days do
         | 
| 20 | 
            +
                    SideEffects.touch!("step_2")
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  step do
         | 
| 24 | 
            +
                    SideEffects.touch!("step_3")
         | 
| 25 | 
            +
                  end
         | 
| 24 26 | 
             
                end
         | 
| 25 27 | 
             
              end
         | 
| 26 28 |  | 
| 27 29 | 
             
              it "speedruns the journey despite waits being configured" do
         | 
| 28 | 
            -
                journey =  | 
| 30 | 
            +
                journey = speedy_journey_class.create!
         | 
| 29 31 | 
             
                expect(journey).to be_ready
         | 
| 30 32 |  | 
| 31 33 | 
             
                expect {
         | 
| @@ -34,7 +36,7 @@ RSpec.describe "StepperMotor::TestHelper" do | |
| 34 36 | 
             
              end
         | 
| 35 37 |  | 
| 36 38 | 
             
              it "is able to perform a single step forcibly" do
         | 
| 37 | 
            -
                journey =  | 
| 39 | 
            +
                journey = speedy_journey_class.create!
         | 
| 38 40 | 
             
                expect(journey).to be_ready
         | 
| 39 41 |  | 
| 40 42 | 
             
                expect {
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: stepper_motor
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.1. | 
| 4 | 
            +
              version: 0.1.3
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Julik Tarkhanov
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2025- | 
| 11 | 
            +
            date: 2025-02-28 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: activerecord
         | 
| @@ -16,14 +16,14 @@ dependencies: | |
| 16 16 | 
             
                requirements:
         | 
| 17 17 | 
             
                - - ">="
         | 
| 18 18 | 
             
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            -
                    version: ' | 
| 19 | 
            +
                    version: '7'
         | 
| 20 20 | 
             
              type: :runtime
         | 
| 21 21 | 
             
              prerelease: false
         | 
| 22 22 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 23 | 
             
                requirements:
         | 
| 24 24 | 
             
                - - ">="
         | 
| 25 25 | 
             
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            -
                    version: ' | 
| 26 | 
            +
                    version: '7'
         | 
| 27 27 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 28 28 | 
             
              name: activejob
         | 
| 29 29 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -108,6 +108,20 @@ dependencies: | |
| 108 108 | 
             
                - - "~>"
         | 
| 109 109 | 
             
                  - !ruby/object:Gem::Version
         | 
| 110 110 | 
             
                    version: '3.0'
         | 
| 111 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 112 | 
            +
              name: rspec-rails
         | 
| 113 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 114 | 
            +
                requirements:
         | 
| 115 | 
            +
                - - ">="
         | 
| 116 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 117 | 
            +
                    version: '0'
         | 
| 118 | 
            +
              type: :development
         | 
| 119 | 
            +
              prerelease: false
         | 
| 120 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 121 | 
            +
                requirements:
         | 
| 122 | 
            +
                - - ">="
         | 
| 123 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 124 | 
            +
                    version: '0'
         | 
| 111 125 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 112 126 | 
             
              name: standard
         | 
| 113 127 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -136,6 +150,20 @@ dependencies: | |
| 136 150 | 
             
                - - ">="
         | 
| 137 151 | 
             
                  - !ruby/object:Gem::Version
         | 
| 138 152 | 
             
                    version: '0'
         | 
| 153 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 154 | 
            +
              name: redcarpet
         | 
| 155 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 156 | 
            +
                requirements:
         | 
| 157 | 
            +
                - - ">="
         | 
| 158 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 159 | 
            +
                    version: '0'
         | 
| 160 | 
            +
              type: :development
         | 
| 161 | 
            +
              prerelease: false
         | 
| 162 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 163 | 
            +
                requirements:
         | 
| 164 | 
            +
                - - ">="
         | 
| 165 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 166 | 
            +
                    version: '0'
         | 
| 139 167 | 
             
            description: Step workflows for Rails/ActiveRecord
         | 
| 140 168 | 
             
            email:
         | 
| 141 169 | 
             
            - me@julik.nl
         | 
| @@ -173,14 +201,14 @@ files: | |
| 173 201 | 
             
            - spec/stepper_motor/journey_spec.rb
         | 
| 174 202 | 
             
            - spec/stepper_motor/test_helper_spec.rb
         | 
| 175 203 | 
             
            - spec/stepper_motor_spec.rb
         | 
| 176 | 
            -
            homepage: https:// | 
| 204 | 
            +
            homepage: https://steppermotor.dev
         | 
| 177 205 | 
             
            licenses:
         | 
| 178 206 | 
             
            - LGPL
         | 
| 179 207 | 
             
            metadata:
         | 
| 180 208 | 
             
              allowed_push_host: https://rubygems.org
         | 
| 181 | 
            -
              homepage_uri: https:// | 
| 182 | 
            -
              source_code_uri: https://github.com/ | 
| 183 | 
            -
              changelog_uri: https://github.com/ | 
| 209 | 
            +
              homepage_uri: https://steppermotor.dev
         | 
| 210 | 
            +
              source_code_uri: https://github.com/stepper-motor/stepper_motor
         | 
| 211 | 
            +
              changelog_uri: https://github.com/stepper-motor/stepper_motor/blob/main/CHANGELOG.md
         | 
| 184 212 | 
             
            post_install_message:
         | 
| 185 213 | 
             
            rdoc_options: []
         | 
| 186 214 | 
             
            require_paths:
         |