pax_global_header00006660000000000000000000000064150733340130014511gustar00rootroot0000000000000052 comment=32405b0d1fe423300c01b40305b85ed1229f1df7 state_machines-0.100.4/000077500000000000000000000000001507333401300146425ustar00rootroot00000000000000state_machines-0.100.4/.github/000077500000000000000000000000001507333401300162025ustar00rootroot00000000000000state_machines-0.100.4/.github/workflows/000077500000000000000000000000001507333401300202375ustar00rootroot00000000000000state_machines-0.100.4/.github/workflows/engines.yml000066400000000000000000000014371507333401300224170ustar00rootroot00000000000000name: Alternative Ruby Engines on: push: branches: [master] pull_request: branches: [master] jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - ruby-version: "jruby-10.0.0.1" name: "JRuby 10.0.0.1 (Java 21)" - ruby-version: "truffleruby" name: "TruffleRuby" name: ${{ matrix.name }} steps: - uses: actions/checkout@v4 - name: Set up Java uses: actions/setup-java@v4 with: distribution: "temurin" java-version: 21 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true - name: Run tests run: bundle exec rake state_machines-0.100.4/.github/workflows/release.yml000066400000000000000000000017351507333401300224100ustar00rootroot00000000000000name: release-please on: push: branches: - master workflow_dispatch: permissions: contents: write pull-requests: write issues: write jobs: release-please: runs-on: ubuntu-latest steps: - uses: googleapis/release-please-action@v4 id: release - name: Checkout if: ${{ steps.release.outputs.release_created }} uses: actions/checkout@v4 - name: Update COSS version if: ${{ steps.release.outputs.release_created }} run: | VERSION=$(grep "VERSION = " lib/state_machines/version.rb | sed "s/.*'\(.*\)'.*/\1/") sed -i "s/^version = .*/version = \"$VERSION\"/" coss.toml git config --local user.email "action@github.com" git config --local user.name "GitHub Action" git add coss.toml if ! git diff --cached --quiet; then git commit -m "chore: update COSS version to $VERSION" git push fi state_machines-0.100.4/.github/workflows/ruby.yml000066400000000000000000000010021507333401300217340ustar00rootroot00000000000000name: CRuby (MRI) on: push: branches: [ master ] pull_request: branches: [ master ] jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: ruby-version: ['3.2', '3.3', '3.4'] name: Ruby ${{ matrix.ruby-version }} steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true - name: Run tests run: bundle exec rake state_machines-0.100.4/.gitignore000066400000000000000000000002641507333401300166340ustar00rootroot00000000000000*.gem *.rbc .bundle .config .ruby-version .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports tmp *.bundle *.so *.o *.a mkmf.log .idea/ state_machines-0.100.4/.release-please-manifest.json000066400000000000000000000000271507333401300223050ustar00rootroot00000000000000{ ".": "0.100.4" } state_machines-0.100.4/.rubocop.yml000066400000000000000000000006521507333401300171170ustar00rootroot00000000000000plugins: - rubocop-minitest - rubocop-rake Layout/LineLength: Enabled: false Metrics/ClassLength: Enabled: false Metrics/AbcSize: Enabled: false Metrics/CyclomaticComplexity: Enabled: false Metrics/PerceivedComplexity: Enabled: false Style/OptionalBooleanParameter: Enabled: false Style/DocumentDynamicEvalDefinition: Enabled: false Metrics/MethodLength: Enabled: false AllCops: NewCops: enable state_machines-0.100.4/CHANGELOG.md000066400000000000000000000232361507333401300164610ustar00rootroot00000000000000## 0.6.0 * Drop support to EOL rubies. * Support kwargs for ruby 3.0+. ## [0.100.4](https://github.com/state-machines/state_machines/compare/state_machines/v0.100.3...state_machines/v0.100.4) (2025-10-10) ### Bug Fixes * export and restore Thread.current storage from Fiber to after callbacks ([#152](https://github.com/state-machines/state_machines/issues/152)) ([38ba8ca](https://github.com/state-machines/state_machines/commit/38ba8ca56497d0af15798ac972cff6e64856630d)) ## [0.100.3](https://github.com/state-machines/state_machines/compare/state_machines/v0.100.2...state_machines/v0.100.3) (2025-09-27) ### Bug Fixes * preserve Thread.current storage object identity across Fiber boundaries ([#152](https://github.com/state-machines/state_machines/issues/152)) ([#156](https://github.com/state-machines/state_machines/issues/156)) ([ff79cd4](https://github.com/state-machines/state_machines/commit/ff79cd4742b9cb9b48ac9091eeb89a5aabf75a6f)), closes [#157](https://github.com/state-machines/state_machines/issues/157) ## [0.100.2](https://github.com/state-machines/state_machines/compare/state_machines/v0.100.1...state_machines/v0.100.2) (2025-09-11) ### Features * Fix Thread.current/Fiber deadlock in pausable transitions ([#152](https://github.com/state-machines/state_machines/issues/152)) ([#153](https://github.com/state-machines/state_machines/issues/153)) ([7472b51](https://github.com/state-machines/state_machines/commit/7472b51c2d8d0dd7fe2e0f8044cc55d9119d536d)) ## [0.100.1](https://github.com/state-machines/state_machines/compare/state_machines/v0.100.0...state_machines/v0.100.1) (2025-07-25) ### Features * allow integration to append their own options ([#150](https://github.com/state-machines/state_machines/issues/150)) ([0540d3b](https://github.com/state-machines/state_machines/commit/0540d3be07ab28c6d73353b2f2c99936b8abb8b3)) ## [0.100.0](https://github.com/state-machines/state_machines/compare/state_machines/v0.50.0...state_machines/v0.100.0) (2025-07-16) ### Features * replace deprecated callc with Fiber ([#147](https://github.com/state-machines/state_machines/issues/147)) ([2ca2b29](https://github.com/state-machines/state_machines/commit/2ca2b29caa13500d29b65957cecfe0578e5800ce)) ## [0.50.0](https://github.com/state-machines/state_machines/compare/state_machines/v0.40.0...state_machines/v0.50.0) (2025-07-12) ### Features * Add coordinated state management guards ([#145](https://github.com/state-machines/state_machines/issues/145)) ([97eb6ef](https://github.com/state-machines/state_machines/commit/97eb6ef28958e7a6a61fed205e5e02752a95b6a4)) ## [0.40.0](https://github.com/state-machines/state_machines/compare/state_machines/v0.31.0...state_machines/v0.40.0) (2025-07-12) ### Features * add async support with declarative async: true parameter ([#144](https://github.com/state-machines/state_machines/issues/144)) ([5fcbbd7](https://github.com/state-machines/state_machines/commit/5fcbbd72cd7c43c6e946afd242e85b0d9c781251)) ### Bug Fixes * prevent event_transition overwriting with multiple state machines ([fab957e](https://github.com/state-machines/state_machines/commit/fab957e6c4deb5486f5f817f98e410d17d2a45bf)) * prevent event_transition overwriting with multiple state machines ([a8c6017](https://github.com/state-machines/state_machines/commit/a8c60175bd0f7b8c0ea65c5f50229be94bc314c5)) ## [0.31.0](https://github.com/state-machines/state_machines/compare/state_machines/v0.30.0...state_machines/v0.31.0) (2025-06-29) ### Features * modernize codebase with Ruby 3.2+ features ([#134](https://github.com/state-machines/state_machines/issues/134)) ([b3ab92d](https://github.com/state-machines/state_machines/commit/b3ab92de9c90826a521097a863a137fd2cb429c2)) * respect ignore_method_conflicts in State#add_predicate ([#139](https://github.com/state-machines/state_machines/issues/139)) ([d897c50](https://github.com/state-machines/state_machines/commit/d897c5042aa4b6160da80b73fc352da0f2aacd8e)), closes [#135](https://github.com/state-machines/state_machines/issues/135) ### Bug Fixes * Add run_action as a hash option ([#137](https://github.com/state-machines/state_machines/issues/137)) ([d213cd0](https://github.com/state-machines/state_machines/commit/d213cd0fa1e5ba51dce81816672ed0532ee364b0)) * Passing event arguments to guards ([#132](https://github.com/state-machines/state_machines/issues/132)) ([4e21b79](https://github.com/state-machines/state_machines/commit/4e21b79a16d2ea3ef6fcb3e882fb2b6288f0c132)) ### Miscellaneous Chores * release 0.31.0 ([c75d9b8](https://github.com/state-machines/state_machines/commit/c75d9b84cf0b2cc6a2a7ec2f9262fd5bb2db5adf)) ## [0.30.0](https://github.com/state-machines/state_machines/compare/state_machines/v0.20.0...state_machines/v0.30.0) (2025-06-19) ### Features * add basic safety check for eval_helpers ([#126](https://github.com/state-machines/state_machines/issues/126)) ([604e3e6](https://github.com/state-machines/state_machines/commit/604e3e6f3958f2b4be7a9fcbac9502b4583946de)) * add more test_helper after receiving feedback ([#128](https://github.com/state-machines/state_machines/issues/128)) ([4f3ab0a](https://github.com/state-machines/state_machines/commit/4f3ab0a4733d2aabfe78b193cde426b354e96d33)) * add support to kwargs ([#130](https://github.com/state-machines/state_machines/issues/130)) ([9be0c8f](https://github.com/state-machines/state_machines/commit/9be0c8f6cd20990745878bfd0dd4ce6d6c8ff8a1)) ### Bug Fixes * extract internal into modules ([#131](https://github.com/state-machines/state_machines/issues/131)) ([9f4850d](https://github.com/state-machines/state_machines/commit/9f4850d032d374239cf261cc4abcfed09e49ea3d)) * restore jruby support and tests ([#129](https://github.com/state-machines/state_machines/issues/129)) ([2bcb42e](https://github.com/state-machines/state_machines/commit/2bcb42e80afff2eefb29c475cd667184061109ab)) ## [0.20.0](https://github.com/state-machines/state_machines/compare/state_machines/v0.10.1...state_machines/v0.20.0) (2025-06-17) ### Features * remove Hash hack that haunted me for years ([#122](https://github.com/state-machines/state_machines/issues/122)) ([8e5de38](https://github.com/state-machines/state_machines/commit/8e5de3867aed2599d4ada6f32ced2bf95c328f9f)) ## [0.10.1](https://github.com/state-machines/state_machines/compare/state_machines/v0.10.0...state_machines/v0.10.1) (2025-06-15) ### Features * expose test helper ([74d2f5b](https://github.com/state-machines/state_machines/commit/74d2f5bb9b4718c1acfc9d11fc4bdf9a2d713622)) * expose test helper ([170f277](https://github.com/state-machines/state_machines/commit/170f27708ab324c0622db462e76db79a181dafd4)) ## [0.10.0](https://github.com/state-machines/state_machines/compare/state_machines-v0.6.0...state_machines/v0.10.0) (2025-05-28) ### Features * Add `all.except` as an alias for `all -` ([da99aee](https://github.com/state-machines/state_machines/commit/da99aeefa4ec99dc72da188d09a14227e49e8412)) * Allow customization of default error messages ([106033f](https://github.com/state-machines/state_machines/commit/106033fea5120a98790d73a6d155c60bcd39ffb6)) * drop support ruby prior 2.7. ([#87](https://github.com/state-machines/state_machines/issues/87)) ([f9cb1e0](https://github.com/state-machines/state_machines/commit/f9cb1e0aa80a7465e1677a80265fa5ae270cb1f9)) * Drop support to ruby prior 3.0 ([#95](https://github.com/state-machines/state_machines/issues/95)) ([0ce8030](https://github.com/state-machines/state_machines/commit/0ce80309941fccd208dfbe9a88b7590d9cae8717)) * improve STDIO renderer ([4ee3edc](https://github.com/state-machines/state_machines/commit/4ee3edc58e67d313f07fc9e125373db0e12a84b2)) * introduce STDIO renderer ([#109](https://github.com/state-machines/state_machines/issues/109)) ([1bee973](https://github.com/state-machines/state_machines/commit/1bee973af26cbe969fd3e9ae094c6829e995b251)) ### Bug Fixes * enhance evaluate_method to support keyword arguments and improve block handling for Procs and Methods ([8b6ebb1](https://github.com/state-machines/state_machines/commit/8b6ebb1ece7cb4a2b7e51f6c752af9ae437b30c0)) * extract Machine class_methods to it own file to reduce file loc ([5d56ad0](https://github.com/state-machines/state_machines/commit/5d56ad036cc4a9d99650764891dca72bdc697b39)) * Implement conflict check in State#add_predicate ([316cb1a](https://github.com/state-machines/state_machines/commit/316cb1a663169127dac8e24508fed785505f483a)) * improve method argument handling to support jruby and truffleruby and add temporary evaluation for strings ([64f9cca](https://github.com/state-machines/state_machines/commit/64f9cca3d1b744e49e9caadfc21d5ff2aa930c5c)) * update documentation and improve STDIO renderer ([15bcd40](https://github.com/state-machines/state_machines/commit/15bcd403e5f0a24fde8d9b8be6b642ab0fcf851f)) * use symbol syntax for instance variable checks ([75a832c](https://github.com/state-machines/state_machines/commit/75a832c39cf2d8a6c29be5b13d7e454cd179c834)) * use symbol syntax for instance variable checks ([0f01465](https://github.com/state-machines/state_machines/commit/0f014651b4709e707d658517e3f85e366f45bac5)) ## 0.5.0 * Fix states being evaluated with wrong `owner_class` context * Fixed state machine false duplication * Fixed inconsistent use of :use_transactions * Namespaced integrations are not registered by default anymore * Pass `static: false` in case you don't want initial states to be forced. e.g. ```ruby # will set the initial machine state @machines.initialize_states(@object) # optionally you can pass the attributes to have that as the initial state @machines.initialize_states(@object, {}, { state: 'finished' }) # or pass set `static` to false if you want to keep the `object.state` current value @machines.initialize_states(@object, { static: false }) ``` state_machines-0.100.4/Gemfile000066400000000000000000000007751507333401300161460ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' gemspec platform :mri do gem 'debug' end gem 'minitest-reporters' gem 'rubocop', require: false gem 'rubocop-minitest', require: false gem 'rubocop-rake', require: false # Async support dependencies (MRI Ruby only) # These gems are required for StateMachines::AsyncMode functionality # and are loaded conditionally based on Ruby engine compatibility platform :ruby do gem 'async', require: false gem 'concurrent-ruby', require: false end state_machines-0.100.4/LICENSE.txt000066400000000000000000000021351507333401300164660ustar00rootroot00000000000000Copyright (c) 2006-2012 Aaron Pfeifer Copyright (c) 2014-2023 Abdelkader Boudih MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. state_machines-0.100.4/README.md000066400000000000000000000765611507333401300161400ustar00rootroot00000000000000![Build Status](https://github.com/state-machines/state_machines/actions/workflows/ruby.yml/badge.svg) # State Machines State Machines adds support for creating state machines for attributes on any Ruby class. *Please note that multiple integrations are available for [Active Model](https://github.com/state-machines/state_machines-activemodel), [Active Record](https://github.com/state-machines/state_machines-activerecord), [Mongoid](https://github.com/state-machines/state_machines-mongoid) and more in the [State Machines organisation](https://github.com/state-machines).* If you want to save state in your database, **you need one of these additional integrations**. ## Installation Add this line to your application's Gemfile: ```ruby gem 'state_machines' ``` And then execute: ```sh bundle ``` Or install it yourself as: ```sh gem install state_machines ``` ## Usage ### Example Below is an example of many of the features offered by this plugin, including: * Initial states * Namespaced states * Transition callbacks * Conditional transitions * Coordinated state management guards * Asynchronous state machines (async: true) * State-driven instance behavior * Customized state values * Parallel events * Path analysis Class definition: ```ruby class Vehicle attr_accessor :seatbelt_on, :time_used, :auto_shop_busy, :parking_meter_number state_machine :state, initial: :parked do before_transition parked: any - :parked, do: :put_on_seatbelt after_transition on: :crash, do: :tow after_transition on: :repair, do: :fix after_transition any => :parked do |vehicle, transition| vehicle.seatbelt_on = false end after_failure on: :ignite, do: :log_start_failure around_transition do |vehicle, transition, block| start = Time.now block.call vehicle.time_used += Time.now - start end event :park do transition [:idling, :first_gear] => :parked end before_transition on: :park do |vehicle, transition| # If using Rails: # options = transition.args.extract_options! options = transition.args.last.is_a?(Hash) ? transition.args.pop : {} meter_number = options[:meter_number] unless meter_number.nil? vehicle.parking_meter_number = meter_number end end event :ignite do transition stalled: same, parked: :idling end event :idle do transition first_gear: :idling end event :shift_up do transition idling: :first_gear, first_gear: :second_gear, second_gear: :third_gear end event :shift_down do transition third_gear: :second_gear, second_gear: :first_gear end event :crash do transition all - [:parked, :stalled] => :stalled, if: ->(vehicle) {!vehicle.passed_inspection?} end event :repair do # The first transition that matches the state and passes its conditions # will be used transition stalled: :parked, unless: :auto_shop_busy transition stalled: same end state :parked do def speed 0 end end state :idling, :first_gear do def speed 10 end end state all - [:parked, :stalled, :idling] do def moving? true end end state :parked, :stalled, :idling do def moving? false end end end state_machine :alarm_state, initial: :active, namespace: :'alarm' do event :enable do transition all => :active end event :disable do transition all => :off end state :active, :value => 1 state :off, :value => 0 end def initialize @seatbelt_on = false @time_used = 0 @auto_shop_busy = true @parking_meter_number = nil super() # NOTE: This *must* be called, otherwise states won't get initialized end def put_on_seatbelt @seatbelt_on = true end def passed_inspection? false end def tow # tow the vehicle end def fix # get the vehicle fixed by a mechanic end def log_start_failure # log a failed attempt to start the vehicle end end ``` **Note** the comment made on the `initialize` method in the class. In order for state machine attributes to be properly initialized, `super()` must be called. See `StateMachines:MacroMethods` for more information about this. Using the above class as an example, you can interact with the state machine like so: ```ruby vehicle = Vehicle.new # => # vehicle.state # => "parked" vehicle.state_name # => :parked vehicle.human_state_name # => "parked" vehicle.parked? # => true vehicle.can_ignite? # => true vehicle.ignite_transition # => # vehicle.state_events # => [:ignite] vehicle.state_transitions # => [#] vehicle.speed # => 0 vehicle.moving? # => false vehicle.ignite # => true vehicle.parked? # => false vehicle.idling? # => true vehicle.speed # => 10 vehicle # => # vehicle.shift_up # => true vehicle.speed # => 10 vehicle.moving? # => true vehicle # => # # A generic event helper is available to fire without going through the event's instance method vehicle.fire_state_event(:shift_up) # => true # Call state-driven behavior that's undefined for the state raises a NoMethodError vehicle.speed # => NoMethodError: super: no superclass method `speed' for # vehicle # => # # The bang (!) operator can raise exceptions if the event fails vehicle.park! # => StateMachines:InvalidTransition: Cannot transition state via :park from :second_gear # Generic state predicates can raise exceptions if the value does not exist vehicle.state?(:parked) # => false vehicle.state?(:invalid) # => IndexError: :invalid is an invalid name # Transition callbacks can receive arguments vehicle.park(meter_number: '12345') # => true vehicle.parked? # => true vehicle.parking_meter_number # => "12345" # Namespaced machines have uniquely-generated methods vehicle.alarm_state # => 1 vehicle.alarm_state_name # => :active vehicle.can_disable_alarm? # => true vehicle.disable_alarm # => true vehicle.alarm_state # => 0 vehicle.alarm_state_name # => :off vehicle.can_enable_alarm? # => true vehicle.alarm_off? # => true vehicle.alarm_active? # => false # Events can be fired in parallel vehicle.fire_events(:shift_down, :enable_alarm) # => true vehicle.state_name # => :first_gear vehicle.alarm_state_name # => :active vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachines:InvalidParallelTransition: Cannot run events in parallel: ignite, enable_alarm # Coordinated State Management State machines can coordinate with each other using state guards, allowing transitions to depend on the state of other state machines within the same object. This enables complex system modeling where components are interdependent. ## State Guard Options ### Single State Guards * `:if_state` - Transition only if another state machine is in a specific state. * `:unless_state` - Transition only if another state machine is NOT in a specific state. ```ruby class TorpedoSystem state_machine :bay_doors, initial: :closed do event :open do transition closed: :open end event :close do transition open: :closed end end state_machine :torpedo_status, initial: :loaded do event :fire_torpedo do # Can only fire torpedo if bay doors are open transition loaded: :fired, if_state: { bay_doors: :open } end event :reload do # Can only reload if bay doors are closed (for safety) transition fired: :loaded, unless_state: { bay_doors: :open } end end end system = TorpedoSystem.new system.fire_torpedo # => false (bay doors are closed) system.open_bay_doors! system.fire_torpedo # => true (bay doors are now open) ``` ### Multiple State Guards * `:if_all_states` - Transition only if ALL specified state machines are in their respective states. * `:unless_all_states` - Transition only if NOT ALL specified state machines are in their respective states. * `:if_any_state` - Transition only if ANY of the specified state machines are in their respective states. * `:unless_any_state` - Transition only if NONE of the specified state machines are in their respective states. ```ruby class StarshipBridge state_machine :shields, initial: :down state_machine :weapons, initial: :offline state_machine :warp_core, initial: :stable state_machine :alert_status, initial: :green do event :red_alert do # Red alert if ANY critical system needs attention transition green: :red, if_any_state: { warp_core: :critical, shields: :down } end event :battle_stations do # Battle stations only if ALL combat systems are ready transition green: :battle, if_all_states: { shields: :up, weapons: :armed } end end end ``` ## Error Handling State guards provide comprehensive error checking: ```ruby # Referencing a non-existent state machine event :invalid, if_state: { nonexistent_machine: :some_state } # => ArgumentError: State machine 'nonexistent_machine' is not defined for StarshipBridge # Referencing a non-existent state event :another_invalid, if_state: { shields: :nonexistent_state } # => ArgumentError: State 'nonexistent_state' is not defined in state machine 'shields' ``` # Asynchronous State Machines State machines can operate asynchronously for high-performance applications. This is ideal for I/O-bound tasks, such as in web servers or other concurrent environments, where you don't want a long-running state transition (like one involving a network call) to block the entire thread. This feature is powered by the [async](https://github.com/socketry/async) gem and uses `concurrent-ruby` for enterprise-grade thread safety. ## Platform Compatibility **Supported Platforms:** * MRI Ruby (CRuby) 3.2+ * Other Ruby engines with full Fiber scheduler support **Unsupported Platforms:** * JRuby - Falls back to synchronous mode with warnings * TruffleRuby - Falls back to synchronous mode with warnings ## Basic Async Usage Enable async mode by adding `async: true` to your state machine declaration: ```ruby class AutonomousDrone < StarfleetShip # Async-enabled state machine for autonomous operation state_machine :status, async: true, initial: :docked do event :launch do transition docked: :flying end event :land do transition flying: :docked end end # Mixed configuration: some machines async, others sync state_machine :teleporter_status, async: true, initial: :offline do event :power_up do transition offline: :charging end event :teleport do transition ready: :teleporting end end # Weapons remain synchronous for safety state_machine :weapons, initial: :disarmed do event :arm do transition disarmed: :armed end end end ``` ## Async Event Methods Async-enabled machines automatically generate async versions of event methods: ```ruby drone = AutonomousDrone.new # Within an Async context Async do # Async event firing - returns Async::Task task = drone.launch_async result = task.wait # => true # Bang methods for strict error handling drone.power_up_async! # => Async::Task (raises on failure) # Generic async event firing drone.fire_event_async(:teleport) # => Async::Task end # Outside Async context - raises error drone.launch_async # => RuntimeError: launch_async must be called within an Async context ``` ## Thread Safety Async state machines use enterprise-grade thread safety with `concurrent-ruby`: ```ruby # Concurrent operations are automatically thread-safe threads = [] 10.times do threads << Thread.new do Async do drone.launch_async.wait drone.land_async.wait end end end threads.each(&:join) ``` ## Performance Considerations * **Thread Safety**: Uses `Concurrent::ReentrantReadWriteLock` for optimal read/write performance. * **Memory**: Each async-enabled object gets its own mutex (lazy-loaded). * **Marshalling**: Objects with async state machines can be serialized (mutex excluded/recreated). * **Mixed Mode**: You can mix async and sync state machines in the same class. ## Dependencies Async functionality requires: ```ruby # Gemfile (automatically scoped to MRI Ruby) platform :ruby do gem 'async', '>= 2.25.0' gem 'concurrent-ruby', '>= 1.3.5' end ``` *Note: These gems are only installed on supported platforms. JRuby/TruffleRuby won't attempt installation.* # Human-friendly names can be accessed for states/events Vehicle.human_state_name(:first_gear) # => "first gear" Vehicle.human_alarm_state_name(:active) # => "active" Vehicle.human_state_event_name(:shift_down) # => "shift down" Vehicle.human_alarm_state_event_name(:enable) # => "enable" # States / events can also be references by the string version of their name Vehicle.human_state_name('first_gear') # => "first gear" Vehicle.human_state_event_name('shift_down') # => "shift down" # Available transition paths can be analyzed for an object vehicle.state_paths # => [[# [:parked, :idling, :first_gear, :stalled, :second_gear, :third_gear] vehicle.state_paths.events # => [:park, :ignite, :shift_up, :idle, :crash, :repair, :shift_down] # Possible states can be analyzed for a class Vehicle.state_machine.states.to_a # [#, #, ...] Vehicle.state_machines[:state].states.to_a # [#, #, ...] # Find all paths that start and end on certain states vehicle.state_paths(:from => :parked, :to => :first_gear) # => [[ # #, # # # ]] # Skipping state_machine and writing to attributes directly vehicle.state = "parked" vehicle.state # => "parked" vehicle.state_name # => :parked # *Note* that the following is not supported (see StateMachines:MacroMethods#state_machine): # vehicle.state = :parked ``` ## Testing State Machines provides an optional `TestHelper` module with assertion methods to make testing state machines easier and more expressive. **Note: TestHelper is not required by default** - you must explicitly require it in your test files. ### Setup First, require the test helper module, then include it in your test class: ```ruby # For Minitest require 'state_machines/test_helper' class VehicleTest < Minitest::Test include StateMachines::TestHelper def test_initial_state vehicle = Vehicle.new assert_sm_state vehicle, :parked end end # For RSpec require 'state_machines/test_helper' RSpec.describe Vehicle do include StateMachines::TestHelper it "starts in parked state" do vehicle = Vehicle.new assert_sm_state vehicle, :parked end end ``` ### Available Assertions The TestHelper provides both basic assertions and comprehensive state machine-specific assertions with `sm_` prefixes: #### Basic Assertions ```ruby vehicle = Vehicle.new # New standardized API (all methods prefixed with assert_sm_) assert_sm_state(vehicle, :parked) # Uses default :state machine assert_sm_state(vehicle, :parked, machine_name: :status) # Specify machine explicitly assert_sm_can_transition(vehicle, :ignite) # Test transition capability assert_sm_cannot_transition(vehicle, :shift_up) # Test transition restriction assert_sm_transition(vehicle, :ignite, :idling) # Test actual transition # Multi-FSM examples assert_sm_state(vehicle, :inactive, machine_name: :insurance_state) # Test insurance state assert_sm_can_transition(vehicle, :buy_insurance, machine_name: :insurance_state) ``` #### Extended State Machine Assertions ```ruby machine = Vehicle.state_machine(:state) vehicle = Vehicle.new # State configuration assert_sm_states_list machine, [:parked, :idling, :stalled] assert_sm_initial_state machine, :parked # Event behavior assert_sm_event_triggers vehicle, :ignite refute_sm_event_triggers vehicle, :shift_up assert_sm_event_raises_error vehicle, :invalid_event, StateMachines::InvalidTransition # Persistence (with ActiveRecord integration) assert_sm_state_persisted record, expected: :active ``` #### Indirect Event Testing Test that methods trigger state machine events indirectly: ```ruby # Minitest style vehicle = Vehicle.new vehicle.ignite # Put in idling state # Test that a custom method triggers a specific event assert_sm_triggers_event(vehicle, :crash) do vehicle.redline # Custom method that calls crash! internally end # Test multiple events assert_sm_triggers_event(vehicle, [:crash, :emergency]) do vehicle.emergency_stop end # Test on specific state machine (multi-FSM support) assert_sm_triggers_event(vehicle, :disable, machine_name: :alarm) do vehicle.turn_off_alarm end ``` ```ruby # RSpec style (coming soon with proper matcher support) RSpec.describe Vehicle do include StateMachines::TestHelper it "triggers crash when redlining" do vehicle = Vehicle.new vehicle.ignite expect_to_trigger_event(vehicle, :crash) do vehicle.redline end end end ``` #### Callback Definition Testing (TDD Support) Verify that callbacks are properly defined in your state machine: ```ruby # Test after_transition callbacks assert_after_transition(Vehicle, on: :crash, do: :tow) assert_after_transition(Vehicle, from: :stalled, to: :parked, do: :log_repair) # Test before_transition callbacks assert_before_transition(Vehicle, from: :parked, do: :put_on_seatbelt) assert_before_transition(Vehicle, on: :ignite, if: :seatbelt_on?) # Works with machine instances too machine = Vehicle.state_machine(:state) assert_after_transition(machine, on: :crash, do: :tow) ``` #### Multiple State Machine Support The TestHelper fully supports objects with multiple state machines: ```ruby # Example: StarfleetShip with 3 state machines ship = StarfleetShip.new # Test states on different machines assert_sm_state(ship, :docked, machine_name: :status) # Main ship status assert_sm_state(ship, :down, machine_name: :shields) # Shield system assert_sm_state(ship, :standby, machine_name: :weapons) # Weapons system # Test transitions on specific machines assert_sm_transition(ship, :undock, :impulse, machine_name: :status) assert_sm_transition(ship, :raise_shields, :up, machine_name: :shields) assert_sm_transition(ship, :arm_weapons, :armed, machine_name: :weapons) # Test event triggering across multiple machines assert_sm_triggers_event(ship, :red_alert, machine_name: :status) do ship.engage_combat_mode # Custom method affecting multiple systems end assert_sm_triggers_event(ship, :raise_shields, machine_name: :shields) do ship.engage_combat_mode end # Test callback definitions on specific machines shields_machine = StarfleetShip.state_machine(:shields) assert_before_transition(shields_machine, from: :down, to: :up, do: :power_up_shields) # Test persistence across multiple machines assert_sm_state_persisted(ship, "impulse", :status) assert_sm_state_persisted(ship, "up", :shields) assert_sm_state_persisted(ship, "armed", :weapons) ``` The test helper works with both Minitest and RSpec, automatically detecting your testing framework. **Note:** All methods use consistent keyword arguments with `machine_name:` as the last parameter, making the API intuitive and Grep-friendly. ## Additional Topics ### Explicit vs. Implicit Event Transitions Every event defined for a state machine generates an instance method on the class that allows the event to be explicitly triggered. Most of the examples in the state_machine documentation use this technique. However, with some types of integrations, like ActiveRecord, you can also *implicitly* fire events by setting a special attribute on the instance. Suppose you're using the ActiveRecord integration and the following model is defined: ```ruby class Vehicle < ActiveRecord::Base state_machine initial: :parked do event :ignite do transition parked: :idling end end end ``` To trigger the `ignite` event, you would typically call the `Vehicle#ignite` method like so: ```ruby vehicle = Vehicle.create # => # vehicle.ignite # => true vehicle.state # => "idling" ``` This is referred to as an *explicit* event transition. The same behavior can also be achieved *implicitly* by setting the state event attribute and invoking the action associated with the state machine. For example: ```ruby vehicle = Vehicle.create # => # vehicle.state_event = 'ignite' # => 'ignite' vehicle.save # => true vehicle.state # => 'idling' vehicle.state_event # => nil ``` As you can see, the `ignite` event was automatically triggered when the `save` action was called. This is particularly useful if you want to allow users to drive the state transitions from a web API. See each integration's API documentation for more information on the implicit approach. ### Symbols vs. Strings In all of the examples used throughout the documentation, you'll notice that states and events are almost always referenced as symbols. This isn't a requirement, but rather a suggested best practice. You can very well define your state machine with Strings like so: ```ruby class Vehicle state_machine initial: 'parked' do event 'ignite' do transition 'parked' => 'idling' end # ... end end ``` You could even use numbers as your state / event names. The **important** thing to keep in mind is that the type being used for referencing states / events in your machine definition must be **consistent**. If you're using Symbols, then all states / events must use Symbols. Otherwise you'll encounter the following error: ```ruby class Vehicle state_machine do event :ignite do transition parked: 'idling' end end end # => ArgumentError: "idling" state defined as String, :parked defined as Symbol; all states must be consistent ``` There **is** an exception to this rule. The consistency is only required within the definition itself. However, when the machine's helper methods are called with input from external sources, such as a web form, state_machine will map that input to a String / Symbol. For example: ```ruby class Vehicle state_machine initial: :parked do event :ignite do transition parked: :idling end end end v = Vehicle.new # => # v.state?('parked') # => true v.state?(:parked) # => true ``` **Note** that none of this actually has to do with the type of the value that gets stored. By default, all state values are assumed to be string -- regardless of whether the state names are symbols or strings. If you want to store states as symbols instead you'll have to be explicit about it: ```ruby class Vehicle state_machine initial: :parked do event :ignite do transition parked: :idling end states.each do |state| self.state(state.name, :value => state.name.to_sym) end end end v = Vehicle.new # => # v.state?('parked') # => true v.state?(:parked) # => true ``` ### Syntax flexibility Although state_machine introduces a simplified syntax, it still remains backwards compatible with previous versions and other state-related libraries by providing some flexibility around how transitions are defined. See below for an overview of these syntaxes. #### Verbose syntax In general, it's recommended that state machines use the implicit syntax for transitions. However, you can be a little more explicit and verbose about transitions by using the `:from`, `:except_from`, `:to`, and `:except_to` options. For example, transitions and callbacks can be defined like so: ```ruby class Vehicle state_machine initial: :parked do before_transition from: :parked, except_to: :parked, do: :put_on_seatbelt after_transition to: :parked do |vehicle, transition| vehicle.seatbelt = 'off' end event :ignite do transition from: :parked, to: :idling end end end ``` #### Transition context Some flexibility is provided around the context in which transitions can be defined. In almost all examples throughout the documentation, transitions are defined within the context of an event. If you prefer to have state machines defined in the context of a **state** either out of preference or in order to easily migrate from a different library, you can do so as shown below: ```ruby class Vehicle state_machine initial: :parked do # ... state :parked do transition to: :idling, :on => [:ignite, :shift_up], if: :seatbelt_on? def speed 0 end end state :first_gear do transition to: :second_gear, on: :shift_up def speed 10 end end state :idling, :first_gear do transition to: :parked, on: :park end end end ``` In the above example, there's no need to specify the `from` state for each transition since it's inferred from the context. You can also define transitions completely outside the context of a particular state / event. This may be useful in cases where you're building a state machine from a data store instead of part of the class definition. See the example below: ```ruby class Vehicle state_machine initial: :parked do # ... transition parked: :idling, :on => [:ignite, :shift_up] transition first_gear: :second_gear, second_gear: :third_gear, on: :shift_up transition [:idling, :first_gear] => :parked, on: :park transition all - [:parked, :stalled]: :stalled, unless: :auto_shop_busy? end end ``` Notice that in these alternative syntaxes: * You can continue to configure `:if` and `:unless` conditions * You can continue to define `from` states (when in the machine context) using the `all`, `any`, and `same` helper methods ### Static / Dynamic definitions In most cases, the definition of a state machine is **static**. That is to say, the states, events and possible transitions are known ahead of time even though they may depend on data that's only known at runtime. For example, certain transitions may only be available depending on an attribute on that object it's being run on. All of the documentation in this library define static machines like so: ```ruby class Vehicle state_machine :state, initial: :parked do event :park do transition [:idling, :first_gear] => :parked end # ... end end ``` #### Draw state machines State machines includes a default STDIORenderer for debugging state machines without external dependencies. This renderer can be used to visualize the state machine in the console. To use the renderer, simply call the `draw` method on the state machine: ```ruby Vehicle.state_machine.draw # Outputs the state machine diagram to the console ``` You can customize the output by passing in options to the `draw` method, such as the output stream: ```ruby Vehicle.state_machine.draw(io: $stderr) # Outputs the state machine diagram to stderr ``` #### Dynamic definitions There may be cases where the definition of a state machine is **dynamic**. This means that you don't know the possible states or events for a machine until runtime. For example, you may allow users in your application to manage the state machine of a project or task in your system. This means that the list of transitions (and their associated states / events) could be stored externally, such as in a database. In a case like this, you can define dynamically-generated state machines like so: ```ruby class Vehicle attr_accessor :state # Make sure the machine gets initialized so the initial state gets set properly def initialize(*) super machine end # Replace this with an external source (like a db) def transitions [ {parked: :idling, on: :ignite}, {idling: :first_gear, first_gear: :second_gear, on: :shift_up} # ... ] end # Create a state machine for this vehicle instance dynamically based on the # transitions defined from the source above def machine vehicle = self @machine ||= Machine.new(vehicle, initial: :parked, action: :save) do vehicle.transitions.each {|attrs| transition(attrs)} end end def save # Save the state change... true end end # Generic class for building machines class Machine def self.new(object, *args, &block) machine_class = Class.new machine = machine_class.state_machine(*args, &block) attribute = machine.attribute action = machine.action # Delegate attributes machine_class.class_eval do define_method(:definition) { machine } define_method(attribute) { object.send(attribute) } define_method("#{attribute}=") {|value| object.send("#{attribute}=", value) } define_method(action) { object.send(action) } if action end machine_class.new end end vehicle = Vehicle.new # => # vehicle.state # => "parked" vehicle.machine.ignite # => true vehicle.machine.state # => "idling" vehicle.state # => "idling" vehicle.machine.state_transitions # => [#] vehicle.machine.definition.states.keys # => :first_gear, :second_gear, :parked, :idling ``` As you can see, state_machine provides enough flexibility for you to be able to create new machine definitions on the fly based on an external source of transitions. ## Dependencies Ruby versions officially supported and tested: * Ruby (MRI) 3.0.0+ For graphing state machine: * [state_machines-graphviz](https://github.com/state-machines/state_machines-graphviz) For documenting state machines: * [state_machines-yard](https://github.com/state-machines/state_machines-yard) For RSpec testing, use the custom RSpec matchers: * [state_machines-rspec](https://github.com/state-machines/state_machines-rspec) ## Contributing 1. Fork it ( ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request state_machines-0.100.4/Rakefile000066400000000000000000000010101507333401300162770ustar00rootroot00000000000000# frozen_string_literal: true require 'bundler/gem_tasks' require 'rake/testtask' Rake::TestTask.new(:functional) do |t| t.libs << 'test' t.test_files = FileList['test/functional/*_test.rb'] end Rake::TestTask.new(:unit) do |t| t.libs << 'test' t.test_files = FileList['test/unit/**/*_test.rb'] end desc 'Default: run all tests.' task test: %i[unit functional] task default: :test desc 'Update COSS version to match current gem version' task :update_coss_version do sh 'scripts/update_coss_version.sh' end state_machines-0.100.4/bin/000077500000000000000000000000001507333401300154125ustar00rootroot00000000000000state_machines-0.100.4/bin/console000077500000000000000000000004401507333401300170000ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require 'bundler/setup' require 'state_machines' # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. require 'irb' IRB.start(__FILE__) state_machines-0.100.4/bin/setup000077500000000000000000000002031507333401300164730ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here state_machines-0.100.4/coss.toml000066400000000000000000000111031507333401300165020ustar00rootroot00000000000000# COSS Metadata Template v0.0.2 # This is the official COSS (Contriboss Open Source Standard) specification template # Copy this file to your project root as coss.toml and customize for your project ########################################################## # 1. Basic Project Information name = "state_machines" version = "0.100.3" description = "Adds support for creating state machines for attributes on any Ruby class" licenses = ["MIT"] ai_contributions = true # AI was used to enhance testing framework coss_compliant = true homepage = "https://github.com/state-machines/state_machines" keywords = ["ruby", "state-machine", "workflow", "finite-state-machine", "transitions"] ########################################################## # 2. Repository and Issue Tracking repository = "https://github.com/state-machines/state_machines" issue_tracker = "https://github.com/state-machines/state_machines/issues" documentation = "https://github.com/state-machines/state_machines/blob/master/README.md" security_policy = "" ########################################################## # 3. Languages, Frameworks, and Platforms languages = ["ruby"] [frameworks] # Pure Ruby library - no specific framework dependencies supported_platforms = ["linux", "darwin", "windows"] ########################################################## # 4. Dependency Lock Files [dependency_locks] ruby = "Gemfile.lock" [packaging] ruby = "gem build state_machines.gemspec" ########################################################## # 5. Maintainers and Governance maintainers = ["state-machines-maintainers@example.com"] governance = { type = "informal" } ########################################################## # 6. Linting, Formatting, and Static Analysis lint = "bundle exec rubocop" format = "bundle exec rubocop -a" static_analysis = ["bundle exec rubocop"] ########################################################## # 7. CI and Build Commands build = "bundle install" test = "rake test" coverage = "" ########################################################## # 8. Tests and Quality Metrics [test_frameworks] ruby = "minitest" test_report_format = "minitest" coverage_threshold = 0 ########################################################## # 9. Commit Guidelines and Formats commit_message_format = "" ########################################################## # 10. Release and Changelog changelog = "CHANGELOG.md" release_tag_pattern = "v{version}" ########################################################## # 11. Badges and Integrations (Optional) [badges] ci = "https://github.com/state-machines/state_machines/actions/workflows/ruby.yml/badge.svg" coverage = "" license_badge = "" ########################################################## # 12. Optional Miscellaneous Fields chat = "" support = { type = "github", contact = "https://github.com/state-machines/state_machines/issues" } apidocs = "" ########################################################## # 13. Environment and Runtime Info [environments] ruby = "3.0+" ########################################################## # 14. Data Schemas and Contracts [data_contracts] openapi = "" graphql = "" avro = "" ########################################################## # 15. Project Classification project_type = "library" maturity = "stable" audience = ["developers", "ruby-developers"] ########################################################## # 16. Localization / Internationalization [i18n] default_locale = "en" supported_locales = ["en"] translation_files = "" ########################################################## # 17. Contribution Automation [contribution_tooling] dependabot = false precommit_hooks = false ai_review = "disabled" codeowners = "" ########################################################## # 18. Security Scanning and SBOM [security] sbom = "" vulnerability_scanner = "" license_compliance_tool = "" ########################################################## # 19. Documentation Quality Flags [docs] coverage = 0 style = "" ai_summary_enabled = false ########################################################## # 20. Submodules and Component References [submodules] # References to integration gems in the state_machines ecosystem state_machines-activemodel = "https://github.com/state-machines/state_machines-activemodel" state_machines-activerecord = "https://github.com/state-machines/state_machines-activerecord" state_machines-audit_trail = "https://github.com/state-machines/state_machines-audit_trail" state_machines-graphviz = "https://github.com/state-machines/state_machines-graphviz" state_machines-yard = "https://github.com/state-machines/state_machines-yard" state_machines-0.100.4/lib/000077500000000000000000000000001507333401300154105ustar00rootroot00000000000000state_machines-0.100.4/lib/state_machines.rb000066400000000000000000000002501507333401300207210ustar00rootroot00000000000000# frozen_string_literal: true require 'state_machines/version' require 'state_machines/core' require 'state_machines/core_ext' require 'state_machines/stdio_renderer' state_machines-0.100.4/lib/state_machines/000077500000000000000000000000001507333401300203775ustar00rootroot00000000000000state_machines-0.100.4/lib/state_machines/async_mode.rb000066400000000000000000000044131507333401300230470ustar00rootroot00000000000000# frozen_string_literal: true # Ruby Engine Compatibility Check # The async gem requires native extensions and Fiber scheduler support # which are not available on JRuby or TruffleRuby if RUBY_ENGINE == 'jruby' || RUBY_ENGINE == 'truffleruby' raise LoadError, <<~ERROR StateMachines::AsyncMode is not available on #{RUBY_ENGINE}. The async gem requires native extensions (io-event) and Fiber scheduler support which are not implemented in #{RUBY_ENGINE}. AsyncMode is only supported on: • MRI Ruby (CRuby) 3.2+ • Other Ruby engines with full Fiber scheduler support If you need async support on #{RUBY_ENGINE}, consider using: • java.util.concurrent classes (JRuby) • Native threading libraries for your platform • Or stick with synchronous state machines ERROR end # Load required gems with version constraints gem 'async', '>= 2.25.0' gem 'concurrent-ruby', '>= 1.3.5' # Security is not negotiable - enterprise-grade thread safety required require 'async' require 'concurrent-ruby' # Load all async mode components require_relative 'async_mode/thread_safe_state' require_relative 'async_mode/async_events' require_relative 'async_mode/async_event_extensions' require_relative 'async_mode/async_machine' require_relative 'async_mode/async_transition_collection' module StateMachines # AsyncMode provides asynchronous state machine capabilities using the async gem # This module enables concurrent, thread-safe state operations for high-performance applications # # @example Basic usage # class AutonomousDrone < StarfleetShip # state_machine :teleporter_status, async: true do # event :power_up do # transition offline: :charging # end # end # end # # drone = AutonomousDrone.new # Async do # result = drone.fire_event_async(:power_up) # => true # task = drone.power_up_async! # => Async::Task # end # module AsyncMode # All components are loaded from separate files: # - ThreadSafeState: Mutex-based thread safety # - AsyncEvents: Async event firing methods # - AsyncEventExtensions: Event method generation # - AsyncMachine: Machine-level async capabilities # - AsyncTransitionCollection: Concurrent transition execution end end state_machines-0.100.4/lib/state_machines/async_mode/000077500000000000000000000000001507333401300225205ustar00rootroot00000000000000state_machines-0.100.4/lib/state_machines/async_mode/async_event_extensions.rb000066400000000000000000000037251507333401300276510ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines module AsyncMode # Extensions to Event class for async bang methods module AsyncEventExtensions # Generate async bang methods for events when async mode is enabled def define_helper(scope, method, *args, &block) result = super # If this is an async-enabled machine and we're defining an event method if scope == :instance && method !~ /_async[!]?$/ && machine.async_mode_enabled? qualified_name = method.to_s # Create async version that returns a task machine.define_helper(scope, "#{qualified_name}_async") do |machine, object, *method_args, **kwargs| # Find the machine that has this event target_machine = object.class.state_machines.values.find { |m| m.events[name] } unless defined?(::Async::Task) && ::Async::Task.current? raise RuntimeError, "#{qualified_name}_async must be called within an Async context" end Async do target_machine.events[name].fire(object, *method_args, **kwargs) end end # Create async bang version that raises exceptions when awaited machine.define_helper(scope, "#{qualified_name}_async!") do |machine, object, *method_args, **kwargs| # Find the machine that has this event target_machine = object.class.state_machines.values.find { |m| m.events[name] } unless defined?(::Async::Task) && ::Async::Task.current? raise RuntimeError, "#{qualified_name}_async! must be called within an Async context" end Async do # Use fire method which will raise exceptions on invalid transitions target_machine.events[name].fire(object, *method_args, **kwargs) || raise(StateMachines::InvalidTransition.new(object, target_machine, name)) end end end result end end end end state_machines-0.100.4/lib/state_machines/async_mode/async_events.rb000066400000000000000000000252111507333401300255470ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines module AsyncMode # Async-aware event firing capabilities using the async gem module AsyncEvents # Fires an event asynchronously using Async # Returns an Async::Task that can be awaited for the result # # Example: # Async do # task = vehicle.async_fire_event(:ignite) # result = task.wait # => true/false # end def async_fire_event(event_name, *args) # Find the machine that has this event machine = self.class.state_machines.values.find { |m| m.events[event_name] } unless machine raise ArgumentError, "Event #{event_name} not found in any state machine" end # Must be called within an Async context unless defined?(::Async::Task) && ::Async::Task.current? raise RuntimeError, "async_fire_event must be called within an Async context. Use: Async { vehicle.async_fire_event(:event) }" end Async do machine.events[event_name].fire(self, *args) end end # Fires multiple events asynchronously across different state machines # Returns an array of Async::Tasks for concurrent execution # # Example: # Async do # tasks = vehicle.async_fire_events(:ignite, :buy_insurance) # results = tasks.map(&:wait) # => [true, true] # end def async_fire_events(*event_names) event_names.map { |event_name| async_fire_event(event_name) } end # Fires an event asynchronously and waits for completion # This is a convenience method that creates and waits for the task # # Example: # result = vehicle.fire_event_async(:ignite) # => true/false def fire_event_async(event_name, *args) raise NoMethodError, "undefined method `fire_event_async' for #{self}" unless has_async_machines? # Find the machine that has this event machine = self.class.state_machines.values.find { |m| m.events[event_name] } unless machine raise ArgumentError, "Event #{event_name} not found in any state machine" end if defined?(::Async::Task) && ::Async::Task.current? # Already in async context, just fire directly machine.events[event_name].fire(self, *args) else # Create async context and wait for result Async do machine.events[event_name].fire(self, *args) end.wait end end # Fires multiple events asynchronously and waits for all completions # Returns results in the same order as the input events # # Example: # results = vehicle.fire_events_async(:ignite, :buy_insurance) # => [true, true] def fire_events_async(*event_names) raise NoMethodError, "undefined method `fire_events_async' for #{self}" unless has_async_machines? if defined?(::Async::Task) && ::Async::Task.current? # Already in async context, run concurrently tasks = event_names.map { |event_name| async_fire_event(event_name) } tasks.map(&:wait) else # Create async context and run concurrently Async do tasks = event_names.map { |event_name| async_fire_event(event_name) } tasks.map(&:wait) end.wait end end # Fires an event asynchronously using Async and raises exception on failure # Returns an Async::Task that raises StateMachines::InvalidTransition when awaited # # Example: # Async do # begin # task = vehicle.async_fire_event!(:ignite) # result = task.wait # puts "Event fired successfully!" # rescue StateMachines::InvalidTransition => e # puts "Transition failed: #{e.message}" # end # end def async_fire_event!(event_name, *args) # Find the machine that has this event machine = self.class.state_machines.values.find { |m| m.events[event_name] } unless machine raise ArgumentError, "Event #{event_name} not found in any state machine" end # Must be called within an Async context unless defined?(::Async::Task) && ::Async::Task.current? raise RuntimeError, "async_fire_event! must be called within an Async context. Use: Async { vehicle.async_fire_event!(:event) }" end Async do # Use the bang version which raises exceptions on failure machine.events[event_name].fire(self, *args) || raise(StateMachines::InvalidTransition.new(self, machine, event_name)) end end # Fires an event asynchronously and waits for result, raising exceptions on failure # This is a convenience method that creates and waits for the task # # Example: # begin # result = vehicle.fire_event_async!(:ignite) # puts "Event fired successfully!" # rescue StateMachines::InvalidTransition => e # puts "Transition failed: #{e.message}" # end def fire_event_async!(event_name, *args) raise NoMethodError, "undefined method `fire_event_async!' for #{self}" unless has_async_machines? # Find the machine that has this event machine = self.class.state_machines.values.find { |m| m.events[event_name] } unless machine raise ArgumentError, "Event #{event_name} not found in any state machine" end if defined?(::Async::Task) && ::Async::Task.current? # Already in async context, just fire directly with bang behavior machine.events[event_name].fire(self, *args) || raise(StateMachines::InvalidTransition.new(self, machine, event_name)) else # Create async context and wait for result (may raise exception) Async do machine.events[event_name].fire(self, *args) || raise(StateMachines::InvalidTransition.new(self, machine, event_name)) end.wait end end # Dynamically handle individual event async methods # This provides launch_async, launch_async!, arm_weapons_async, etc. def method_missing(method_name, *args, **kwargs, &block) method_str = method_name.to_s # Check if this is an async event method if method_str.end_with?('_async!') # Remove the _async! suffix to get the base event method base_method = method_str.chomp('_async!').to_sym # Check if the base method exists and this machine is async-enabled if respond_to?(base_method) && async_method_for_event?(base_method) return handle_individual_event_async_bang(base_method, *args, **kwargs) end elsif method_str.end_with?('_async') # Remove the _async suffix to get the base event method base_method = method_str.chomp('_async').to_sym # Check if the base method exists and this machine is async-enabled if respond_to?(base_method) && async_method_for_event?(base_method) return handle_individual_event_async(base_method, *args, **kwargs) end end # If not an async method, call the original method_missing super end # Check if we should respond to async methods for this event def respond_to_missing?(method_name, include_private = false) # Only provide async methods if this object has async-enabled machines return super unless has_async_machines? method_str = method_name.to_s if method_str.end_with?('_async!') || method_str.end_with?('_async') base_method = method_str.chomp('_async!').chomp('_async').to_sym return respond_to?(base_method) && async_method_for_event?(base_method) end super end # Check if this object has any async-enabled state machines def has_async_machines? self.class.state_machines.any? { |name, machine| machine.async_mode_enabled? } end private # Check if this event method should have async versions def async_method_for_event?(event_method) # Find which machine contains this event self.class.state_machines.each do |name, machine| if machine.async_mode_enabled? # Check if this event method belongs to this machine machine.events.each do |event| qualified_name = event.qualified_name if qualified_name.to_sym == event_method || "#{qualified_name}!".to_sym == event_method return true end end end end false end # Handle individual event async methods (returns task) def handle_individual_event_async(event_method, *args, **kwargs) unless defined?(::Async::Task) && ::Async::Task.current? raise RuntimeError, "#{event_method}_async must be called within an Async context" end Async do send(event_method, *args, **kwargs) end end # Handle individual event async bang methods (returns task, raises on failure) def handle_individual_event_async_bang(event_method, *args, **kwargs) # Extract event name from method and use bang version bang_method = "#{event_method}!".to_sym unless defined?(::Async::Task) && ::Async::Task.current? raise RuntimeError, "#{event_method}_async! must be called within an Async context" end Async do send(bang_method, *args, **kwargs) end end # Extract event name from method name, handling namespaced events def extract_event_name(method_name) method_str = method_name.to_s # Find the machine and event for this method self.class.state_machines.each do |name, machine| machine.events.each do |event| qualified_name = event.qualified_name if qualified_name.to_s == method_str || "#{qualified_name}!".to_s == method_str return event.name end end end # Fallback: assume the method name is the event name method_str.chomp('!').to_sym end public # Fires multiple events concurrently within an async context # This method should be called from within an Async block # # Example: # Async do # results = vehicle.fire_events_concurrent(:ignite, :buy_insurance) # end def fire_events_concurrent(*event_names) unless defined?(::Async::Task) && ::Async::Task.current? raise RuntimeError, "fire_events_concurrent must be called within an Async context" end tasks = async_fire_events(*event_names) tasks.map(&:wait) end end end end state_machines-0.100.4/lib/state_machines/async_mode/async_machine.rb000066400000000000000000000041231507333401300256460ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines module AsyncMode # Enhanced machine class with async capabilities module AsyncMachine # Thread-safe state reading for machines def read_safely(object, attribute, ivar = false) if object.respond_to?(:read_state_safely) object.read_state_safely(self, attribute, ivar) else read(object, attribute, ivar) end end # Thread-safe state writing for machines def write_safely(object, attribute, value, ivar = false) if object.respond_to?(:write_state_safely) object.write_state_safely(self, attribute, value, ivar) else write(object, attribute, value, ivar) end end # Fires an event asynchronously on the given object # Returns an Async::Task for concurrent execution def async_fire_event(object, event_name, *args) unless defined?(::Async::Task) && ::Async::Task.current? raise RuntimeError, "async_fire_event must be called within an Async context" end Async do events[event_name].fire(object, *args) end end # Creates an async-aware transition collection # Supports concurrent transition execution with proper synchronization def create_async_transition_collection(transitions, options = {}) if defined?(AsyncTransitionCollection) AsyncTransitionCollection.new(transitions, options) else # Fallback to regular collection if async collection isn't available TransitionCollection.new(transitions, options) end end # Thread-safe callback execution for async operations def run_callbacks_safely(type, object, context, transition) if object.respond_to?(:state_machine_mutex) object.state_machine_mutex.with_read_lock do callbacks[type].each { |callback| callback.call(object, context, transition) } end else callbacks[type].each { |callback| callback.call(object, context, transition) } end end end end end state_machines-0.100.4/lib/state_machines/async_mode/async_transition_collection.rb000066400000000000000000000110751507333401300306530ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines module AsyncMode # Error class for async-specific transition failures class AsyncTransitionError < StateMachines::Error def initialize(object, machines, failed_events) @object = object @machines = machines @failed_events = failed_events super("Failed to perform async transitions: #{failed_events.join(', ')}") end attr_reader :object, :machines, :failed_events end # Async-aware transition collection that can execute transitions concurrently class AsyncTransitionCollection < TransitionCollection # Performs transitions asynchronously using Async # Provides better concurrency for I/O-bound operations def perform_async(&block) reset unless defined?(::Async::Task) && ::Async::Task.current? return Async do perform_async(&block) end.wait end if valid? # Create async tasks for each transition tasks = map do |transition| Async do if use_event_attributes? && !block_given? transition.transient = true transition.machine.write_safely(object, :event_transition, transition) run_actions transition else within_transaction do catch(:halt) { run_callbacks(&block) } rollback unless success? end transition end end end # Wait for all tasks to complete completed_transitions = [] tasks.each do |task| begin result = task.wait completed_transitions << result if result rescue StandardError => e # Handle individual transition failures rollback raise AsyncTransitionError.new(object, map(&:machine), [e.message]) end end # Check if all transitions succeeded @success = completed_transitions.length == length end success? end # Performs transitions concurrently using threads # Better for CPU-bound operations but requires more careful synchronization def perform_threaded(&block) reset if valid? # Use basic thread approach threads = [] results = [] results_mutex = Concurrent::ReentrantReadWriteLock.new each do |transition| threads << Thread.new do begin result = if use_event_attributes? && !block_given? transition.transient = true transition.machine.write_safely(object, :event_transition, transition) run_actions transition else within_transaction do catch(:halt) { run_callbacks(&block) } rollback unless success? end transition end results_mutex.with_write_lock { results << result } rescue StandardError => e # Handle individual transition failures rollback raise AsyncTransitionError.new(object, [transition.machine], [e.message]) end end end # Wait for all threads to complete threads.each(&:join) @success = results.length == length end success? end private # Override run_actions to be thread-safe when needed def run_actions(&block) catch_exceptions do @success = if block_given? result = yield actions.each { |action| results[action] = result } !!result else actions.compact.each do |action| next if skip_actions # Use thread-safe write for results if object.respond_to?(:state_machine_mutex) object.state_machine_mutex.with_write_lock do results[action] = object.send(action) end else results[action] = object.send(action) end end results.values.all? end end end end end end state_machines-0.100.4/lib/state_machines/async_mode/thread_safe_state.rb000066400000000000000000000033411507333401300265130ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines module AsyncMode # Thread-safe state operations for async-enabled state machines # Uses concurrent-ruby for enterprise-grade thread safety module ThreadSafeState # Gets or creates a reentrant mutex for thread-safe state operations on an object # Each object gets its own mutex to avoid global locking # Uses Concurrent::ReentrantReadWriteLock for better performance def state_machine_mutex @_state_machine_mutex ||= Concurrent::ReentrantReadWriteLock.new end # Thread-safe version of state reading # Ensures atomic read operations across concurrent threads def read_state_safely(machine, attribute, ivar = false) state_machine_mutex.with_read_lock do machine.read(self, attribute, ivar) end end # Thread-safe version of state writing # Ensures atomic write operations across concurrent threads def write_state_safely(machine, attribute, value, ivar = false) state_machine_mutex.with_write_lock do machine.write(self, attribute, value, ivar) end end # Handle marshalling by excluding the mutex (will be recreated when needed) def marshal_dump # Get instance variables excluding the mutex vars = instance_variables.reject { |var| var == :@_state_machine_mutex } vars.map { |var| [var, instance_variable_get(var)] } end # Restore marshalled object, mutex will be lazily recreated when needed def marshal_load(data) data.each do |var, value| instance_variable_set(var, value) end # Don't set @_state_machine_mutex - let it be lazily created end end end end state_machines-0.100.4/lib/state_machines/branch.rb000066400000000000000000000243361507333401300221710ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'options_validator' module StateMachines # Represents a set of requirements that must be met in order for a transition # or callback to occur. Branches verify that the event, from state, and to # state of the transition match, in addition to if/unless conditionals for # an object's state. class Branch include EvalHelpers # The condition that must be met on an object attr_reader :if_condition # The condition that must *not* be met on an object attr_reader :unless_condition # The requirement for verifying the event being matched attr_reader :event_requirement # One or more requirements for verifying the states being matched. All # requirements contain a mapping of {:from => matcher, :to => matcher}. attr_reader :state_requirements # A list of all of the states known to this branch. This will pull states # from the following options (in the same order): # * +from+ / +except_from+ # * +to+ / +except_to+ attr_reader :known_states # Creates a new branch def initialize(options = {}) # :nodoc: # Build conditionals @if_condition = options.delete(:if) @unless_condition = options.delete(:unless) @if_state_condition = options.delete(:if_state) @unless_state_condition = options.delete(:unless_state) @if_all_states_condition = options.delete(:if_all_states) @unless_all_states_condition = options.delete(:unless_all_states) @if_any_state_condition = options.delete(:if_any_state) @unless_any_state_condition = options.delete(:unless_any_state) # Build event requirement @event_requirement = build_matcher(options, :on, :except_on) if (options.keys - %i[from to on except_from except_to except_on]).empty? # Explicit from/to requirements specified @state_requirements = [{ from: build_matcher(options, :from, :except_from), to: build_matcher(options, :to, :except_to) }] else # Separate out the event requirement options.delete(:on) options.delete(:except_on) # Implicit from/to requirements specified @state_requirements = options.collect do |from, to| from = WhitelistMatcher.new(from) unless from.is_a?(Matcher) to = WhitelistMatcher.new(to) unless to.is_a?(Matcher) { from: from, to: to } end end # Track known states. The order that requirements are iterated is based # on the priority in which tracked states should be added. @known_states = [] @state_requirements.each do |state_requirement| %i[from to].each { |option| @known_states |= state_requirement[option].values } end end # Determines whether the given object / query matches the requirements # configured for this branch. In addition to matching the event, from state, # and to state, this will also check whether the configured :if/:unless # conditions pass on the given object. # # == Examples # # branch = StateMachines::Branch.new(:parked => :idling, :on => :ignite) # # # Successful # branch.matches?(object, :on => :ignite) # => true # branch.matches?(object, :from => nil) # => true # branch.matches?(object, :from => :parked) # => true # branch.matches?(object, :to => :idling) # => true # branch.matches?(object, :from => :parked, :to => :idling) # => true # branch.matches?(object, :on => :ignite, :from => :parked, :to => :idling) # => true # # # Unsuccessful # branch.matches?(object, :on => :park) # => false # branch.matches?(object, :from => :idling) # => false # branch.matches?(object, :to => :first_gear) # => false # branch.matches?(object, :from => :parked, :to => :first_gear) # => false # branch.matches?(object, :on => :park, :from => :parked, :to => :idling) # => false def matches?(object, query = {}) !match(object, query).nil? end # Alias for Minitest's assert_match alias =~ matches? # Attempts to match the given object / query against the set of requirements # configured for this branch. In addition to matching the event, from state, # and to state, this will also check whether the configured :if/:unless # conditions pass on the given object. # # If a match is found, then the event/state requirements that the query # passed successfully will be returned. Otherwise, nil is returned if there # was no match. # # Query options: # * :from - One or more states being transitioned from. If none # are specified, then this will always match. # * :to - One or more states being transitioned to. If none are # specified, then this will always match. # * :on - One or more events that fired the transition. If none # are specified, then this will always match. # * :guard - Whether to guard matches with the if/unless # conditionals defined for this branch. Default is true. # # Event arguments are passed to guard conditions if they accept multiple parameters. # # == Examples # # branch = StateMachines::Branch.new(:parked => :idling, :on => :ignite) # # branch.match(object, :on => :ignite) # => {:to => ..., :from => ..., :on => ...} # branch.match(object, :on => :park) # => nil def match(object, query = {}, event_args = []) StateMachines::OptionsValidator.assert_valid_keys!(query, :from, :to, :on, :guard) return unless (match = match_query(query)) && matches_conditions?(object, query, event_args) match end def draw(graph, event, valid_states, io = $stdout) machine.renderer.draw_branch(self, graph, event, valid_states, io) end protected # Builds a matcher strategy to use for the given options. If neither a # whitelist nor a blacklist option is specified, then an AllMatcher is # built. def build_matcher(options, whitelist_option, blacklist_option) StateMachines::OptionsValidator.assert_exclusive_keys!(options, whitelist_option, blacklist_option) if options.include?(whitelist_option) value = options[whitelist_option] value.is_a?(Matcher) ? value : WhitelistMatcher.new(options[whitelist_option]) elsif options.include?(blacklist_option) value = options[blacklist_option] raise ArgumentError, ":#{blacklist_option} option cannot use matchers; use :#{whitelist_option} instead" if value.is_a?(Matcher) BlacklistMatcher.new(value) else AllMatcher.instance end end # Verifies that all configured requirements (event and state) match the # given query. If a match is found, then a hash containing the # event/state requirements that passed will be returned; otherwise, nil. def match_query(query) query ||= {} if match_event(query) && (state_requirement = match_states(query)) state_requirement.merge(on: event_requirement) end end # Verifies that the event requirement matches the given query def match_event(query) matches_requirement?(query, :on, event_requirement) end # Verifies that the state requirements match the given query. If a # matching requirement is found, then it is returned. def match_states(query) state_requirements.detect do |state_requirement| %i[from to].all? { |option| matches_requirement?(query, option, state_requirement[option]) } end end # Verifies that an option in the given query matches the values required # for that option def matches_requirement?(query, option, requirement) !query.include?(option) || requirement.matches?(query[option], query) end # Verifies that the conditionals for this branch evaluate to true for the # given object. Event arguments are passed to guards that accept multiple parameters. def matches_conditions?(object, query, event_args = []) return true if query[:guard] == false # Evaluate original if/unless conditions if_passes = !if_condition || Array(if_condition).all? { |condition| evaluate_method_with_event_args(object, condition, event_args) } unless_passes = !unless_condition || Array(unless_condition).none? { |condition| evaluate_method_with_event_args(object, condition, event_args) } return false unless if_passes && unless_passes # Consolidate all state guards state_guards = { if_state: @if_state_condition, unless_state: @unless_state_condition, if_all_states: @if_all_states_condition, unless_all_states: @unless_all_states_condition, if_any_state: @if_any_state_condition, unless_any_state: @unless_any_state_condition }.compact return true if state_guards.empty? validate_and_check_state_guards(object, state_guards) end private def validate_and_check_state_guards(object, guards) guards.all? do |guard_type, conditions| case guard_type when :if_state, :if_all_states conditions.all? { |machine, state| check_state(object, machine, state) } when :unless_state conditions.none? { |machine, state| check_state(object, machine, state) } when :if_any_state conditions.any? { |machine, state| check_state(object, machine, state) } when :unless_all_states !conditions.all? { |machine, state| check_state(object, machine, state) } when :unless_any_state conditions.none? { |machine, state| check_state(object, machine, state) } end end end def check_state(object, machine_name, state_name) machine = object.class.state_machines[machine_name] raise ArgumentError, "State machine '#{machine_name}' is not defined for #{object.class.name}" unless machine state = machine.states[state_name] raise ArgumentError, "State '#{state_name}' is not defined in state machine '#{machine_name}'" unless state state.matches?(object.send(machine_name)) end end end state_machines-0.100.4/lib/state_machines/callback.rb000066400000000000000000000166061507333401300224710ustar00rootroot00000000000000# frozen_string_literal: true require 'state_machines/branch' require 'state_machines/eval_helpers' module StateMachines # Callbacks represent hooks into objects that allow logic to be triggered # before, after, or around a specific set of transitions. class Callback include EvalHelpers class << self # Determines whether to automatically bind the callback to the object # being transitioned. This only applies to callbacks that are defined as # lambda blocks (or Procs). Some integrations, such as DataMapper, handle # callbacks by executing them bound to the object involved, while other # integrations, such as ActiveRecord, pass the object as an argument to # the callback. This can be configured on an application-wide basis by # setting this configuration to +true+ or +false+. The default value # is +false+. # # *Note* that the DataMapper and Sequel integrations automatically # configure this value on a per-callback basis, so it does not have to # be enabled application-wide. # # == Examples # # When not bound to the object: # # class Vehicle # state_machine do # before_transition do |vehicle| # vehicle.set_alarm # end # end # # def set_alarm # ... # end # end # # When bound to the object: # # StateMachines::Callback.bind_to_object = true # # class Vehicle # state_machine do # before_transition do # self.set_alarm # end # end # # def set_alarm # ... # end # end attr_accessor :bind_to_object # The application-wide terminator to use for callbacks when not # explicitly defined. Terminators determine whether to cancel a # callback chain based on the return value of the callback. # # See StateMachines::Callback#terminator for more information. attr_accessor :terminator end # The type of callback chain this callback is for. This can be one of the # following: # * +before+ # * +after+ # * +around+ # * +failure+ attr_accessor :type # An optional block for determining whether to cancel the callback chain # based on the return value of the callback. By default, the callback # chain never cancels based on the return value (i.e. there is no implicit # terminator). Certain integrations, such as ActiveRecord and Sequel, # change this default value. # # == Examples # # Canceling the callback chain without a terminator: # # class Vehicle # state_machine do # before_transition do |vehicle| # throw :halt # end # end # end # # Canceling the callback chain with a terminator value of +false+: # # class Vehicle # state_machine do # before_transition do |vehicle| # false # end # end # end attr_reader :terminator # The branch that determines whether or not this callback can be invoked # based on the context of the transition. The event, from state, and # to state must all match in order for the branch to pass. # # See StateMachines::Branch for more information. attr_reader :branch # Creates a new callback that can get called based on the configured # options. # # In addition to the possible configuration options for branches, the # following options can be configured: # * :bind_to_object - Whether to bind the callback to the object involved. # If set to false, the object will be passed as a parameter instead. # Default is integration-specific or set to the application default. # * :terminator - A block/proc that determines what callback # results should cause the callback chain to halt (if not using the # default throw :halt technique). # # More information about how those options affect the behavior of the # callback can be found in their attribute definitions. def initialize(type, *args, &block) @type = type raise ArgumentError, 'Type must be :before, :after, :around, or :failure' unless %i[before after around failure].include?(type) options = args.last.is_a?(Hash) ? args.pop : {} @methods = args @methods.concat(Array(options.delete(:do))) @methods << block if block_given? raise ArgumentError, 'Method(s) for callback must be specified' unless @methods.any? options = { bind_to_object: self.class.bind_to_object, terminator: self.class.terminator }.merge(options) # Proxy lambda blocks so that they're bound to the object bind_to_object = options.delete(:bind_to_object) @methods.map! do |method| bind_to_object && method.is_a?(Proc) ? bound_method(method) : method end @terminator = options.delete(:terminator) @branch = Branch.new(options) end # Gets a list of the states known to this callback by looking at the # branch's known states def known_states branch.known_states end # Runs the callback as long as the transition context matches the branch # requirements configured for this callback. If a block is provided, it # will be called when the last method has run. # # If a terminator has been configured and it matches the result from the # evaluated method, then the callback chain should be halted. def call(object, context = {}, *, &) if @branch.matches?(object, context) run_methods(object, context, 0, *, &) true else false end end private # Runs all of the methods configured for this callback. # # When running +around+ callbacks, this will evaluate each method and # yield when the last method has yielded. The callback will only halt if # one of the methods does not yield. # # For all other types of callbacks, this will evaluate each method in # order. The callback will only halt if the resulting value from the # method passes the terminator. def run_methods(object, context = {}, index = 0, *args, &block) case type when :around current_method = @methods[index] if current_method yielded = false evaluate_method(object, current_method, *args) do yielded = true run_methods(object, context, index + 1, *args, &block) end throw :halt unless yielded elsif block_given? yield end else @methods.each do |method| result = evaluate_method(object, method, *args) throw :halt if @terminator&.call(result) end end end # Generates a method that can be bound to the object being transitioned # when the callback is invoked def bound_method(block) type = self.type arity = block.arity arity += 1 if arity >= 0 # Make sure the object gets passed arity += 1 if arity == 1 && type == :around # Make sure the block gets passed method = ->(object, *args) { object.instance_exec(*args, &block) } # Proxy arity to the original block ( class << method self end).class_eval do define_method(:arity) { arity } end method end end end state_machines-0.100.4/lib/state_machines/core.rb000066400000000000000000000023031507333401300216520ustar00rootroot00000000000000# frozen_string_literal: true # Load all of the core implementation required to use state_machine. This # includes: # * StateMachines::MacroMethods which adds the state_machine DSL to your class # * A set of initializers for setting state_machine defaults based on the current # running environment (such as within Rails) require 'state_machines/error' require 'state_machines/extensions' require 'state_machines/integrations' require 'state_machines/integrations/base' require 'state_machines/eval_helpers' require 'singleton' require 'state_machines/matcher' require 'state_machines/matcher_helpers' require 'state_machines/transition' require 'state_machines/transition_collection' require 'state_machines/branch' require 'state_machines/helper_module' require 'state_machines/callback' require 'state_machines/node_collection' require 'state_machines/state_context' require 'state_machines/state' require 'state_machines/state_collection' require 'state_machines/event' require 'state_machines/event_collection' require 'state_machines/path' require 'state_machines/path_collection' require 'state_machines/machine' require 'state_machines/machine_collection' require 'state_machines/macro_methods' state_machines-0.100.4/lib/state_machines/core_ext.rb000066400000000000000000000002231507333401300225310ustar00rootroot00000000000000# frozen_string_literal: true # Loads all of the extensions to be made to Ruby core classes require 'state_machines/core_ext/class/state_machine' state_machines-0.100.4/lib/state_machines/core_ext/000077500000000000000000000000001507333401300222075ustar00rootroot00000000000000state_machines-0.100.4/lib/state_machines/core_ext/class/000077500000000000000000000000001507333401300233145ustar00rootroot00000000000000state_machines-0.100.4/lib/state_machines/core_ext/class/state_machine.rb000066400000000000000000000001351507333401300264440ustar00rootroot00000000000000# frozen_string_literal: true Class.class_eval do include StateMachines::MacroMethods end state_machines-0.100.4/lib/state_machines/error.rb000066400000000000000000000054161507333401300220630ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines # An error occurred during a state machine invocation class Error < StandardError # The object that failed attr_reader :object def initialize(object, message = nil) # :nodoc: @object = object super(message) end end # An invalid integration was specified class IntegrationNotFound < Error def initialize(name) super(nil, "#{name.inspect} is an invalid integration. #{error_message}") end def valid_integrations "Valid integrations are: #{valid_integrations_name}" end def valid_integrations_name Integrations.list.collect(&:integration_name) end def no_integrations 'No integrations registered' end def error_message if Integrations.list.size.zero? no_integrations else valid_integrations end end end # An invalid integration was registered class IntegrationError < StandardError end # An invalid event was specified class InvalidEvent < Error # The event that was attempted to be run attr_reader :event def initialize(object, event_name) # :nodoc: @event = event_name super(object, "#{event.inspect} is an unknown state machine event") end end # An invalid transition was attempted class InvalidTransition < Error # The machine attempting to be transitioned attr_reader :machine # The current state value for the machine attr_reader :from def initialize(object, machine, event) # :nodoc: @machine = machine @from_state = machine.states.match!(object) @from = machine.read(object, :state) @event = machine.events.fetch(event) errors = machine.errors_for(object) message = "Cannot transition #{machine.name} via :#{self.event} from #{from_name.inspect}" message << " (Reason(s): #{errors})" unless errors.empty? super(object, message) end # The event that triggered the failed transition def event @event.name end # The fully-qualified name of the event that triggered the failed transition def qualified_event @event.qualified_name end # The name for the current state def from_name @from_state.name end # The fully-qualified name for the current state def qualified_from_name @from_state.qualified_name end end # A set of transition failed to run in parallel class InvalidParallelTransition < Error # The set of events that failed the transition(s) attr_reader :events def initialize(object, events) # :nodoc: @events = events super(object, "Cannot run events in parallel: #{events * ', '}") end end # A method was called in an invalid state context class InvalidContext < Error end end state_machines-0.100.4/lib/state_machines/eval_helpers.rb000066400000000000000000000216361507333401300234050ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'syntax_validator' module StateMachines # Provides a set of helper methods for evaluating methods within the context # of an object. module EvalHelpers # Evaluates one of several different types of methods within the context # of the given object. Methods can be one of the following types: # * Symbol # * Method / Proc # * String # # == Examples # # Below are examples of the various ways that a method can be evaluated # on an object: # # class Person # def initialize(name) # @name = name # end # # def name # @name # end # end # # class PersonCallback # def self.run(person) # person.name # end # end # # person = Person.new('John Smith') # # evaluate_method(person, :name) # => "John Smith" # evaluate_method(person, PersonCallback.method(:run)) # => "John Smith" # evaluate_method(person, Proc.new {|person| person.name}) # => "John Smith" # evaluate_method(person, lambda {|person| person.name}) # => "John Smith" # evaluate_method(person, '@name') # => "John Smith" # # == Additional arguments # # Additional arguments can be passed to the methods being evaluated. If # the method defines additional arguments other than the object context, # then all arguments are required. # # For guard conditions in state machines, event arguments can be passed # automatically based on the guard's arity: # - Guards with arity 1 receive only the object (backward compatible) # - Guards with arity -1 or > 1 receive object + event arguments # # For example, # # person = Person.new('John Smith') # # evaluate_method(person, lambda {|person| person.name}, 21) # => "John Smith" # evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21) # => "John Smith is 21" # evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21, 'male') # => ArgumentError: wrong number of arguments (3 for 2) # # With event arguments for guards: # # # Single parameter guard (backward compatible) # guard = lambda {|obj| obj.valid? } # evaluate_method_with_event_args(object, guard, [arg1, arg2]) # => calls guard.call(object) # # # Multi-parameter guard (receives event args) # guard = lambda {|obj, *args| obj.valid? && args[0] == :force } # evaluate_method_with_event_args(object, guard, [:force]) # => calls guard.call(object, :force) def evaluate_method(object, method, *args, **, &block) case method in Symbol => sym klass = (class << object; self; end) args = [] if (klass.method_defined?(sym) || klass.private_method_defined?(sym)) && object.method(sym).arity.zero? object.send(sym, *args, **, &block) in Proc => proc args.unshift(object) arity = proc.arity # Handle blocks for Procs case [block_given?, arity] in [true, arity] if arity != 0 case arity in 1 | 2 # Force the block to be either the only argument or the second one # after the object (may mean additional arguments get discarded) args = args[0, arity - 1] + [block] else # insert the block to the end of the args args << block end in [_, 0 | 1] # These method types are only called with 0, 1, or n arguments args = args[0, arity] else # No changes needed for other cases end # Call the Proc with the arguments proc.call(*args, **) in Method => meth args.unshift(object) arity = meth.arity # Methods handle blocks via &block, not as arguments # Only limit arguments if necessary based on arity args = args[0, arity] if [0, 1].include?(arity) # Call the Method with the arguments and pass the block meth.call(*args, **, &block) in String => str # Input validation for string evaluation validate_eval_string(str) # Evaluate the string in the object's context if block_given? # TruffleRuby and some other implementations need special handling for blocks # Create a temporary method to evaluate the string with block support eigen = class << object; self; end eigen.class_eval <<-RUBY, __FILE__, __LINE__ + 1 def __temp_eval_method__(*args, &b) #{str} end RUBY result = object.__temp_eval_method__(*args, &block) eigen.send(:remove_method, :__temp_eval_method__) result else object.instance_eval(str, __FILE__, __LINE__) end else raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated' end end # Evaluates a guard method with support for event arguments passed to transitions. # This method uses arity detection to determine whether to pass event arguments # to the guard, ensuring backward compatibility. # # == Parameters # * object - The object context to evaluate within # * method - The guard method/proc to evaluate # * event_args - Array of arguments passed to the event (optional) # # == Arity-based behavior # * Arity 1: Only passes the object (backward compatible) # * Arity -1 or > 1: Passes object + event arguments # # == Examples # # # Backward compatible single-parameter guard # guard = lambda {|obj| obj.valid? } # evaluate_method_with_event_args(object, guard, [:force]) # => calls guard.call(object) # # # New multi-parameter guard receiving event args # guard = lambda {|obj, *args| obj.valid? && args[0] != :skip } # evaluate_method_with_event_args(object, guard, [:skip]) # => calls guard.call(object, :skip) def evaluate_method_with_event_args(object, method, event_args = []) case method in Symbol # Symbol methods currently don't support event arguments # This maintains backward compatibility evaluate_method(object, method) in Proc => proc arity = proc.arity # Arity-based decision for backward compatibility using pattern matching case arity in 0 proc.call in 1 proc.call(object) in -1 # Splat parameters: object + all event args proc.call(object, *event_args) in arity if arity > 1 # Explicit parameters: object + limited event args args_needed = arity - 1 # Subtract 1 for the object parameter proc.call(object, *event_args[0, args_needed]) else # Negative arity other than -1 (unlikely but handle gracefully) proc.call(object, *event_args) end in Method => meth arity = meth.arity case arity in 0 meth.call in 1 meth.call(object) in -1 meth.call(object, *event_args) in arity if arity > 1 args_needed = arity - 1 meth.call(object, *event_args[0, args_needed]) else meth.call(object, *event_args) end in String # String evaluation doesn't support event arguments for security evaluate_method(object, method) else # Fall back to standard evaluation evaluate_method(object, method) end end private # Validates string input before eval to prevent code injection # This is a basic safety check - not foolproof security def validate_eval_string(method_string) # Check for obviously dangerous patterns dangerous_patterns = [ /`.*`/, # Backticks (shell execution) /system\s*\(/, # System calls /exec\s*\(/, # Exec calls /eval\s*\(/, # Nested eval /require\s+['"]/, # Require statements /load\s+['"]/, # Load statements /File\./, # File operations /IO\./, # IO operations /Dir\./, # Directory operations /Kernel\./ # Kernel operations ] dangerous_patterns.each do |pattern| raise SecurityError, "Potentially dangerous code detected in eval string: #{method_string.inspect}" if method_string.match?(pattern) end # Basic syntax validation - but allow yield since it's valid in block context begin test_code = method_string.include?('yield') ? "def dummy_method; #{method_string}; end" : method_string SyntaxValidator.validate!(test_code, '(eval)') rescue SyntaxError => e raise ArgumentError, "Invalid Ruby syntax in eval string: #{e.message}" end end end end state_machines-0.100.4/lib/state_machines/event.rb000066400000000000000000000240421507333401300220470ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'options_validator' module StateMachines # An event defines an action that transitions an attribute from one state to # another. The state that an attribute is transitioned to depends on the # branches configured for the event. class Event include MatcherHelpers # The state machine for which this event is defined attr_accessor :machine # The name of the event attr_reader :name # The fully-qualified name of the event, scoped by the machine's namespace attr_reader :qualified_name # The human-readable name for the event attr_writer :human_name # The list of branches that determine what state this event transitions # objects to when fired attr_reader :branches # A list of all of the states known to this event using the configured # branches/transitions as the source attr_reader :known_states # Creates a new event within the context of the given machine # # Configuration options: # * :human_name - The human-readable version of this event's name def initialize(machine, name, options = nil, human_name: nil, **extra_options) # :nodoc: # Handle both old hash style and new kwargs style for backward compatibility case options in Hash # Old style: initialize(machine, name, {human_name: 'Custom Name'}) StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name) human_name = options[:human_name] in nil # New style: initialize(machine, name, human_name: 'Custom Name') StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :human_name) unless extra_options.empty? else # Handle unexpected options raise ArgumentError, "Unexpected positional argument in Event initialize: #{options.inspect}" end @machine = machine @name = name @qualified_name = machine.namespace ? :"#{name}_#{machine.namespace}" : name @human_name = human_name || @name.to_s.tr('_', ' ') reset # Output a warning if another event has a conflicting qualified name if (conflict = machine.owner_class.state_machines.detect { |_other_name, other_machine| other_machine != @machine && other_machine.events[qualified_name, :qualified_name] }) _name, other_machine = conflict warn "Event #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}" else add_actions end end # Creates a copy of this event in addition to the list of associated # branches to prevent conflicts across events within a class hierarchy. def initialize_copy(orig) # :nodoc: super @branches = @branches.dup @known_states = @known_states.dup end # Transforms the event name into a more human-readable format, such as # "turn on" instead of "turn_on" def human_name(klass = @machine.owner_class) @human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name end # Evaluates the given block within the context of this event. This simply # provides a DSL-like syntax for defining transitions. def context(&) instance_eval(&) end # Creates a new transition that determines what to change the current state # to when this event fires. # # Since this transition is being defined within an event context, you do # *not* need to specify the :on option for the transition. For # example: # # state_machine do # event :ignite do # transition :parked => :idling, :idling => same, :if => :seatbelt_on? # Transitions to :idling if seatbelt is on # transition all => :parked, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off # end # end # # See StateMachines::Machine#transition for a description of the possible # configurations for defining transitions. def transition(options) raise ArgumentError, 'Must specify as least one transition requirement' if options.empty? # Only a certain subset of explicit options are allowed for transition # requirements StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless, :if_state, :unless_state, :if_all_states, :unless_all_states, :if_any_state, :unless_any_state) if (options.keys - %i[from to on except_from except_to except_on if unless if_state unless_state if_all_states unless_all_states if_any_state unless_any_state]).empty? branches << branch = Branch.new(options.merge(on: name)) @known_states |= branch.known_states branch end # Determines whether any transitions can be performed for this event based # on the current state of the given object. # # If the event can't be fired, then this will return false, otherwise true. # # *Note* that this will not take the object context into account. Although # a transition may be possible based on the state machine definition, # object-specific behaviors (like validations) may prevent it from firing. def can_fire?(object, requirements = {}) !transition_for(object, requirements).nil? end # Finds and builds the next transition that can be performed on the given # object. If no transitions can be made, then this will return nil. # # Valid requirement options: # * :from - One or more states being transitioned from. If none # are specified, then this will be the object's current state. # * :to - One or more states being transitioned to. If none are # specified, then this will match any to state. # * :guard - Whether to guard transitions with the if/unless # conditionals defined for each one. Default is true. # # Event arguments are passed to guard conditions if they accept multiple parameters. def transition_for(object, requirements = {}, *event_args) StateMachines::OptionsValidator.assert_valid_keys!(requirements, :from, :to, :guard) requirements[:from] = machine.states.match!(object).name unless (custom_from_state = requirements.include?(:from)) branches.each do |branch| next unless (match = branch.match(object, requirements, event_args)) # Branch allows for the transition to occur from = requirements[:from] to = if match[:to].is_a?(LoopbackMatcher) from else values = requirements.include?(:to) ? [requirements[:to]].flatten : [from] | machine.states.map { |state| state.name } match[:to].filter(values).first end return Transition.new(object, machine, name, from, to, !custom_from_state) end # No transition matched nil end # Attempts to perform the next available transition on the given object. # If no transitions can be made, then this will return false, otherwise # true. # # Any additional arguments are passed to the StateMachines::Transition#perform # instance method. def fire(object, *event_args) machine.reset(object) if (transition = transition_for(object, {}, *event_args)) transition.perform(*event_args) else on_failure(object, *event_args) false end end # Marks the object as invalid and runs any failure callbacks associated with # this event. This should get called anytime this event fails to transition. def on_failure(object, *args) state = machine.states.match!(object) machine.invalidate(object, :state, :invalid_transition, [[:event, human_name(object.class)], [:state, state.human_name(object.class)]]) transition = Transition.new(object, machine, name, state.name, state.name) transition.args = args if args.any? transition.run_callbacks(before: false) end # Resets back to the initial state of the event, with no branches / known # states associated. This allows you to redefine an event in situations # where you either are re-using an existing state machine implementation # or are subclassing machines. def reset @branches = [] @known_states = [] end def draw(graph, options = {}, io = $stdout) machine.renderer.draw_event(self, graph, options, io) end # Generates a nicely formatted description of this event's contents. # # For example, # # event = StateMachines::Event.new(machine, :park) # event.transition all - :idling => :parked, :idling => same # event # => # :parked, :idling => same]> def inspect transitions = branches.flat_map do |branch| branch.state_requirements.map do |state_requirement| "#{state_requirement[:from].description} => #{state_requirement[:to].description}" end end.join(', ') "#<#{self.class} name=#{name.inspect} transitions=[#{transitions}]>" end protected # Add the various instance methods that can transition the object using # the current event def add_actions # Checks whether the event can be fired on the current object machine.define_helper(:instance, "can_#{qualified_name}?") do |machine, object, *args, **kwargs| machine.event(name).can_fire?(object, *args, **kwargs) end # Gets the next transition that would be performed if the event were # fired now machine.define_helper(:instance, "#{qualified_name}_transition") do |machine, object, *args, **kwargs| machine.event(name).transition_for(object, *args, **kwargs) end # Fires the event machine.define_helper(:instance, qualified_name) do |machine, object, *args, **kwargs| machine.event(name).fire(object, *args, **kwargs) end # Fires the event, raising an exception if it fails machine.define_helper(:instance, "#{qualified_name}!") do |machine, object, *args, **kwargs| object.send(qualified_name, *args, **kwargs) || raise(StateMachines::InvalidTransition.new(object, machine, name)) end end end end state_machines-0.100.4/lib/state_machines/event_collection.rb000066400000000000000000000143201507333401300242600ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines # Represents a collection of events in a state machine class EventCollection < NodeCollection def initialize(machine) # :nodoc: super(machine, index: %i[name qualified_name]) end # Gets the list of events that can be fired on the given object. # # Valid requirement options: # * :from - One or more states being transitioned from. If none # are specified, then this will be the object's current state. # * :to - One or more states being transitioned to. If none are # specified, then this will match any to state. # * :on - One or more events that fire the transition. If none # are specified, then this will match any event. # * :guard - Whether to guard transitions with the if/unless # conditionals defined for each one. Default is true. # # == Examples # # class Vehicle # state_machine :initial => :parked do # event :park do # transition :idling => :parked # end # # event :ignite do # transition :parked => :idling # end # end # end # # events = Vehicle.state_machine(:state).events # # vehicle = Vehicle.new # => # # events.valid_for(vehicle) # => [# :idling]>] # # vehicle.state = 'idling' # events.valid_for(vehicle) # => [# :parked]>] def valid_for(object, requirements = {}) match(requirements).select { |event| event.can_fire?(object, requirements) } end # Gets the list of transitions that can be run on the given object. # # Valid requirement options: # * :from - One or more states being transitioned from. If none # are specified, then this will be the object's current state. # * :to - One or more states being transitioned to. If none are # specified, then this will match any to state. # * :on - One or more events that fire the transition. If none # are specified, then this will match any event. # * :guard - Whether to guard transitions with the if/unless # conditionals defined for each one. Default is true. # # == Examples # # class Vehicle # state_machine :initial => :parked do # event :park do # transition :idling => :parked # end # # event :ignite do # transition :parked => :idling # end # end # end # # events = Vehicle.state_machine.events # # vehicle = Vehicle.new # => # # events.transitions_for(vehicle) # => [#] # # vehicle.state = 'idling' # events.transitions_for(vehicle) # => [#] # # # Search for explicit transitions regardless of the current state # events.transitions_for(vehicle, :from => :parked) # => [#] def transitions_for(object, requirements = {}) match(requirements).map { |event| event.transition_for(object, requirements) }.compact end # Gets the transition that should be performed for the event stored in the # given object's event attribute. This also takes an additional parameter # for automatically invalidating the object if the event or transition are # invalid. By default, this is turned off. # # *Note* that if a transition has already been generated for the event, then # that transition will be used. # # == Examples # # class Vehicle < ActiveRecord::Base # state_machine :initial => :parked do # event :ignite do # transition :parked => :idling # end # end # end # # vehicle = Vehicle.new # => # # events = Vehicle.state_machine.events # # vehicle.state_event = nil # events.attribute_transition_for(vehicle) # => nil # Event isn't defined # # vehicle.state_event = 'invalid' # events.attribute_transition_for(vehicle) # => false # Event is invalid # # vehicle.state_event = 'ignite' # events.attribute_transition_for(vehicle) # => # def attribute_transition_for(object, invalidate = false) return unless machine.action # TODO, simplify # First try the regular event_transition transition = machine.read(object, :event_transition) # If not found and we have stored transitions by machine (issue #91) if !transition && (transitions_by_machine = object.instance_variable_get(:@_state_machine_event_transitions)) transition = transitions_by_machine[machine.name] end transition || if event_name = machine.read(object, :event) if event = self[event_name.to_sym, :name] event.transition_for(object) || begin # No valid transition: invalidate machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).human_name(object.class)]]) if invalidate false end else # Event is unknown: invalidate machine.invalidate(object, :event, :invalid) if invalidate false end end end private def match(requirements) # :nodoc: requirements && requirements[:on] ? [fetch(requirements.delete(:on))] : self end end end state_machines-0.100.4/lib/state_machines/extensions.rb000066400000000000000000000125141507333401300231260ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines module ClassMethods def self.extended(base) # :nodoc: base.class_eval do @state_machines = MachineCollection.new end end # Gets the current list of state machines defined for this class. This # class-level attribute acts like an inheritable attribute. The attribute # is available to each subclass, each having a copy of its superclass's # attribute. # # The hash of state machines maps :attribute => +machine+, e.g. # # Vehicle.state_machines # => {:state => #} def state_machines @state_machines ||= superclass.state_machines.dup end end module InstanceMethods # Runs one or more events in parallel. All events will run through the # following steps: # * Before callbacks # * Persist state # * Invoke action # * After callbacks # # For example, if two events (for state machines A and B) are run in # parallel, the order in which steps are run is: # * A - Before transition callbacks # * B - Before transition callbacks # * A - Persist new state # * B - Persist new state # * A - Invoke action # * B - Invoke action (only if different than A's action) # * A - After transition callbacks # * B - After transition callbacks # # *Note* that multiple events on the same state machine / attribute cannot # be run in parallel. If this is attempted, an ArgumentError will be # raised. # # == Halting callbacks # # When running multiple events in parallel, special consideration should # be taken with regard to how halting within callbacks affects the flow. # # For *before* callbacks, any :halt error that's thrown will # immediately cancel the perform for all transitions. As a result, it's # possible for one event's transition to affect the continuation of # another. # # On the other hand, any :halt error that's thrown within an # *after* callback with only affect that event's transition. Other # transitions will continue to run their own callbacks. # # == Example # # class Vehicle # state_machine :initial => :parked do # event :ignite do # transition :parked => :idling # end # # event :park do # transition :idling => :parked # end # end # # state_machine :alarm_state, :namespace => 'alarm', :initial => :on do # event :enable do # transition all => :active # end # # event :disable do # transition all => :off # end # end # end # # vehicle = Vehicle.new # => # # vehicle.state # => "parked" # vehicle.alarm_state # => "active" # # vehicle.fire_events(:ignite, :disable_alarm) # => true # vehicle.state # => "idling" # vehicle.alarm_state # => "off" # # # If any event fails, the entire event chain fails # vehicle.fire_events(:ignite, :enable_alarm) # => false # vehicle.state # => "idling" # vehicle.alarm_state # => "off" # # # Exception raised on invalid event # vehicle.fire_events(:park, :invalid) # => StateMachines::InvalidEvent: :invalid is an unknown event # vehicle.state # => "idling" # vehicle.alarm_state # => "off" def fire_events(*events) self.class.state_machines.fire_events(self, *events) end # Run one or more events in parallel. If any event fails to run, then # a StateMachines::InvalidTransition exception will be raised. # # See StateMachines::InstanceMethods#fire_events for more information. # # == Example # # class Vehicle # state_machine :initial => :parked do # event :ignite do # transition :parked => :idling # end # # event :park do # transition :idling => :parked # end # end # # state_machine :alarm_state, :namespace => 'alarm', :initial => :active do # event :enable do # transition all => :active # end # # event :disable do # transition all => :off # end # end # end # # vehicle = Vehicle.new # => # # vehicle.fire_events(:ignite, :disable_alarm) # => true # # vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachines::InvalidParallelTransition: Cannot run events in parallel: ignite, disable_alarm def fire_events!(*events) run_action = [true, false].include?(events.last) ? events.pop : true fire_events(*(events + [run_action])) || raise(StateMachines::InvalidParallelTransition.new(self, events)) end protected def initialize_state_machines(options = {}, &) # :nodoc: self.class.state_machines.initialize_states(self, options, &) end end end state_machines-0.100.4/lib/state_machines/helper_module.rb000066400000000000000000000011051507333401300235450ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines # Represents a type of module that defines instance / class methods for a # state machine class HelperModule < Module # :nodoc: def initialize(machine, kind) @machine = machine @kind = kind end # Provides a human-readable description of the module def to_s owner_class = @machine.owner_class owner_class_name = owner_class.name && !owner_class.name.empty? ? owner_class.name : owner_class.to_s "#{owner_class_name} #{@machine.name.inspect} #{@kind} helpers" end end end state_machines-0.100.4/lib/state_machines/integrations.rb000066400000000000000000000100741507333401300234340ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines # Integrations allow state machines to take advantage of features within the # context of a particular library. This is currently most useful with # database libraries. For example, the various database integrations allow # state machines to hook into features like: # * Saving # * Transactions # * Observers # * Scopes # * Callbacks # * Validation errors # # This type of integration allows the user to work with state machines in a # fashion similar to other object models in their application. # # The integration interface is loosely defined by various unimplemented # methods in the StateMachines::Machine class. See that class or the various # built-in integrations for more information about how to define additional # integrations. module Integrations @integrations = [] class << self # Register integration def register(name_or_module) case name_or_module.class.to_s when 'Module' add(name_or_module) else raise IntegrationError end true end def reset # :nodoc:# @integrations = [] end # Gets a list of all of the available integrations for use. # # == Example # # StateMachines::Integrations.integrations # # => [] # StateMachines::Integrations.register(StateMachines::Integrations::ActiveModel) # StateMachines::Integrations.integrations # # => [StateMachines::Integrations::ActiveModel] attr_reader :integrations alias list integrations # Attempts to find an integration that matches the given class. This will # look through all of the built-in integrations under the StateMachines::Integrations # namespace and find one that successfully matches the class. # # == Examples # # class Vehicle # end # # class ActiveModelVehicle # include ActiveModel::Observing # include ActiveModel::Validations # end # # class ActiveRecordVehicle < ActiveRecord::Base # end # # StateMachines::Integrations.match(Vehicle) # => nil # StateMachines::Integrations.match(ActiveModelVehicle) # => StateMachines::Integrations::ActiveModel # StateMachines::Integrations.match(ActiveRecordVehicle) # => StateMachines::Integrations::ActiveRecord def match(klass) integrations.detect { |integration| integration.matches?(klass) } end # Attempts to find an integration that matches the given list of ancestors. # This will look through all of the built-in integrations under the StateMachines::Integrations # namespace and find one that successfully matches one of the ancestors. # # == Examples # # StateMachines::Integrations.match_ancestors([]) # => nil # StateMachines::Integrations.match_ancestors([ActiveRecord::Base]) # => StateMachines::Integrations::ActiveModel def match_ancestors(ancestors) integrations.detect { |integration| integration.matches_ancestors?(ancestors) } end # Finds an integration with the given name. If the integration cannot be # found, then a NameError exception will be raised. # # == Examples # # StateMachines::Integrations.find_by_name(:active_model) # => StateMachines::Integrations::ActiveModel # StateMachines::Integrations.find_by_name(:active_record) # => StateMachines::Integrations::ActiveRecord # StateMachines::Integrations.find_by_name(:invalid) # => StateMachines::IntegrationNotFound: :invalid is an invalid integration def find_by_name(name) integrations.detect { |integration| integration.integration_name == name } || raise(IntegrationNotFound.new(name)) end private def add(integration) return unless integration.respond_to?(:integration_name) @integrations.insert(0, integration) unless @integrations.include?(integration) end end end end state_machines-0.100.4/lib/state_machines/integrations/000077500000000000000000000000001507333401300231055ustar00rootroot00000000000000state_machines-0.100.4/lib/state_machines/integrations/base.rb000066400000000000000000000026721507333401300243530ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines module Integrations # Provides a set of base helpers for managing individual integrations module Base module ClassMethods # The default options to use for state machines using this integration attr_reader :defaults # The name of the integration def integration_name @integration_name ||= begin name = self.name.split('::').last name.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2') name.gsub!(/([a-z\d])([A-Z])/, '\1_\2') name.downcase! name.to_sym end end # The list of ancestor names that cause this integration to matched. def matching_ancestors [] end # Whether the integration should be used for the given class. def matches?(klass) matching_ancestors.any? { |ancestor| klass <= ancestor } end # Whether the integration should be used for the given list of ancestors. def matches_ancestors?(ancestors) (ancestors & matching_ancestors).any? end # Additional options that this integration adds to the state machine. # Integrations can override this method to specify additional valid options. def integration_options [] end end def self.included(base) # :nodoc: base.extend ClassMethods end end end end state_machines-0.100.4/lib/state_machines/machine.rb000066400000000000000000002013221507333401300223300ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'options_validator' require_relative 'machine/class_methods' require_relative 'machine/utilities' require_relative 'machine/parsing' require_relative 'machine/validation' require_relative 'machine/helper_generators' require_relative 'machine/action_hooks' require_relative 'machine/scoping' require_relative 'machine/configuration' require_relative 'machine/state_methods' require_relative 'machine/event_methods' require_relative 'machine/callbacks' require_relative 'machine/rendering' require_relative 'machine/integration' require_relative 'machine/async_extensions' require_relative 'syntax_validator' module StateMachines # Represents a state machine for a particular attribute. State machines # consist of states, events and a set of transitions that define how the # state changes after a particular event is fired. # # A state machine will not know all of the possible states for an object # unless they are referenced *somewhere* in the state machine definition. # As a result, any unused states should be defined with the +other_states+ # or +state+ helper. # # == Actions # # When an action is configured for a state machine, it is invoked when an # object transitions via an event. The success of the event becomes # dependent on the success of the action. If the action is successful, then # the transitioned state remains persisted. However, if the action fails # (by returning false), the transitioned state will be rolled back. # # For example, # # class Vehicle # attr_accessor :fail, :saving_state # # state_machine :initial => :parked, :action => :save do # event :ignite do # transition :parked => :idling # end # # event :park do # transition :idling => :parked # end # end # # def save # @saving_state = state # fail != true # end # end # # vehicle = Vehicle.new # => # # vehicle.save # => true # vehicle.saving_state # => "parked" # The state was "parked" was save was called # # # Successful event # vehicle.ignite # => true # vehicle.saving_state # => "idling" # The state was "idling" when save was called # vehicle.state # => "idling" # # # Failed event # vehicle.fail = true # vehicle.park # => false # vehicle.saving_state # => "parked" # vehicle.state # => "idling" # # As shown, even though the state is set prior to calling the +save+ action # on the object, it will be rolled back to the original state if the action # fails. *Note* that this will also be the case if an exception is raised # while calling the action. # # === Indirect transitions # # In addition to the action being run as the _result_ of an event, the action # can also be used to run events itself. For example, using the above as an # example: # # vehicle = Vehicle.new # => # # # vehicle.state_event = 'ignite' # vehicle.save # => true # vehicle.state # => "idling" # vehicle.state_event # => nil # # As can be seen, the +save+ action automatically invokes the event stored in # the +state_event+ attribute (:ignite in this case). # # One important note about using this technique for running transitions is # that if the class in which the state machine is defined *also* defines the # action being invoked (and not a superclass), then it must manually run the # StateMachine hook that checks for event attributes. # # For example, in ActiveRecord, DataMapper, Mongoid, MongoMapper, and Sequel, # the default action (+save+) is already defined in a base class. As a result, # when a state machine is defined in a model / resource, StateMachine can # automatically hook into the +save+ action. # # On the other hand, the Vehicle class from above defined its own +save+ # method (and there is no +save+ method in its superclass). As a result, it # must be modified like so: # # def save # self.class.state_machines.transitions(self, :save).perform do # @saving_state = state # fail != true # end # end # # This will add in the functionality for firing the event stored in the # +state_event+ attribute. # # == Callbacks # # Callbacks are supported for hooking before and after every possible # transition in the machine. Each callback is invoked in the order in which # it was defined. See StateMachines::Machine#before_transition and # StateMachines::Machine#after_transition for documentation on how to define # new callbacks. # # *Note* that callbacks only get executed within the context of an event. As # a result, if a class has an initial state when it's created, any callbacks # that would normally get executed when the object enters that state will # *not* get triggered. # # For example, # # class Vehicle # state_machine initial: :parked do # after_transition all => :parked do # raise ArgumentError # end # ... # end # end # # vehicle = Vehicle.new # => # # vehicle.save # => true (no exception raised) # # If you need callbacks to get triggered when an object is created, this # should be done by one of the following techniques: # * Use a before :create or equivalent hook: # # class Vehicle # before :create, :track_initial_transition # # state_machine do # ... # end # end # # * Set an initial state and use the correct event to create the # object with the proper state, resulting in callbacks being triggered and # the object getting persisted (note that the :pending state is # actually stored as nil): # # class Vehicle # state_machine initial: :pending # after_transition pending: :parked, do: :track_initial_transition # # event :park do # transition pending: :parked # end # # state :pending, value: nil # end # end # # vehicle = Vehicle.new # vehicle.park # # * Use a default event attribute that will automatically trigger when the # configured action gets run (note that the :pending state is # actually stored as nil): # # class Vehicle < ActiveRecord::Base # state_machine initial: :pending # after_transition pending: :parked, do: :track_initial_transition # # event :park do # transition pending: :parked # end # # state :pending, value: nil # end # # def initialize(*) # super # self.state_event = 'park' # end # end # # vehicle = Vehicle.new # vehicle.save # # === Canceling callbacks # # Callbacks can be canceled by throwing :halt at any point during the # callback. For example, # # ... # throw :halt # ... # # If a +before+ callback halts the chain, the associated transition and all # later callbacks are canceled. If an +after+ callback halts the chain, # the later callbacks are canceled, but the transition is still successful. # # These same rules apply to +around+ callbacks with the exception that any # +around+ callback that doesn't yield will essentially result in :halt being # thrown. Any code executed after the yield will behave in the same way as # +after+ callbacks. # # *Note* that if a +before+ callback fails and the bang version of an event # was invoked, an exception will be raised instead of returning false. For # example, # # class Vehicle # state_machine :initial => :parked do # before_transition any => :idling, :do => lambda {|vehicle| throw :halt} # ... # end # end # # vehicle = Vehicle.new # vehicle.park # => false # vehicle.park! # => StateMachines::InvalidTransition: Cannot transition state via :park from "idling" # # == Observers # # Observers, in the sense of external classes and *not* Ruby's Observable # mechanism, can hook into state machines as well. Such observers use the # same callback api that's used internally. # # Below are examples of defining observers for the following state machine: # # class Vehicle # state_machine do # event :park do # transition idling: :parked # end # ... # end # ... # end # # Event/Transition behaviors: # # class VehicleObserver # def self.before_park(vehicle, transition) # logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}" # end # # def self.after_park(vehicle, transition, result) # logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}" # end # # def self.before_transition(vehicle, transition) # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}" # end # # def self.after_transition(vehicle, transition) # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}" # end # # def self.around_transition(vehicle, transition) # logger.info Benchmark.measure { yield } # end # end # # Vehicle.state_machine do # before_transition :on => :park, :do => VehicleObserver.method(:before_park) # before_transition VehicleObserver.method(:before_transition) # # after_transition :on => :park, :do => VehicleObserver.method(:after_park) # after_transition VehicleObserver.method(:after_transition) # # around_transition VehicleObserver.method(:around_transition) # end # # One common callback is to record transitions for all models in the system # for auditing/debugging purposes. Below is an example of an observer that # can easily automate this process for all models: # # class StateMachineObserver # def self.before_transition(object, transition) # Audit.log_transition(object.attributes) # end # end # # [Vehicle, Switch, Project].each do |klass| # klass.state_machines.each do |attribute, machine| # machine.before_transition StateMachineObserver.method(:before_transition) # end # end # # Additional observer-like behavior may be exposed by the various integrations # available. See below for more information on integrations. # # == Overriding instance / class methods # # Hooking in behavior to the generated instance / class methods from the # state machine, events, and states is very simple because of the way these # methods are generated on the class. Using the class's ancestors, the # original generated method can be referred to via +super+. For example, # # class Vehicle # state_machine do # event :park do # ... # end # end # # def park(*args) # logger.info "..." # super # end # end # # In the above example, the +park+ instance method that's generated on the # Vehicle class (by the associated event) is overridden with custom behavior. # Once this behavior is complete, the original method from the state machine # is invoked by simply calling +super+. # # The same technique can be used for +state+, +state_name+, and all other # instance *and* class methods on the Vehicle class. # # == Method conflicts # # By default state_machine does not redefine methods that exist on # superclasses (*including* Object) or any modules (*including* Kernel) that # were included before it was defined. This is in order to ensure that # existing behavior on the class is not broken by the inclusion of # state_machine. # # If a conflicting method is detected, state_machine will generate a warning. # For example, consider the following class: # # class Vehicle # state_machine do # event :open do # ... # end # end # end # # In the above class, an event named "open" is defined for its state machine. # However, "open" is already defined as an instance method in Ruby's Kernel # module that gets included in every Object. As a result, state_machine will # generate the following warning: # # Instance method "open" is already defined in Object, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true. # # Even though you may not be using Kernel's implementation of the "open" # instance method, state_machine isn't aware of this and, as a result, stays # safe and just skips redefining the method. # # As with almost all helpers methods defined by state_machine in your class, # there are generic methods available for working around this method conflict. # In the example above, you can invoke the "open" event like so: # # vehicle = Vehicle.new # => # # vehicle.fire_events(:open) # => true # # # This will not work # vehicle.open # => NoMethodError: private method `open' called for # # # If you want to take on the risk of overriding existing methods and just # ignore method conflicts altogether, you can do so by setting the following # configuration: # # StateMachines::Machine.ignore_method_conflicts = true # # This will allow you to define events like "open" as described above and # still generate the "open" instance helper method. For example: # # StateMachines::Machine.ignore_method_conflicts = true # # class Vehicle # state_machine do # event :open do # ... # end # end # # vehicle = Vehicle.new # => # # vehicle.open # => true # # By default, state_machine helps prevent you from making mistakes and # accidentally overriding methods that you didn't intend to. Once you # understand this and what the consequences are, setting the # +ignore_method_conflicts+ option is a perfectly reasonable workaround. # # == Integrations # # By default, state machines are library-agnostic, meaning that they work # on any Ruby class and have no external dependencies. However, there are # certain libraries which expose additional behavior that can be taken # advantage of by state machines. # # This library is built to work out of the box with a few popular Ruby # libraries that allow for additional behavior to provide a cleaner and # smoother experience. This is especially the case for objects backed by a # database that may allow for transactions, persistent storage, # search/filters, callbacks, etc. # # When a state machine is defined for classes using any of the above libraries, # it will try to automatically determine the integration to use (Agnostic, # ActiveModel, ActiveRecord, DataMapper, Mongoid, MongoMapper, or Sequel) # based on the class definition. To see how each integration affects the # machine's behavior, refer to all constants defined under the # StateMachines::Integrations namespace. class Machine extend ClassMethods include EvalHelpers include MatcherHelpers include Utilities include Parsing include Validation include HelperGenerators include ActionHooks include Scoping include Configuration include StateMethods include EventMethods include Callbacks include Rendering include Integration # Whether to ignore any conflicts that are detected for helper methods that # get generated for a machine's owner class. Default is false. # Thread-safe via atomic reference updates @ignore_method_conflicts = false # The class that the machine is defined in attr_reader :owner_class # The name of the machine, used for scoping methods generated for the # machine as a whole (not states or events) attr_reader :name # The events that trigger transitions. These are sorted, by default, in # the order in which they were defined. attr_reader :events # A list of all of the states known to this state machine. This will pull # states from the following sources: # * Initial state # * State behaviors # * Event transitions (:to, :from, and :except_from options) # * Transition callbacks (:to, :from, :except_to, and :except_from options) # * Unreferenced states (using +other_states+ helper) # # These are sorted, by default, in the order in which they were referenced. attr_reader :states # The callbacks to invoke before/after a transition is performed # # Maps :before => callbacks and :after => callbacks attr_reader :callbacks # The action to invoke when an object transitions attr_reader :action # An identifier that forces all methods (including state predicates and # event methods) to be generated with the value prefixed or suffixed, # depending on the context. attr_reader :namespace # Whether the machine will use transactions when firing events attr_reader :use_transactions # Creates a new state machine for the given attribute # Gets the initial state of the machine for the given object. If a dynamic # initial state was configured for this machine, then the object will be # passed into the lambda block to help determine the actual state. # # == Examples # # With a static initial state: # # class Vehicle # state_machine :initial => :parked do # ... # end # end # # vehicle = Vehicle.new # Vehicle.state_machine.initial_state(vehicle) # => # # # With a dynamic initial state: # # class Vehicle # attr_accessor :force_idle # # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do # ... # end # end # # vehicle = Vehicle.new # # vehicle.force_idle = true # Vehicle.state_machine.initial_state(vehicle) # => # # # vehicle.force_idle = false # Vehicle.state_machine.initial_state(vehicle) # => # # Defines a new helper method in an instance or class scope with the given # name. If the method is already defined in the scope, then this will not # override it. # # If passing in a block, there are two side effects to be aware of # 1. The method cannot be chained, meaning that the block cannot call +super+ # 2. If the method is already defined in an ancestor, then it will not get # overridden and a warning will be output. # # Example: # # # Instance helper # machine.define_helper(:instance, :state_name) do |machine, object| # machine.states.match(object).name # end # # # Class helper # machine.define_helper(:class, :state_machine_name) do |machine, klass| # "State" # end # # You can also define helpers using string evaluation like so: # # # Instance helper # machine.define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1 # def state_name # self.class.state_machine(:state).states.match(self).name # end # end_eval # # # Class helper # machine.define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1 # def state_machine_name # "State" # end # end_eval def define_helper(scope, method, *, **, &block) helper_module = @helper_modules.fetch(scope) if block_given? if !self.class.ignore_method_conflicts && (conflicting_ancestor = owner_class_ancestor_has_method?(scope, method)) ancestor_name = conflicting_ancestor.name && !conflicting_ancestor.name.empty? ? conflicting_ancestor.name : conflicting_ancestor.to_s warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true." else name = self.name helper_module.class_eval do define_method(method) do |*args, **kwargs| block.call((scope == :instance ? self.class : self).state_machine(name), self, *args, **kwargs) end end end else helper_module.class_eval(method, __FILE__, __LINE__) end end # Customizes the definition of one or more states in the machine. # # Configuration options: # * :value - The actual value to store when an object transitions # to the state. Default is the name (stringified). # * :cache - If a dynamic value (via a lambda block) is being used, # then setting this to true will cache the evaluated result # * :if - Determines whether an object's value matches the state # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}). # By default, the configured value is matched. # * :human_name - The human-readable version of this state's name. # By default, this is either defined by the integration or stringifies the # name and converts underscores to spaces. # # == Customizing the stored value # # Whenever a state is automatically discovered in the state machine, its # default value is assumed to be the stringified version of the name. For # example, # # class Vehicle # state_machine :initial => :parked do # event :ignite do # transition :parked => :idling # end # end # end # # In the above state machine, there are two states automatically discovered: # :parked and :idling. These states, by default, will store their stringified # equivalents when an object moves into that state (e.g. "parked" / "idling"). # # For legacy systems or when tying state machines into existing frameworks, # it's oftentimes necessary to need to store a different value for a state # than the default. In order to continue taking advantage of an expressive # state machine and helper methods, every defined state can be re-configured # with a custom stored value. For example, # # class Vehicle # state_machine :initial => :parked do # event :ignite do # transition :parked => :idling # end # # state :idling, :value => 'IDLING' # state :parked, :value => 'PARKED # end # end # # This is also useful if being used in association with a database and, # instead of storing the state name in a column, you want to store the # state's foreign key: # # class VehicleState < ActiveRecord::Base # end # # class Vehicle < ActiveRecord::Base # state_machine :attribute => :state_id, :initial => :parked do # event :ignite do # transition :parked => :idling # end # # states.each do |state| # self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true) # end # end # end # # In the above example, each known state is configured to store it's # associated database id in the +state_id+ attribute. Also, notice that a # lambda block is used to define the state's value. This is required in # situations (like testing) where the model is loaded without any existing # data (i.e. no VehicleState records available). # # One caveat to the above example is to keep performance in mind. To avoid # constant db hits for looking up the VehicleState ids, the value is cached # by specifying the :cache option. Alternatively, a custom # caching strategy can be used like so: # # class VehicleState < ActiveRecord::Base # cattr_accessor :cache_store # self.cache_store = ActiveSupport::Cache::MemoryStore.new # # def self.find_by_name(name) # cache_store.fetch(name) { find(:first, :conditions => {:name => name}) } # end # end # # === Dynamic values # # In addition to customizing states with other value types, lambda blocks # can also be specified to allow for a state's value to be determined # dynamically at runtime. For example, # # class Vehicle # state_machine :purchased_at, :initial => :available do # event :purchase do # transition all => :purchased # end # # event :restock do # transition all => :available # end # # state :available, :value => nil # state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now} # end # end # # In the above definition, the :purchased state is customized with # both a dynamic value *and* a value matcher. # # When an object transitions to the purchased state, the value's lambda # block will be called. This will get the current time and store it in the # object's +purchased_at+ attribute. # # *Note* that the custom matcher is very important here. Since there's no # way for the state machine to figure out an object's state when it's set to # a runtime value, it must be explicitly defined. If the :if option # were not configured for the state, then an ArgumentError exception would # be raised at runtime, indicating that the state machine could not figure # out what the current state of the object was. # # == Behaviors # # Behaviors define a series of methods to mixin with objects when the current # state matches the given one(s). This allows instance methods to behave # a specific way depending on what the value of the object's state is. # # For example, # # class Vehicle # attr_accessor :driver # attr_accessor :passenger # # state_machine :initial => :parked do # event :ignite do # transition :parked => :idling # end # # state :parked do # def speed # 0 # end # # def rotate_driver # driver = self.driver # self.driver = passenger # self.passenger = driver # true # end # end # # state :idling, :first_gear do # def speed # 20 # end # # def rotate_driver # self.state = 'parked' # rotate_driver # end # end # # other_states :backing_up # end # end # # In the above example, there are two dynamic behaviors defined for the # class: # * +speed+ # * +rotate_driver+ # # Each of these behaviors are instance methods on the Vehicle class. However, # which method actually gets invoked is based on the current state of the # object. Using the above class as the example: # # vehicle = Vehicle.new # vehicle.driver = 'John' # vehicle.passenger = 'Jane' # # # Behaviors in the "parked" state # vehicle.state # => "parked" # vehicle.speed # => 0 # vehicle.rotate_driver # => true # vehicle.driver # => "Jane" # vehicle.passenger # => "John" # # vehicle.ignite # => true # # # Behaviors in the "idling" state # vehicle.state # => "idling" # vehicle.speed # => 20 # vehicle.rotate_driver # => true # vehicle.driver # => "John" # vehicle.passenger # => "Jane" # # As can be seen, both the +speed+ and +rotate_driver+ instance method # implementations changed how they behave based on what the current state # of the vehicle was. # # === Invalid behaviors # # If a specific behavior has not been defined for a state, then a # NoMethodError exception will be raised, indicating that that method would # not normally exist for an object with that state. # # Using the example from before: # # vehicle = Vehicle.new # vehicle.state = 'backing_up' # vehicle.speed # => NoMethodError: undefined method 'speed' for # in state "backing_up" # # === Using matchers # # The +all+ / +any+ matchers can be used to easily define behaviors for a # group of states. Note, however, that you cannot use these matchers to # set configurations for states. Behaviors using these matchers can be # defined at any point in the state machine and will always get applied to # the proper states. # # For example: # # state_machine :initial => :parked do # ... # # state all - [:parked, :idling, :stalled] do # validates_presence_of :speed # # def speed # gear * 10 # end # end # end # # == State-aware class methods # # In addition to defining scopes for instance methods that are state-aware, # the same can be done for certain types of class methods. # # Some libraries have support for class-level methods that only run certain # behaviors based on a conditions hash passed in. For example: # # class Vehicle < ActiveRecord::Base # state_machine do # ... # state :first_gear, :second_gear, :third_gear do # validates_presence_of :speed # validates_inclusion_of :speed, :in => 0..25, :if => :in_school_zone? # end # end # end # # In the above ActiveRecord model, two validations have been defined which # will *only* run when the Vehicle object is in one of the three states: # +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless # conditions can continue to be used. # # This functionality is not library-specific and can work for any class-level # method that is defined like so: # # def validates_presence_of(attribute, options = {}) # ... # end # # The minimum requirement is that the last argument in the method be an # options hash which contains at least :if condition support. # Defines one or more events for the machine and the transitions that can # be performed when those events are run. # # This method is also aliased as +on+ for improved compatibility with # using a domain-specific language. # # Configuration options: # * :human_name - The human-readable version of this event's name. # By default, this is either defined by the integration or stringifies the # name and converts underscores to spaces. # # == Instance methods # # The following instance methods are generated when a new event is defined # (the "park" event is used as an example): # * park(..., run_action = true) - Fires the "park" event, # transitioning from the current state to the next valid state. If the # last argument is a boolean, it will control whether the machine's action # gets run. # * park!(..., run_action = true) - Fires the "park" event, # transitioning from the current state to the next valid state. If the # transition fails, then a StateMachines::InvalidTransition error will be # raised. If the last argument is a boolean, it will control whether the # machine's action gets run. # * can_park?(requirements = {}) - Checks whether the "park" event # can be fired given the current state of the object. This will *not* run # validations or callbacks in ORM integrations. It will only determine if # the state machine defines a valid transition for the event. To check # whether an event can fire *and* passes validations, use event attributes # (e.g. state_event) as described in the "Events" documentation of each # ORM integration. # * park_transition(requirements = {}) - Gets the next transition # that would be performed if the "park" event were to be fired now on the # object or nil if no transitions can be performed. Like can_park? # this will also *not* run validations or callbacks. It will only # determine if the state machine defines a valid transition for the event. # # With a namespace of "car", the above names map to the following methods: # * can_park_car? # * park_car_transition # * park_car # * park_car! # # The can_park? and park_transition helpers both take an # optional set of requirements for determining what transitions are available # for the current object. These requirements include: # * :from - One or more states to transition from. If none are # specified, then this will be the object's current state. # * :to - One or more states to transition to. If none are # specified, then this will match any to state. # * :guard - Whether to guard transitions with the if/unless # conditionals defined for each one. Default is true. # # == Defining transitions # # +event+ requires a block which allows you to define the possible # transitions that can happen as a result of that event. For example, # # event :park, :stop do # transition :idling => :parked # end # # event :first_gear do # transition :parked => :first_gear, :if => :seatbelt_on? # transition :parked => same # Allow to loopback if seatbelt is off # end # # See StateMachines::Event#transition for more information on # the possible options that can be passed in. # # *Note* that this block is executed within the context of the actual event # object. As a result, you will not be able to reference any class methods # on the model without referencing the class itself. For example, # # class Vehicle # def self.safe_states # [:parked, :idling, :stalled] # end # # state_machine do # event :park do # transition Vehicle.safe_states => :parked # end # end # end # # == Overriding the event method # # By default, this will define an instance method (with the same name as the # event) that will fire the next possible transition for that. Although the # +before_transition+, +after_transition+, and +around_transition+ hooks # allow you to define behavior that gets executed as a result of the event's # transition, you can also override the event method in order to have a # little more fine-grained control. # # For example: # # class Vehicle # state_machine do # event :park do # ... # end # end # # def park(*) # take_deep_breath # Executes before the transition (and before_transition hooks) even if no transition is possible # if result = super # Runs the transition and all before/after/around hooks # applaud # Executes after the transition (and after_transition hooks) # end # result # end # end # # There are a few important things to note here. First, the method # signature is defined with an unlimited argument list in order to allow # callers to continue passing arguments that are expected by state_machine. # For example, it will still allow calls to +park+ with a single parameter # for skipping the configured action. # # Second, the overridden event method must call +super+ in order to run the # logic for running the next possible transition. In order to remain # consistent with other events, the result of +super+ is returned. # # Third, any behavior defined in this method will *not* get executed if # you're taking advantage of attribute-based event transitions. For example: # # vehicle = Vehicle.new # vehicle.state_event = 'park' # vehicle.save # # In this case, the +park+ event will run the before/after/around transition # hooks and transition the state, but the behavior defined in the overriden # +park+ method will *not* be executed. # # == Defining additional arguments # # Additional arguments can be passed into events and accessed by transition # hooks like so: # # class Vehicle # state_machine do # after_transition :on => :park do |vehicle, transition| # kind = *transition.args # :parallel # ... # end # after_transition :on => :park, :do => :take_deep_breath # # event :park do # ... # end # # def take_deep_breath(transition) # kind = *transition.args # :parallel # ... # end # end # end # # vehicle = Vehicle.new # vehicle.park(:parallel) # # *Remember* that if the last argument is a boolean, it will be used as the # +run_action+ parameter to the event action. Using the +park+ action # example from above, you can might call it like so: # # vehicle.park # => Uses default args and runs machine action # vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action # vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action # # If you decide to override the +park+ event method *and* define additional # arguments, you can do so as shown below: # # class Vehicle # state_machine do # event :park do # ... # end # end # # def park(kind = :parallel, *args) # take_deep_breath if kind == :parallel # super # end # end # # Note that +super+ is called instead of super(*args). This allow # the entire arguments list to be accessed by transition callbacks through # StateMachines::Transition#args. # # === Using matchers # # The +all+ / +any+ matchers can be used to easily execute blocks for a # group of events. Note, however, that you cannot use these matchers to # set configurations for events. Blocks using these matchers can be # defined at any point in the state machine and will always get applied to # the proper events. # # For example: # # state_machine :initial => :parked do # ... # # event all - [:crash] do # transition :stalled => :parked # end # end # # == Example # # class Vehicle # state_machine do # # The park, stop, and halt events will all share the given transitions # event :park, :stop, :halt do # transition [:idling, :backing_up] => :parked # end # # event :stop do # transition :first_gear => :idling # end # # event :ignite do # transition :parked => :idling # transition :idling => same # Allow ignite while still idling # end # end # end # Creates a new transition that determines what to change the current state # to when an event fires. # # == Defining transitions # # The options for a new transition uses the Hash syntax to map beginning # states to ending states. For example, # # transition :parked => :idling, :idling => :first_gear, :on => :ignite # # In this case, when the +ignite+ event is fired, this transition will cause # the state to be +idling+ if it's current state is +parked+ or +first_gear+ # if it's current state is +idling+. # # To help define these implicit transitions, a set of helpers are available # for slightly more complex matching: # * all - Matches every state in the machine # * all - [:parked, :idling, ...] - Matches every state except those specified # * any - An alias for +all+ (matches every state in the machine) # * same - Matches the same state being transitioned from # # See StateMachines::MatcherHelpers for more information. # # Examples: # # transition all => nil, :on => :ignite # Transitions to nil regardless of the current state # transition all => :idling, :on => :ignite # Transitions to :idling regardless of the current state # transition all - [:idling, :first_gear] => :idling, :on => :ignite # Transitions every state but :idling and :first_gear to :idling # transition nil => :idling, :on => :ignite # Transitions to :idling from the nil state # transition :parked => :idling, :on => :ignite # Transitions to :idling if :parked # transition [:parked, :stalled] => :idling, :on => :ignite # Transitions to :idling if :parked or :stalled # # transition :parked => same, :on => :park # Loops :parked back to :parked # transition [:parked, :stalled] => same, :on => [:park, :stall] # Loops either :parked or :stalled back to the same state on the park and stall events # transition all - :parked => same, :on => :noop # Loops every state but :parked back to the same state # # # Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear # transition :parked => :idling, :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up # # == Verbose transitions # # Transitions can also be defined use an explicit set of configuration # options: # * :from - A state or array of states that can be transitioned from. # If not specified, then the transition can occur for *any* state. # * :to - The state that's being transitioned to. If not specified, # then the transition will simply loop back (i.e. the state will not change). # * :except_from - A state or array of states that *cannot* be # transitioned from. # # These options must be used when defining transitions within the context # of a state. # # Examples: # # transition :to => nil, :on => :park # transition :to => :idling, :on => :ignite # transition :except_from => [:idling, :first_gear], :to => :idling, :on => :ignite # transition :from => nil, :to => :idling, :on => :ignite # transition :from => [:parked, :stalled], :to => :idling, :on => :ignite # # == Conditions # # In addition to the state requirements for each transition, a condition # can also be defined to help determine whether that transition is # available. These options will work on both the normal and verbose syntax. # # Configuration options: # * :if - A method, proc or string to call to determine if the # transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}). # The condition should return or evaluate to true or false. # * :unless - A method, proc or string to call to determine if the # transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}). # The condition should return or evaluate to true or false. # # Examples: # # transition :parked => :idling, :on => :ignite, :if => :moving? # transition :parked => :idling, :on => :ignite, :unless => :stopped? # transition :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up, :if => :seatbelt_on? # # transition :from => :parked, :to => :idling, :on => ignite, :if => :moving? # transition :from => :parked, :to => :idling, :on => ignite, :unless => :stopped? # # == Order of operations # # Transitions are evaluated in the order in which they're defined. As a # result, if more than one transition applies to a given object, then the # first transition that matches will be performed. # Creates a callback that will be invoked *before* a transition is # performed so long as the given requirements match the transition. # # == The callback # # Callbacks must be defined as either an argument, in the :do option, or # as a block. For example, # # class Vehicle # state_machine do # before_transition :set_alarm # before_transition :set_alarm, all => :parked # before_transition all => :parked, :do => :set_alarm # before_transition all => :parked do |vehicle, transition| # vehicle.set_alarm # end # ... # end # end # # Notice that the first three callbacks are the same in terms of how the # methods to invoke are defined. However, using the :do can # provide for a more fluid DSL. # # In addition, multiple callbacks can be defined like so: # # class Vehicle # state_machine do # before_transition :set_alarm, :lock_doors, all => :parked # before_transition all => :parked, :do => [:set_alarm, :lock_doors] # before_transition :set_alarm do |vehicle, transition| # vehicle.lock_doors # end # end # end # # Notice that the different ways of configuring methods can be mixed. # # == State requirements # # Callbacks can require that the machine be transitioning from and to # specific states. These requirements use a Hash syntax to map beginning # states to ending states. For example, # # before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm # # In this case, the +set_alarm+ callback will only be called if the machine # is transitioning from +parked+ to +idling+ or from +idling+ to +parked+. # # To help define state requirements, a set of helpers are available for # slightly more complex matching: # * all - Matches every state/event in the machine # * all - [:parked, :idling, ...] - Matches every state/event except those specified # * any - An alias for +all+ (matches every state/event in the machine) # * same - Matches the same state being transitioned from # # See StateMachines::MatcherHelpers for more information. # # Examples: # # before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear # before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling # before_transition all => :parked, :do => ... # Matches all states to parked # before_transition any => same, :do => ... # Matches every loopback # # == Event requirements # # In addition to state requirements, an event requirement can be defined so # that the callback is only invoked on specific events using the +on+ # option. This can also use the same matcher helpers as the state # requirements. # # Examples: # # before_transition :on => :ignite, :do => ... # Matches only on ignite # before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite # before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite # # == Verbose Requirements # # Requirements can also be defined using verbose options rather than the # implicit Hash syntax and helper methods described above. # # Configuration options: # * :from - One or more states being transitioned from. If none # are specified, then all states will match. # * :to - One or more states being transitioned to. If none are # specified, then all states will match. # * :on - One or more events that fired the transition. If none # are specified, then all events will match. # * :except_from - One or more states *not* being transitioned from # * :except_to - One more states *not* being transitioned to # * :except_on - One or more events that *did not* fire the transition # # Examples: # # before_transition :from => :ignite, :to => :idling, :on => :park, :do => ... # before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ... # # == Conditions # # In addition to the state/event requirements, a condition can also be # defined to help determine whether the callback should be invoked. # # Configuration options: # * :if - A method, proc or string to call to determine if the # callback should occur (e.g. :if => :allow_callbacks, or # :if => lambda {|user| user.signup_step > 2}). The method, proc or string # should return or evaluate to a true or false value. # * :unless - A method, proc or string to call to determine if the # callback should not occur (e.g. :unless => :skip_callbacks, or # :unless => lambda {|user| user.signup_step <= 2}). The method, proc or # string should return or evaluate to a true or false value. # # Examples: # # before_transition :parked => :idling, :if => :moving?, :do => ... # before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ... # # == Accessing the transition # # In addition to passing the object being transitioned, the actual # transition describing the context (e.g. event, from, to) can be accessed # as well. This additional argument is only passed if the callback allows # for it. # # For example, # # class Vehicle # # Only specifies one parameter (the object being transitioned) # before_transition all => :parked do |vehicle| # vehicle.set_alarm # end # # # Specifies 2 parameters (object being transitioned and actual transition) # before_transition all => :parked do |vehicle, transition| # vehicle.set_alarm(transition) # end # end # # *Note* that the object in the callback will only be passed in as an # argument if callbacks are configured to *not* be bound to the object # involved. This is the default and may change on a per-integration basis. # # See StateMachines::Transition for more information about the # attributes available on the transition. # # == Usage with delegates # # As noted above, state_machine uses the callback method's argument list # arity to determine whether to include the transition in the method call. # If you're using delegates, such as those defined in ActiveSupport or # Forwardable, the actual arity of the delegated method gets masked. This # means that callbacks which reference delegates will always get passed the # transition as an argument. For example: # # class Vehicle # extend Forwardable # delegate :refresh => :dashboard # # state_machine do # before_transition :refresh # ... # end # # def dashboard # @dashboard ||= Dashboard.new # end # end # # class Dashboard # def refresh(transition) # # ... # end # end # # In the above example, Dashboard#refresh *must* defined a # +transition+ argument. Otherwise, an +ArgumentError+ exception will get # raised. The only way around this is to avoid the use of delegates and # manually define the delegate method so that the correct arity is used. # # == Examples # # Below is an example of a class with one state machine and various types # of +before+ transitions defined for it: # # class Vehicle # state_machine do # # Before all transitions # before_transition :update_dashboard # # # Before specific transition: # before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt # # # With conditional callback: # before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on? # # # Using helpers: # before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard # ... # end # end # # As can be seen, any number of transitions can be created using various # combinations of configuration options. def before_transition(*args, **options, &) # Extract legacy positional arguments and merge with keyword options parsed_options = parse_callback_arguments(args, options) # Only validate callback-specific options, not state transition requirements callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator) StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator) add_callback(:before, parsed_options, &) end # Creates a callback that will be invoked *after* a transition is # performed so long as the given requirements match the transition. # # See +before_transition+ for a description of the possible configurations # for defining callbacks. def after_transition(*args, **options, &) # Extract legacy positional arguments and merge with keyword options parsed_options = parse_callback_arguments(args, options) # Only validate callback-specific options, not state transition requirements callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator) StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator) add_callback(:after, parsed_options, &) end # Creates a callback that will be invoked *around* a transition so long as # the given requirements match the transition. # # == The callback # # Around callbacks wrap transitions, executing code both before and after. # These callbacks are defined in the exact same manner as before / after # callbacks with the exception that the transition must be yielded to in # order to finish running it. # # If defining +around+ callbacks using blocks, you must yield within the # transition by directly calling the block (since yielding is not allowed # within blocks). # # For example, # # class Vehicle # state_machine do # around_transition do |block| # Benchmark.measure { block.call } # end # # around_transition do |vehicle, block| # logger.info "vehicle was #{state}..." # block.call # logger.info "...and is now #{state}" # end # # around_transition do |vehicle, transition, block| # logger.info "before #{transition.event}: #{vehicle.state}" # block.call # logger.info "after #{transition.event}: #{vehicle.state}" # end # end # end # # Notice that referencing the block is similar to doing so within an # actual method definition in that it is always the last argument. # # On the other hand, if you're defining +around+ callbacks using method # references, you can yield like normal: # # class Vehicle # state_machine do # around_transition :benchmark # ... # end # # def benchmark # Benchmark.measure { yield } # end # end # # See +before_transition+ for a description of the possible configurations # for defining callbacks. def around_transition(*args, **options, &) # Extract legacy positional arguments and merge with keyword options parsed_options = parse_callback_arguments(args, options) # Only validate callback-specific options, not state transition requirements callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator) StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator) add_callback(:around, parsed_options, &) end # Creates a callback that will be invoked *after* a transition failures to # be performed so long as the given requirements match the transition. # # See +before_transition+ for a description of the possible configurations # for defining callbacks. *Note* however that you cannot define the state # requirements in these callbacks. You may only define event requirements. # # = The callback # # Failure callbacks get invoked whenever an event fails to execute. This # can happen when no transition is available, a +before+ callback halts # execution, or the action associated with this machine fails to succeed. # In any of these cases, any failure callback that matches the attempted # transition will be run. # # For example, # # class Vehicle # state_machine do # after_failure do |vehicle, transition| # logger.error "vehicle #{vehicle} failed to transition on #{transition.event}" # end # # after_failure :on => :ignite, :do => :log_ignition_failure # # ... # end # end def after_failure(*args, **options, &) # Extract legacy positional arguments and merge with keyword options parsed_options = parse_callback_arguments(args, options) StateMachines::OptionsValidator.assert_valid_keys!(parsed_options, :on, :do, :if, :unless) add_callback(:failure, parsed_options, &) end # Generates a list of the possible transition sequences that can be run on # the given object. These paths can reveal all of the possible states and # events that can be encountered in the object's state machine based on the # object's current state. # # Configuration options: # * +from+ - The initial state to start all paths from. By default, this # is the object's current state. # * +to+ - The target state to end all paths on. By default, paths will # end when they loop back to the first transition on the path. # * +deep+ - Whether to allow the target state to be crossed more than once # in a path. By default, paths will immediately stop when the target # state (if specified) is reached. If this is enabled, then paths can # continue even after reaching the target state; they will stop when # reaching the target state a second time. # # *Note* that the object is never modified when the list of paths is # generated. # # == Examples # # class Vehicle # state_machine :initial => :parked do # event :ignite do # transition :parked => :idling # end # # event :shift_up do # transition :idling => :first_gear, :first_gear => :second_gear # end # # event :shift_down do # transition :second_gear => :first_gear, :first_gear => :idling # end # end # end # # vehicle = Vehicle.new # => # # vehicle.state # => "parked" # # vehicle.state_paths # # => [ # # [#, # # #, # # #, # # #, # # #], # # # # [#, # # #, # # #] # # ] # # vehicle.state_paths(:from => :parked, :to => :second_gear) # # => [ # # [#, # # #, # # #] # # ] # # In addition to getting the possible paths that can be accessed, you can # also get summary information about the states / events that can be # accessed at some point along one of the paths. For example: # # # Get the list of states that can be accessed from the current state # vehicle.state_paths.to_states # => [:idling, :first_gear, :second_gear] # # # Get the list of events that can be accessed from the current state # vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down] # Marks the given object as invalid with the given message. # # By default, this is a no-op. def invalidate(_object, _attribute, _message, _values = []); end # Gets a description of the errors for the given object. This is used to # provide more detailed information when an InvalidTransition exception is # raised. def errors_for(_object) '' end # Resets any errors previously added when invalidating the given object. # # By default, this is a no-op. def reset(_object); end # Generates the message to use when invalidating the given object after # failing to transition on a specific event def generate_message(name, values = []) message = @messages[name] || self.class.default_messages[name] # Check whether there are actually any values to interpolate to avoid # any warnings if message.scan(/%./).any? { |match| match != '%%' } message % values.map(&:last) else message end end # Runs a transaction, rolling back any changes if the yielded block fails. # # This is only applicable to integrations that involve databases. By # default, this will not run any transactions since the changes aren't # taking place within the context of a database. def within_transaction(object, &) if use_transactions transaction(object, &) else yield end end def renderer self.class.renderer end def draw(**) renderer.draw_machine(self, **) end # Determines whether an action hook was defined for firing attribute-based # event transitions when the configured action gets called. def action_hook?(self_only = false) @action_hook_defined || (!self_only && owner_class.state_machines.any? { |_name, machine| machine.action == action && machine != self && machine.action_hook?(true) }) end protected # Runs additional initialization hooks. By default, this is a no-op. def after_initialize; end # Determines whether there's already a helper method defined within the # given scope. This is true only if one of the owner's ancestors defines # the method and is further along in the ancestor chain than this # machine's helper module. # Always yields def transaction(_object) yield end # Gets the initial attribute value defined by the owner class (outside of # the machine's definition). By default, this is always nil. def owner_class_attribute_default nil end # Checks whether the given state matches the attribute default specified # by the owner class def owner_class_attribute_default_matches?(state) state.matches?(owner_class_attribute_default) end end end state_machines-0.100.4/lib/state_machines/machine/000077500000000000000000000000001507333401300220035ustar00rootroot00000000000000state_machines-0.100.4/lib/state_machines/machine/action_hooks.rb000066400000000000000000000036411507333401300250140ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines class Machine module ActionHooks protected # Determines whether action helpers should be defined for this machine. # This is only true if there is an action configured and no other machines # have process this same configuration already. def define_action_helpers? action && owner_class.state_machines.none? { |_name, machine| machine.action == action && machine != self } end # Adds helper methods for automatically firing events when an action # is invoked def define_action_helpers return unless action_hook @action_hook_defined = true define_action_hook end # Hooks directly into actions by defining the same method in an included # module. As a result, when the action gets invoked, any state events # defined for the object will get run. Method visibility is preserved. def define_action_hook action_hook = self.action_hook action = self.action private_action_hook = owner_class.private_method_defined?(action_hook) # Only define helper if it hasn't define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1 def #{action_hook}(*) self.class.state_machines.transitions(self, #{action.inspect}).perform { super } end private #{action_hook.inspect} if #{private_action_hook} END_EVAL end # The method to hook into for triggering transitions when invoked. By # default, this is the action configured for the machine. # # Since the default hook technique relies on module inheritance, the # action must be defined in an ancestor of the owner classs in order for # it to be the action hook. def action_hook action && owner_class_ancestor_has_method?(:instance, action) ? action : nil end end end end state_machines-0.100.4/lib/state_machines/machine/async_extensions.rb000066400000000000000000000057571507333401300257420ustar00rootroot00000000000000# frozen_string_literal: true # This file provides optional async extensions for the Machine class. # It should only be loaded when async functionality is explicitly requested. module StateMachines class Machine # AsyncMode extensions for the Machine class # Provides async-aware methods while maintaining backward compatibility module AsyncExtensions # Instance methods added to Machine for async support # Configure this specific machine instance for async mode # # Example: # class Vehicle # state_machine initial: :parked do # configure_async_mode! # Enable async for this machine # # event :ignite do # transition parked: :idling # end # end # end def configure_async_mode!(enabled = true) if enabled begin require 'state_machines/async_mode' @async_mode_enabled = true owner_class.include(StateMachines::AsyncMode::ThreadSafeState) owner_class.include(StateMachines::AsyncMode::AsyncEvents) extend(StateMachines::AsyncMode::AsyncMachine) # Extend events to generate async versions events.each do |event| event.extend(StateMachines::AsyncMode::AsyncEventExtensions) end rescue LoadError => e # Fallback to sync mode with warning (only once per class) unless owner_class.instance_variable_get(:@async_fallback_warned) warn <<~WARNING ⚠️ #{owner_class.name}: Async mode requested but not available on #{RUBY_ENGINE}. #{e.message} ⚠️ Falling back to synchronous mode. Results may be unpredictable due to engine limitations. For production async support, use MRI Ruby (CRuby) 3.2+ WARNING owner_class.instance_variable_set(:@async_fallback_warned, true) end @async_mode_enabled = false end else @async_mode_enabled = false end self end # Check if this specific machine instance has async mode enabled def async_mode_enabled? @async_mode_enabled || false end # Thread-safe version of state reading def read_safely(object, attribute, ivar = false) object.read_state_safely(self, attribute, ivar) end # Thread-safe version of state writing def write_safely(object, attribute, value, ivar = false) object.write_state_safely(self, attribute, value, ivar) end # Thread-safe callback execution for async operations def run_callbacks_safely(type, object, context, transition) object.state_machine_mutex.with_write_lock do callbacks[type].each { |callback| callback.call(object, context, transition) } end end end # Include async extensions by default (but only load AsyncMode when requested) include AsyncExtensions end end state_machines-0.100.4/lib/state_machines/machine/callbacks.rb000066400000000000000000000054731507333401300242600ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines class Machine module Callbacks # Creates a callback that will be invoked *before* a transition is # performed so long as the given requirements match the transition. def before_transition(*args, **options, &) # Extract legacy positional arguments and merge with keyword options parsed_options = parse_callback_arguments(args, options) # Only validate callback-specific options, not state transition requirements callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator) StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator) add_callback(:before, parsed_options, &) end # Creates a callback that will be invoked *after* a transition is # performed so long as the given requirements match the transition. def after_transition(*args, **options, &) # Extract legacy positional arguments and merge with keyword options parsed_options = parse_callback_arguments(args, options) # Only validate callback-specific options, not state transition requirements callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator) StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator) add_callback(:after, parsed_options, &) end # Creates a callback that will be invoked *around* a transition so long # as the given requirements match the transition. def around_transition(*args, **options, &) # Extract legacy positional arguments and merge with keyword options parsed_options = parse_callback_arguments(args, options) # Only validate callback-specific options, not state transition requirements callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator) StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator) add_callback(:around, parsed_options, &) end # Creates a callback that will be invoked after a transition has failed # to be performed. def after_failure(*args, **options, &) # Extract legacy positional arguments and merge with keyword options parsed_options = parse_callback_arguments(args, options) # Only validate callback-specific options, not state transition requirements callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator) StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator) add_callback(:failure, parsed_options, &) end end end end state_machines-0.100.4/lib/state_machines/machine/class_methods.rb000066400000000000000000000064531507333401300251700ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines class Machine module ClassMethods # Attempts to find or create a state machine for the given class. For # example, # # StateMachines::Machine.find_or_create(Vehicle) # StateMachines::Machine.find_or_create(Vehicle, :initial => :parked) # StateMachines::Machine.find_or_create(Vehicle, :status) # StateMachines::Machine.find_or_create(Vehicle, :status, :initial => :parked) # # If a machine of the given name already exists in one of the class's # superclasses, then a copy of that machine will be created and stored # in the new owner class (the original will remain unchanged). def find_or_create(owner_class, *args, &) options = args.last.is_a?(Hash) ? args.pop : {} name = args.first || :state # Find an existing machine machine = (owner_class.respond_to?(:state_machines) && ((args.first && owner_class.state_machines[name]) || (!args.first && owner_class.state_machines.values.first))) || nil if machine # Only create a new copy if changes are being made to the machine in # a subclass if machine.owner_class != owner_class && (options.any? || block_given?) machine = machine.clone machine.initial_state = options[:initial] if options.include?(:initial) machine.owner_class = owner_class # Configure async mode if requested in options machine.configure_async_mode!(options[:async]) if options.include?(:async) end # Evaluate DSL machine.instance_eval(&) if block_given? else # No existing machine: create a new one machine = new(owner_class, name, options, &) end machine end def draw(*) raise NotImplementedError end # Default messages to use for validation errors in ORM integrations # Thread-safe access via atomic operations on simple values attr_accessor :ignore_method_conflicts def default_messages @default_messages ||= { invalid: 'is invalid', invalid_event: 'cannot transition when %s', invalid_transition: 'cannot transition via "%1$s"' }.freeze end def default_messages=(messages) # Atomic replacement with frozen object @default_messages = deep_freeze_hash(messages) end def replace_messages(message_hash) # Atomic replacement: read current messages, merge with new ones, replace atomically current_messages = @default_messages || {} merged_messages = current_messages.merge(message_hash) @default_messages = deep_freeze_hash(merged_messages) end attr_writer :renderer def renderer return @renderer if @renderer STDIORenderer end private # Deep freezes a hash and all its string values for thread safety def deep_freeze_hash(hash) hash.each_with_object({}) do |(key, value), frozen_hash| frozen_key = key.respond_to?(:freeze) ? key.freeze : key frozen_value = value.respond_to?(:freeze) ? value.freeze : value frozen_hash[frozen_key] = frozen_value end.freeze end end end end state_machines-0.100.4/lib/state_machines/machine/configuration.rb000066400000000000000000000127061507333401300252050ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines class Machine module Configuration # Initializes a new state machine with the given configuration. def initialize(owner_class, *args, &) options = args.last.is_a?(Hash) ? args.pop : {} # Find an integration that matches this machine's owner class @integration = if options.include?(:integration) options[:integration] && StateMachines::Integrations.find_by_name(options[:integration]) else StateMachines::Integrations.match(owner_class) end # Validate options including integration-specific options valid_keys = [:attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions, :async] valid_keys += @integration.integration_options if @integration StateMachines::OptionsValidator.assert_valid_keys!(options, valid_keys) if @integration extend @integration options = (@integration.defaults || {}).merge(options) end # Add machine-wide defaults options = { use_transactions: true, initialize: true }.merge(options) # Set machine configuration @name = args.first || :state @attribute = options[:attribute] || @name @events = EventCollection.new(self) @states = StateCollection.new(self) @callbacks = { before: [], after: [], failure: [] } @namespace = options[:namespace] @messages = options[:messages] || {} @action = options[:action] @use_transactions = options[:use_transactions] @initialize_state = options[:initialize] @action_hook_defined = false @async_requested = options[:async] self.owner_class = owner_class # Merge with sibling machine configurations add_sibling_machine_configs # Define class integration define_helpers define_scopes(options[:plural]) after_initialize # Evaluate DSL instance_eval(&) if block_given? # Configure async mode if requested, after owner_class is set and DSL is evaluated configure_async_mode!(true) if @async_requested self.initial_state = options[:initial] unless sibling_machines.any? end # Creates a copy of this machine in addition to copies of each associated # event/states/callback, so that the modifications to those collections do # not affect the original machine. def initialize_copy(orig) # :nodoc: super @events = @events.dup @events.machine = self @states = @states.dup @states.machine = self @callbacks = { before: @callbacks[:before].dup, after: @callbacks[:after].dup, failure: @callbacks[:failure].dup } @async_requested = orig.instance_variable_get(:@async_requested) @async_mode_enabled = orig.instance_variable_get(:@async_mode_enabled) end # Sets the class which is the owner of this state machine. Any methods # generated by states, events, or other parts of the machine will be defined # on the given owner class. def owner_class=(klass) @owner_class = klass # Create modules for extending the class with state/event-specific methods @helper_modules = helper_modules = { instance: HelperModule.new(self, :instance), class: HelperModule.new(self, :class) } owner_class.class_eval do extend helper_modules[:class] include helper_modules[:instance] end # Add class-/instance-level methods to the owner class for state initialization unless owner_class < StateMachines::InstanceMethods owner_class.class_eval do extend StateMachines::ClassMethods include StateMachines::InstanceMethods end define_state_initializer if @initialize_state end # Record this machine as matched to the name in the current owner class. # This will override any machines mapped to the same name in any superclasses. owner_class.state_machines[name] = self end # Sets the initial state of the machine. This can be either the static name # of a state or a lambda block which determines the initial state at # creation time. def initial_state=(new_initial_state) @initial_state = new_initial_state add_states([@initial_state]) unless dynamic_initial_state? # Update all states to reflect the new initial state states.each { |state| state.initial = (state.name == @initial_state) } # Output a warning if there are conflicting initial states for the machine's # attribute initial_state = states.detect(&:initial) has_owner_default = !owner_class_attribute_default.nil? has_conflicting_default = dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state) return unless has_owner_default && has_conflicting_default warn( "Both #{owner_class.name} and its #{name.inspect} machine have defined " \ "a different default for \"#{attribute}\". Use only one or the other for " \ 'defining defaults to avoid unexpected behaviors.' ) end # Gets the attribute name for the given machine scope. def attribute(name = :state) name == :state ? @attribute : :"#{self.name}_#{name}" end end end end state_machines-0.100.4/lib/state_machines/machine/event_methods.rb000066400000000000000000000040771507333401300252040ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines class Machine module EventMethods # Defines one or more events for the machine and the transitions that can # be performed when those events are run. def event(*names, &) options = names.last.is_a?(Hash) ? names.pop : {} StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name) # Store the context so that it can be used for / matched against any event # that gets added @events.context(names, &) if block_given? if names.first.is_a?(Matcher) # Add any events referenced in the matcher. When matchers are used, # events are not allowed to be configured. raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any? events = add_events(names.first.values) else events = add_events(names) # Update the configuration for the event(s) events.each do |event| event.human_name = options[:human_name] if options.include?(:human_name) # Add any states that may have been referenced within the event add_states(event.known_states) end end events.length == 1 ? events.first : events end alias on event # Creates a new transition that determines what to change the current state # to when an event fires. def transition(options) raise ArgumentError, 'Must specify :on event' unless options[:on] branches = [] options = options.dup event(*Array(options.delete(:on))) { branches << transition(options) } branches.length == 1 ? branches.first : branches end # Gets the list of all possible transition paths from the current state to # the given target state. If multiple target states are provided, then # this will return all possible paths to those states. def paths_for(object, requirements = {}) PathCollection.new(object, self, requirements) end end end end state_machines-0.100.4/lib/state_machines/machine/helper_generators.rb000066400000000000000000000116501507333401300260430ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines class Machine module HelperGenerators protected # Adds helper methods for interacting with the state machine, including # for states, events, and transitions def define_helpers define_state_accessor define_state_predicate define_event_helpers define_path_helpers define_action_helpers if define_action_helpers? define_name_helpers end # Defines the initial values for state machine attributes. Static values # are set prior to the original initialize method and dynamic values are # set *after* the initialize method in case it is dependent on it. def define_state_initializer define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1 def initialize(*) self.class.state_machines.initialize_states(self) { super } end END_EVAL end # Adds reader/writer methods for accessing the state attribute def define_state_accessor attribute = self.attribute @helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute) @helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=") end # Adds predicate method to the owner class for determining the name of the # current state def define_state_predicate call_super = owner_class_ancestor_has_method?(:instance, "#{name}?") ? true : false define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1 def #{name}?(*args) args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args) end END_EVAL end # Adds helper methods for getting information about this state machine's # events def define_event_helpers # Gets the events that are allowed to fire on the current object define_helper(:instance, attribute(:events)) do |machine, object, *args| machine.events.valid_for(object, *args).map(&:name) end # Gets the next possible transitions that can be run on the current # object define_helper(:instance, attribute(:transitions)) do |machine, object, *args| machine.events.transitions_for(object, *args) end # Fire an arbitrary event for this machine define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args| machine.events.fetch(event).fire(object, *args) end # Add helpers for tracking the event / transition to invoke when the # action is called return unless action event_attribute = attribute(:event) define_helper(:instance, event_attribute) do |machine, object| # Interpret non-blank events as present event = machine.read(object, :event, true) event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil end # A roundabout way of writing the attribute is used here so that # integrations can hook into this modification define_helper(:instance, "#{event_attribute}=") do |machine, object, value| machine.write(object, :event, value, true) end event_transition_attribute = attribute(:event_transition) define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1 protected; attr_accessor #{event_transition_attribute.inspect} END_EVAL end # Adds helper methods for getting information about this state machine's # available transition paths def define_path_helpers # Gets the paths of transitions available to the current object define_helper(:instance, attribute(:paths)) do |machine, object, *args| machine.paths_for(object, *args) end end # Adds helper methods for accessing naming information about states and # events on the owner class def define_name_helpers # Gets the humanized version of a state define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state| machine.states.fetch(state).human_name(klass) end # Gets the humanized version of an event define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event| machine.events.fetch(event).human_name(klass) end # Gets the state name for the current value define_helper(:instance, attribute(:name)) do |machine, object| machine.states.match!(object).name end # Gets the human state name for the current value define_helper(:instance, "human_#{attribute(:name)}") do |machine, object| machine.states.match!(object).human_name(object.class) end end end end end state_machines-0.100.4/lib/state_machines/machine/integration.rb000066400000000000000000000040721507333401300246560ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines class Machine module Integration # Marks the given object as invalid with the given message. # # By default, this is a no-op. def invalidate(_object, _attribute, _message, _values = []); end # Gets a description of the errors for the given object. This is used to # provide more detailed information when an InvalidTransition exception is # raised. def errors_for(_object) '' end # Resets any errors previously added when invalidating the given object. # # By default, this is a no-op. def reset(_object); end # Generates a user-friendly name for the given message. def generate_message(name, values = []) format(@messages[name] || @messages[:invalid_transition] || default_messages[name] || default_messages[:invalid_transition], state: values.first) end # Runs a transaction, yielding the given block. # # By default, this is a no-op. def within_transaction(object, &) if use_transactions && respond_to?(:transaction, true) transaction(object, &) else yield end end protected # Runs additional initialization hooks. By default, this is a no-op. def after_initialize; end # Always yields def transaction(_object) yield end # Gets the initial attribute value defined by the owner class (outside of # the machine's definition). By default, this is always nil. def owner_class_attribute_default nil end # Checks whether the given state matches the attribute default specified # by the owner class def owner_class_attribute_default_matches?(state) state.matches?(owner_class_attribute_default) end private # Gets the default messages that can be used in the machine for invalid # transitions. def default_messages { invalid_transition: '%s cannot transition via "%s"' } end end end end state_machines-0.100.4/lib/state_machines/machine/parsing.rb000066400000000000000000000056451507333401300240050ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines class Machine module Parsing private # Parses callback arguments for backward compatibility with both positional # and keyword argument styles. Supports Ruby 3.2+ keyword arguments while # maintaining full backward compatibility with the legacy API. def parse_callback_arguments(args, options) # Handle legacy positional args: before_transition(:method1, :method2, from: :state) if args.any? # Extract hash options from the end of args if present parsed_options = args.last.is_a?(Hash) ? args.pop.dup : {} # Merge any additional keyword options parsed_options.merge!(options) if options.any? # Remaining args become the :do option (method names to call) parsed_options[:do] = args if args.any? parsed_options else # Pure keyword argument style: before_transition(from: :state, to: :other, do: :method) options.dup end end # Adds a new transition callback of the given type. def add_callback(type, options, &) callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &) add_states(callback.known_states) callback end # Tracks the given set of states in the list of all known states for # this machine def add_states(new_states) new_states.map do |new_state| # Check for other states that use a different class type for their name. # This typically prevents string / symbol misuse. if new_state && (conflict = states.detect { |state| state.name && state.name.class != new_state.class }) raise ArgumentError, "#{new_state.inspect} state defined as #{new_state.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all states must be consistent" end unless (state = states[new_state]) states << state = State.new(self, new_state) # Copy states over to sibling machines sibling_machines.each { |machine| machine.states << state } end state end end # Tracks the given set of events in the list of all known events for # this machine def add_events(new_events) new_events.map do |new_event| # Check for other states that use a different class type for their name. # This typically prevents string / symbol misuse. if (conflict = events.detect { |event| event.name.class != new_event.class }) raise ArgumentError, "#{new_event.inspect} event defined as #{new_event.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all events must be consistent" end unless (event = events[new_event]) events << event = Event.new(self, new_event) end event end end end end end state_machines-0.100.4/lib/state_machines/machine/rendering.rb000066400000000000000000000005501507333401300243050ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines class Machine module Rendering # Gets the renderer for this machine. def renderer @renderer ||= StdioRenderer.new end # Generates a visual representation of this machine for a given format. def draw(**) renderer.draw(self, **) end end end end state_machines-0.100.4/lib/state_machines/machine/scoping.rb000066400000000000000000000027031507333401300237740ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines class Machine module Scoping protected # Defines the with/without scope helpers for this attribute. Both the # singular and plural versions of the attribute are defined for each # scope helper. A custom plural can be specified if it cannot be # automatically determined by either calling +pluralize+ on the attribute # name or adding an "s" to the end of the name. def define_scopes(custom_plural = nil) plural = custom_plural || pluralize(name) %i[with without].each do |kind| [name, plural].map(&:to_s).uniq.each do |suffix| method = "#{kind}_#{suffix}" next unless (scope = send("create_#{kind}_scope", method)) # Converts state names to their corresponding values so that they # can be looked up properly define_helper(:class, method) do |machine, klass, *states| run_scope(scope, machine, klass, states) end end end end # Creates a scope for finding objects *with* a particular value or values # for the attribute. # # By default, this is a no-op. def create_with_scope(name); end # Creates a scope for finding objects *without* a particular value or # values for the attribute. # # By default, this is a no-op. def create_without_scope(name); end end end end state_machines-0.100.4/lib/state_machines/machine/state_methods.rb000066400000000000000000000073771507333401300252110ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines class Machine module StateMethods # Gets the initial state of the machine for the given object. If a dynamic # initial state was configured for this machine, then the object will be # passed into the lambda block to help determine the actual state. def initial_state(object) states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?(:@initial_state) end # Whether a dynamic initial state is being used in the machine def dynamic_initial_state? instance_variable_defined?(:@initial_state) && @initial_state.is_a?(Proc) end # Initializes the state on the given object. Initial values are only set if # the machine's attribute hasn't been previously initialized. # # Configuration options: # * :force - Whether to initialize the state regardless of its # current value # * :to - A hash to set the initial value in instead of writing # directly to the object def initialize_state(object, options = {}) state = initial_state(object) return unless state && (options[:force] || initialize_state?(object)) value = state.value if (hash = options[:to]) hash[attribute.to_s] = value else write(object, :state, value) end end # Customizes the definition of one or more states in the machine. def state(*names, &) options = names.last.is_a?(Hash) ? names.pop : {} StateMachines::OptionsValidator.assert_valid_keys!(options, :value, :cache, :if, :human_name) # Store the context so that it can be used for / matched against any state # that gets added @states.context(names, &) if block_given? if names.first.is_a?(Matcher) # Add any states referenced in the matcher. When matchers are used, # states are not allowed to be configured. raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any? states = add_states(names.first.values) else states = add_states(names) # Update the configuration for the state(s) states.each do |state| if options.include?(:value) state.value = options[:value] self.states.update(state) end state.human_name = options[:human_name] if options.include?(:human_name) state.cache = options[:cache] if options.include?(:cache) state.matcher = options[:if] if options.include?(:if) end end states.length == 1 ? states.first : states end alias other_states state # Gets the current value stored in the given object's attribute. def read(object, attribute, ivar = false) attribute = self.attribute(attribute) if ivar object.instance_variable_defined?(:"@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil else object.send(attribute) end end # Sets a new value in the given object's attribute. def write(object, attribute, value, ivar = false) attribute = self.attribute(attribute) ivar ? object.instance_variable_set(:"@#{attribute}", value) : object.send("#{attribute}=", value) end protected # Determines if the machine's attribute needs to be initialized. This # will only be true if the machine's attribute is blank. def initialize_state?(object) value = read(object, :state) (value.nil? || (value.respond_to?(:empty?) && value.empty?)) && !states[value, :value] end end end end state_machines-0.100.4/lib/state_machines/machine/utilities.rb000066400000000000000000000065731507333401300243560ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines class Machine module Utilities protected # Looks up other machines that have been defined in the owner class and # are targeting the same attribute as this machine. When accessing # sibling machines, they will be automatically copied for the current # class if they haven't been already. This ensures that any configuration # changes made to the sibling machines only affect this class and not any # base class that may have originally defined the machine. def sibling_machines owner_class.state_machines.each_with_object([]) do |(name, machine), machines| machines << (owner_class.state_machine(name) {}) if machine.attribute == attribute && machine != self end end # Looks up the ancestor class that has the given method defined. This # is used to find the method owner which is used to determine where to # define new methods. def owner_class_ancestor_has_method?(scope, method) return false unless owner_class_has_method?(scope, method) superclasses = owner_class.ancestors.select { |ancestor| ancestor.is_a?(Class) }[1..] if scope == :class current = owner_class.singleton_class superclass = superclasses.first else current = owner_class superclass = owner_class.superclass end # Generate the list of modules that *only* occur in the owner class, but # were included *prior* to the helper modules, in addition to the # superclasses ancestors = current.ancestors - superclass.ancestors + superclasses helper_module_index = ancestors.index(@helper_modules[scope]) ancestors = helper_module_index ? ancestors[helper_module_index..].reverse : ancestors.reverse # Search for for the first ancestor that defined this method ancestors.detect do |ancestor| ancestor = ancestor.singleton_class if scope == :class && ancestor.is_a?(Class) ancestor.method_defined?(method) || ancestor.private_method_defined?(method) end end # Determines whether the given method is defined in the owner class or # in a superclass. def owner_class_has_method?(scope, method) target = scope == :class ? owner_class.singleton_class : owner_class target.method_defined?(method) || target.private_method_defined?(method) end # Pluralizes the given word using #pluralize (if available) or simply # adding an "s" to the end of the word def pluralize(word) word = word.to_s if word.respond_to?(:pluralize) word.pluralize else "#{word}s" end end # Generates the results for the given scope based on one or more states to # filter by def run_scope(scope, machine, klass, states) values = states.flatten.compact.map { |state| machine.states.fetch(state).value } scope.call(klass, values) end # Adds sibling machine configurations to the current machine. This # will add states from other machines that have the same attribute. def add_sibling_machine_configs # Add existing states sibling_machines.each do |machine| machine.states.each { |state| states << state unless states[state.name] } end end end end end state_machines-0.100.4/lib/state_machines/machine/validation.rb000066400000000000000000000025221507333401300244630ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines class Machine module Validation # Frozen constant to avoid repeated array allocations DANGEROUS_PATTERNS = [ /`.*`/, # Backticks (shell execution) /system\s*\(/, # System calls /exec\s*\(/, # Exec calls /eval\s*\(/, # Nested eval /require\s+['"]/, # Require statements /load\s+['"]/, # Load statements /File\./, # File operations /IO\./, # IO operations /Dir\./, # Directory operations /Kernel\./ # Kernel operations ].freeze private # Validates string input before eval to prevent code injection # This is a basic safety check - not foolproof security def validate_eval_string(method_string) # Check for obviously dangerous patterns DANGEROUS_PATTERNS.each do |pattern| raise SecurityError, "Potentially dangerous code detected in eval string: #{method_string.inspect}" if method_string.match?(pattern) end # Basic syntax validation (cross-platform) begin SyntaxValidator.validate!(method_string, '(eval)') rescue SyntaxError => e raise ArgumentError, "Invalid Ruby syntax in eval string: #{e.message}" end end end end end state_machines-0.100.4/lib/state_machines/machine_collection.rb000066400000000000000000000100611507333401300245410ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'options_validator' module StateMachines # Represents a collection of state machines for a class class MachineCollection < Hash # Initializes the state of each machine in the given object. This can allow # states to be initialized in two groups: static and dynamic. For example: # # machines.initialize_states(object) do # # After static state initialization, before dynamic state initialization # end # # If no block is provided, then all states will still be initialized. # # Valid configuration options: # * :static - Whether to initialize static states. Unless set to # false, the state will be initialized regardless of its current value. # Default is true. # * :dynamic - Whether to initialize dynamic states. If set to # :force, the state will be initialized regardless of its current value. # Default is true. # * :to - A hash to write the initialized state to instead of # writing to the object. Default is to write directly to the object. def initialize_states(object, options = {}, attributes = {}) StateMachines::OptionsValidator.assert_valid_keys!(options, :static, :dynamic, :to) options = { static: true, dynamic: true }.merge(options) result = yield if block_given? if options[:static] each_value do |machine| unless machine.dynamic_initial_state? force = options[:static] == :force || !attributes.keys.map(&:to_sym).include?(machine.attribute) machine.initialize_state(object, force: force, to: options[:to]) end end end if options[:dynamic] each_value do |machine| machine.initialize_state(object, force: options[:dynamic] == :force, to: options[:to]) if machine.dynamic_initial_state? end end result end # Runs one or more events in parallel on the given object. See # StateMachines::InstanceMethods#fire_events for more information. def fire_events(object, *events) run_action = [true, false].include?(events.last) ? events.pop : true # Generate the transitions to run for each event transitions = events.collect do |event_name| # Find the actual event being run event = nil detect { |_name, machine| event = machine.events[event_name, :qualified_name] } raise(InvalidEvent.new(object, event_name)) unless event # Get the transition that will be performed for the event unless (transition = event.transition_for(object)) event.on_failure(object) end transition end.compact # Run the events in parallel only if valid transitions were found for # all of them if events.length == transitions.length TransitionCollection.new(transitions, { use_transactions: resolve_use_transactions, actions: run_action }).perform else false end end # Builds the collection of transitions for all event attributes defined on # the given object. This will only include events whose machine actions # match the one specified. # # These should only be fired as a result of the action being run. def transitions(object, action, options = {}) transitions = map do |_name, machine| machine.events.attribute_transition_for(object, true) if machine.action == action end AttributeTransitionCollection.new(transitions.compact, { use_transactions: resolve_use_transactions }.merge(options)) end protected def resolve_use_transactions use_transactions = nil each_value do |machine| # Determine use_transactions setting for this set of transitions. If from multiple state_machines, the settings must match. raise 'Encountered mismatched use_transactions configurations for multiple state_machines' if !use_transactions.nil? && use_transactions != machine.use_transactions use_transactions = machine.use_transactions end use_transactions end end end state_machines-0.100.4/lib/state_machines/macro_methods.rb000066400000000000000000000525761507333401300235670ustar00rootroot00000000000000# frozen_string_literal: true # A state machine is a model of behavior composed of states, events, and # transitions. This helper adds support for defining this type of # functionality on any Ruby class. module StateMachines module MacroMethods # Creates a new state machine with the given name. The default name, if not # specified, is :state. # # Configuration options: # * :attribute - The name of the attribute to store the state value # in. By default, this is the same as the name of the machine. # * :initial - The initial state of the attribute. This can be a # static state or a lambda block which will be evaluated at runtime # (e.g. lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling}). # Default is nil. # * :initialize - Whether to automatically initialize the attribute # by hooking into #initialize on the owner class. Default is true. # * :action - The instance method to invoke when an object # transitions. Default is nil unless otherwise specified by the # configured integration. # * :namespace - The name to use for namespacing all generated # state / event instance methods (e.g. "heater" would generate # :turn_on_heater and :turn_off_heater for the :turn_on/:turn_off events). # Default is nil. # * :integration - The name of the integration to use for adding # library-specific behavior to the machine. Built-in integrations # include :active_model, :active_record, :data_mapper, :mongo_mapper, and # :sequel. By default, this is determined automatically. # # Configuration options relevant to ORM integrations: # * :plural - The pluralized version of the name. By default, this # will attempt to call +pluralize+ on the name. If this method is not # available, an "s" is appended. This is used for generating scopes. # * :messages - The error messages to use when invalidating # objects due to failed transitions. Messages include: # * :invalid # * :invalid_event # * :invalid_transition # * :use_transactions - Whether transactions should be used when # firing events. Default is true unless otherwise specified by the # configured integration. # # This also expects a block which will be used to actually configure the # states, events and transitions for the state machine. *Note* that this # block will be executed within the context of the state machine. As a # result, you will not be able to access any class methods unless you refer # to them directly (i.e. specifying the class name). # # For examples on the types of state machine configurations and blocks, see # the section below. # # == Examples # # With the default name/attribute and no configuration: # # class Vehicle # state_machine do # event :park do # ... # end # end # end # # The above example will define a state machine named "state" that will # store the value in the +state+ attribute. Every vehicle will start # without an initial state. # # With a custom name / attribute: # # class Vehicle # state_machine :status, :attribute => :status_value do # ... # end # end # # With a static initial state: # # class Vehicle # state_machine :status, :initial => :parked do # ... # end # end # # With a dynamic initial state: # # class Vehicle # state_machine :status, :initial => lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling} do # ... # end # end # # == Class Methods # # The following class methods will be automatically generated by the # state machine based on the *name* of the machine. Any existing methods # will not be overwritten. # * human_state_name(state) - Gets the humanized value for the # given state. This may be generated by internationalization libraries if # supported by the integration. # * human_state_event_name(event) - Gets the humanized value for # the given event. This may be generated by internationalization # libraries if supported by the integration. # # For example, # # class Vehicle # state_machine :state, :initial => :parked do # event :ignite do # transition :parked => :idling # end # # event :shift_up do # transition :idling => :first_gear # end # end # end # # Vehicle.human_state_name(:parked) # => "parked" # Vehicle.human_state_name(:first_gear) # => "first gear" # Vehicle.human_state_event_name(:park) # => "park" # Vehicle.human_state_event_name(:shift_up) # => "shift up" # # == Instance Methods # # The following instance methods will be automatically generated by the # state machine based on the *name* of the machine. Any existing methods # will not be overwritten. # * state - Gets the current value for the attribute # * state=(value) - Sets the current value for the attribute # * state?(name) - Checks the given state name against the current # state. If the name is not a known state, then an ArgumentError is raised. # * state_name - Gets the name of the state for the current value # * human_state_name - Gets the human-readable name of the state # for the current value # * state_events(requirements = {}) - Gets the list of events that # can be fired on the current object's state (uses the *unqualified* event # names) # * state_transitions(requirements = {}) - Gets the list of # transitions that can be made on the current object's state # * state_paths(requirements = {}) - Gets the list of sequences of # transitions that can be run from the current object's state # * fire_state_event(name, *args) - Fires an arbitrary event with # the given argument list. This is essentially the same as calling the # actual event method itself. # # The state_events, state_transitions, and state_paths # helpers all take an optional set of requirements for determining what's # available for the current object. These requirements include: # * :from - One or more states to transition from. If none are # specified, then this will be the object's current state. # * :to - One or more states to transition to. If none are # specified, then this will match any to state. # * :on - One or more events to transition on. If none are # specified, then this will match any event. # * :guard - Whether to guard transitions with the if/unless # conditionals defined for each one. Default is true. # # For example, # # class Vehicle # state_machine :state, :initial => :parked do # event :ignite do # transition :parked => :idling # end # # event :park do # transition :idling => :parked # end # end # end # # vehicle = Vehicle.new # vehicle.state # => "parked" # vehicle.state_name # => :parked # vehicle.human_state_name # => "parked" # vehicle.state?(:parked) # => true # # # Changing state # vehicle.state = 'idling' # vehicle.state # => "idling" # vehicle.state_name # => :idling # vehicle.state?(:parked) # => false # # # Getting current event / transition availability # vehicle.state_events # => [:park] # vehicle.park # => true # vehicle.state_events # => [:ignite] # vehicle.state_events(:from => :idling) # => [:park] # vehicle.state_events(:to => :parked) # => [] # # vehicle.state_transitions # => [#] # vehicle.ignite # => true # vehicle.state_transitions # => [#] # # vehicle.state_transitions(:on => :ignite) # => [] # # # Getting current path availability # vehicle.state_paths # => [ # # [#, # # #] # # ] # vehicle.state_paths(:guard => false) # => # # [#, # # #] # # ] # # # Fire arbitrary events # vehicle.fire_state_event(:park) # => true # # == Attribute initialization # # For most classes, the initial values for state machine attributes are # automatically assigned when a new object is created. However, this # behavior will *not* work if the class defines an +initialize+ method # without properly calling +super+. # # For example, # # class Vehicle # state_machine :state, :initial => :parked do # ... # end # end # # vehicle = Vehicle.new # => # # vehicle.state # => "parked" # # In the above example, no +initialize+ method is defined. As a result, # the default behavior of initializing the state machine attributes is used. # # In the following example, a custom +initialize+ method is defined: # # class Vehicle # state_machine :state, :initial => :parked do # ... # end # # def initialize # end # end # # vehicle = Vehicle.new # => # # vehicle.state # => nil # # Since the +initialize+ method is defined, the state machine attributes # never get initialized. In order to ensure that all initialization hooks # are called, the custom method *must* call +super+ without any arguments # like so: # # class Vehicle # state_machine :state, :initial => :parked do # ... # end # # def initialize(attributes = {}) # ... # super() # end # end # # vehicle = Vehicle.new # => # # vehicle.state # => "parked" # # Because of the way the inclusion of modules works in Ruby, calling # super() will not only call the superclass's +initialize+, but # also +initialize+ on all included modules. This allows the original state # machine hook to get called properly. # # If you want to avoid calling the superclass's constructor, but still want # to initialize the state machine attributes: # # class Vehicle # state_machine :state, :initial => :parked do # ... # end # # def initialize(attributes = {}) # ... # initialize_state_machines # end # end # # vehicle = Vehicle.new # => # # vehicle.state # => "parked" # # You may also need to call the +initialize_state_machines+ helper manually # in cases where you want to change how static / dynamic initial states get # set. For example, the following example forces the initialization of # static states regardless of their current value: # # class Vehicle # state_machine :state, :initial => :parked do # state nil, :idling # ... # end # # def initialize(attributes = {}) # @state = 'idling' # initialize_state_machines(:static => :force) do # ... # end # end # end # # vehicle = Vehicle.new # => # # vehicle.state # => "parked" # # The above example is also noteworthy because it demonstrates how to avoid # initialization issues when +nil+ is a valid state. Without passing in # :static => :force, state_machine would never have initialized # the state because +nil+ (the default attribute value) would have been # interpreted as a valid current state. As a result, state_machine would # have simply skipped initialization. # # == States # # All of the valid states for the machine are automatically tracked based # on the events, transitions, and callbacks defined for the machine. If # there are additional states that are never referenced, these should be # explicitly added using the StateMachines::Machine#state or # StateMachines::Machine#other_states helpers. # # When a new state is defined, a predicate method for that state is # generated on the class. For example, # # class Vehicle # state_machine :initial => :parked do # event :ignite do # transition all => :idling # end # end # end # # ...will generate the following instance methods (assuming they're not # already defined in the class): # * parked? # * idling? # # Each predicate method will return true if it matches the object's # current state. Otherwise, it will return false. # # == Attribute access # # The actual value for a state is stored in the attribute configured for the # state machine. In most cases, this is the same as the name of the state # machine. For example: # # class Vehicle # attr_accessor :state # # state_machine :state, :initial => :parked do # ... # state :parked, :value => 0 # start :idling, :value => 1 # end # end # # vehicle = Vehicle.new # => # # vehicle.state # => 0 # vehicle.parked? # => true # vehicle.state = 1 # vehicle.idling? # => true # # The most important thing to note from the example above is what it means # to read from and write to the state machine's attribute. In particular, # state_machine treats the attribute (+state+ in this case) like a basic # attr_accessor that's been defined on the class. There are no special # behaviors added, such as allowing the attribute to be written to based on # the name of a state in the machine. This is the case for a few reasons: # * Setting the attribute directly is an edge case that is meant to only be # used when you want to skip state_machine altogether. This means that # state_machine shouldn't have any effect on the attribute accessor # methods. If you want to change the state, you should be using one of # the events defined in the state machine. # * Many ORMs provide custom behavior for the attribute reader / writer - it # may even be defined by your own framework / method implementation just # the example above showed. In order to avoid having to worry about the # different ways an attribute can get written, state_machine just makes # sure that the configured value for a state is always used when writing # to the attribute. # # If you were interested in accessing the name of a state (instead of its # actual value through the attribute), you could do the following: # # vehicle.state_name # => :idling # # == Events and Transitions # # Events defined on the machine are the interface to transitioning states # for an object. Events can be fired either directly (through the method # generated for the event) or indirectly (through attributes defined on # the machine). # # For example, # # class Vehicle # include DataMapper::Resource # property :id, Serial # # state_machine :initial => :parked do # event :ignite do # transition :parked => :idling # end # end # # state_machine :alarm_state, :initial => :active do # event :disable do # transition all => :off # end # end # end # # # Fire +ignite+ event directly # vehicle = Vehicle.create # => # # vehicle.ignite # => true # vehicle.state # => "idling" # vehicle.alarm_state # => "active" # # # Fire +disable+ event automatically # vehicle.alarm_state_event = 'disable' # vehicle.save # => true # vehicle.alarm_state # => "off" # # In the above example, the +state+ attribute is transitioned using the # +ignite+ action that's generated from the state machine. On the other # hand, the +alarm_state+ attribute is transitioned using the +alarm_state_event+ # attribute that automatically gets fired when the machine's action (+save+) # is invoked. # # For more information about how to configure an event and its associated # transitions, see StateMachines::Machine#event. # # == Defining callbacks # # Within the +state_machine+ block, you can also define callbacks for # transitions. For more information about defining these callbacks, # see StateMachines::Machine#before_transition, StateMachines::Machine#after_transition, # and StateMachines::Machine#around_transition, and StateMachines::Machine#after_failure. # # == Namespaces # # When a namespace is configured for a state machine, the name provided # will be used in generating the instance methods for interacting with # states/events in the machine. This is particularly useful when a class # has multiple state machines and it would be difficult to differentiate # between the various states / events. # # For example, # # class Vehicle # state_machine :heater_state, :initial => :off, :namespace => 'heater' do # event :turn_on do # transition all => :on # end # # event :turn_off do # transition all => :off # end # end # # state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do # event :turn_on do # transition all => :active # end # # event :turn_off do # transition all => :off # end # end # end # # The above class defines two state machines: +heater_state+ and +alarm_state+. # For the +heater_state+ machine, the following methods are generated since # it's namespaced by "heater": # * can_turn_on_heater? # * turn_on_heater # * ... # * can_turn_off_heater? # * turn_off_heater # * .. # * heater_off? # * heater_on? # # As shown, each method is unique to the state machine so that the states # and events don't conflict. The same goes for the +alarm_state+ machine: # * can_turn_on_alarm? # * turn_on_alarm # * ... # * can_turn_off_alarm? # * turn_off_alarm # * .. # * alarm_active? # * alarm_off? # # == Scopes # # For integrations that support it, a group of default scope filters will # be automatically created for assisting in finding objects that have the # attribute set to one of a given set of states. # # For example, # # Vehicle.with_state(:parked) # => All vehicles where the state is parked # Vehicle.with_states(:parked, :idling) # => All vehicles where the state is either parked or idling # # Vehicle.without_state(:parked) # => All vehicles where the state is *not* parked # Vehicle.without_states(:parked, :idling) # => All vehicles where the state is *not* parked or idling # # *Note* that if class methods already exist with those names (i.e. # :with_state, :with_states, :without_state, or :without_states), then a # scope will not be defined for that name. # # See StateMachines::Machine for more information about using integrations # and the individual integration docs for information about the actual # scopes that are generated. def state_machine(*, &) StateMachines::Machine.find_or_create(self, *, &) end end end state_machines-0.100.4/lib/state_machines/matcher.rb000066400000000000000000000071111507333401300223470ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines # Provides a general strategy pattern for determining whether a match is found # for a value. The algorithm that actually determines the match depends on # the matcher in use. class Matcher # The list of values against which queries are matched attr_reader :values # Creates a new matcher for querying against the given set of values def initialize(values = []) @values = values.is_a?(Array) ? values : [values] end # Generates a subset of values that exists in both the set of values being # filtered and the values configured for the matcher def filter(values) self.values & values end end # Matches any given value. Since there is no configuration for this type of # matcher, it must be used as a singleton. class AllMatcher < Matcher include Singleton # Generates a blacklist matcher based on the given set of values # # == Examples # # matcher = StateMachines::AllMatcher.instance - [:parked, :idling] # matcher.matches?(:parked) # => false # matcher.matches?(:first_gear) # => true def -(other) BlacklistMatcher.new(other) end alias except - # Always returns true def matches?(_value, _context = {}) true end # Always returns the given set of values def filter(values) values end # A human-readable description of this matcher. Always "all". def description 'all' end end # Matches a specific set of values class WhitelistMatcher < Matcher # Checks whether the given value exists within the whitelist configured # for this matcher. # # == Examples # # matcher = StateMachines::WhitelistMatcher.new([:parked, :idling]) # matcher.matches?(:parked) # => true # matcher.matches?(:first_gear) # => false def matches?(value, _context = {}) values.include?(value) end # A human-readable description of this matcher def description values.length == 1 ? values.first.inspect : values.inspect end end # Matches everything but a specific set of values class BlacklistMatcher < Matcher # Checks whether the given value exists outside the blacklist configured # for this matcher. # # == Examples # # matcher = StateMachines::BlacklistMatcher.new([:parked, :idling]) # matcher.matches?(:parked) # => false # matcher.matches?(:first_gear) # => true def matches?(value, _context = {}) !values.include?(value) end # Finds all values that are *not* within the blacklist configured for this # matcher def filter(values) values - self.values end # A human-readable description of this matcher def description "all - #{values.length == 1 ? values.first.inspect : values.inspect}" end end # Matches a loopback of two values within a context. Since there is no # configuration for this type of matcher, it must be used as a singleton. class LoopbackMatcher < Matcher include Singleton # Checks whether the given value matches what the value originally was. # This value should be defined in the context. # # == Examples # # matcher = StateMachines::LoopbackMatcher.instance # matcher.matches?(:parked, :from => :parked) # => true # matcher.matches?(:parked, :from => :idling) # => false def matches?(value, context) context[:from] == value end # A human-readable description of this matcher. Always "same". def description 'same' end end end state_machines-0.100.4/lib/state_machines/matcher_helpers.rb000066400000000000000000000027251507333401300240770ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines # Provides a set of helper methods for generating matchers module MatcherHelpers # Represents a state that matches all known states in a machine. # # == Examples # # class Vehicle # state_machine do # before_transition any => :parked, :do => lambda {...} # before_transition all - :parked => all - :idling, :do => lambda {} # # event :park # transition all => :parked # end # # event :crash # transition all - :parked => :stalled # end # end # end # # In the above example, +all+ will match the following states since they # are known: # * +parked+ # * +stalled+ # * +idling+ def all AllMatcher.instance end alias any all # Represents a state that matches the original +from+ state. This is useful # for defining transitions which are loopbacks. # # == Examples # # class Vehicle # state_machine do # event :ignite # transition [:idling, :first_gear] => same # end # end # end # # In the above example, +same+ will match whichever the from state is. In # the case of the +ignite+ event, it is essential the same as the following: # # transition :idling => :idling, :first_gear => :first_gear def same LoopbackMatcher.instance end end end state_machines-0.100.4/lib/state_machines/node_collection.rb000066400000000000000000000165411507333401300240730ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'options_validator' module StateMachines # Represents a collection of nodes in a state machine, be it events or states. # Nodes will not differentiate between the String and Symbol versions of the # values being indexed. class NodeCollection include Enumerable # The machine associated with the nodes attr_reader :machine # Creates a new collection of nodes for the given state machine. By default, # the collection is empty. # # Configuration options: # * :index - One or more attributes to automatically generate # hashed indices for in order to perform quick lookups. Default is to # index by the :name attribute def initialize(machine, options = {}) StateMachines::OptionsValidator.assert_valid_keys!(options, :index) options = { index: :name }.merge(options) @machine = machine @nodes = [] @index_names = Array(options[:index]) @indices = @index_names.each_with_object({}) do |name, indices| indices[name] = {} indices[:"#{name}_to_s"] = {} indices[:"#{name}_to_sym"] = {} end @default_index = Array(options[:index]).first @contexts = [] end # Creates a copy of this collection such that modifications don't affect # the original collection def initialize_copy(orig) # :nodoc: super nodes = @nodes contexts = @contexts @nodes = [] @contexts = [] @indices = @indices.each_with_object({}) do |(name, *), indices| indices[name] = {} end # Add nodes *prior* to copying over the contexts so that they don't get # evaluated multiple times concat(nodes.map { |n| n.dup }) @contexts = contexts.dup end # Changes the current machine associated with the collection. In turn, this # will change the state machine associated with each node in the collection. def machine=(new_machine) @machine = new_machine each { |node| node.machine = new_machine } end # Gets the number of nodes in this collection def length @nodes.length end # Gets the set of unique keys for the given index def keys(index_name = @default_index) index(index_name).keys end # Tracks a context that should be evaluated for any nodes that get added # which match the given set of nodes. Matchers can be used so that the # context can get added once and evaluated after multiple adds. def context(nodes, &block) nodes = nodes.first.is_a?(Matcher) ? nodes.first : WhitelistMatcher.new(nodes) @contexts << context = { nodes: nodes, block: block } # Evaluate the new context for existing nodes each { |node| eval_context(context, node) } context end # Adds a new node to the collection. By doing so, this will also add it to # the configured indices. This will also evaluate any existings contexts # that match the new node. def <<(node) @nodes << node @index_names.each { |name| add_to_index(name, value(node, name), node) } @contexts.each { |context| eval_context(context, node) } self end # Appends a group of nodes to the collection def concat(nodes) nodes.each { |node| self << node } end # Updates the indexed keys for the given node. If the node's attribute # has changed since it was added to the collection, the old indexed keys # will be replaced with the updated ones. def update(node) @index_names.each { |name| update_index(name, node) } end # Calls the block once for each element in self, passing that element as a # parameter. # # states = StateMachines::NodeCollection.new # states << StateMachines::State.new(machine, :parked) # states << StateMachines::State.new(machine, :idling) # states.each {|state| puts state.name, ' -- '} # # ...produces: # # parked -- idling -- def each(&) @nodes.each(&) self end # Gets the node at the given index. # # states = StateMachines::NodeCollection.new # states << StateMachines::State.new(machine, :parked) # states << StateMachines::State.new(machine, :idling) # # states.at(0).name # => :parked # states.at(1).name # => :idling def at(index) @nodes[index] end # Gets the node indexed by the given key. By default, this will look up the # key in the first index configured for the collection. A custom index can # be specified like so: # # collection['parked', :value] # # The above will look up the "parked" key in a hash indexed by each node's # +value+ attribute. # # If the key cannot be found, then nil will be returned. def [](key, index_name = @default_index) index(index_name)[key] || index(:"#{index_name}_to_s")[key.to_s] || (to_sym?(key) && index(:"#{index_name}_to_sym")[:"#{key}"]) || nil end # Gets the node indexed by the given key. By default, this will look up the # key in the first index configured for the collection. A custom index can # be specified like so: # # collection['parked', :value] # # The above will look up the "parked" key in a hash indexed by each node's # +value+ attribute. # # If the key cannot be found, then an IndexError exception will be raised: # # collection['invalid', :value] # => IndexError: "invalid" is an invalid value def fetch(key, index_name = @default_index) self[key, index_name] || raise(IndexError, "#{key.inspect} is an invalid #{index_name}") end protected # Gets the given index. If the index does not exist, then an ArgumentError # is raised. def index(name) raise ArgumentError, 'No indices configured' unless @indices.any? @indices[name] || raise(ArgumentError, "Invalid index: #{name.inspect}") end # Gets the value for the given attribute on the node def value(node, attribute) node.send(attribute) end # Adds the given key / node combination to an index, including the string # and symbol versions of the index def add_to_index(name, key, node) index(name)[key] = node index(:"#{name}_to_s")[key.to_s] = node index(:"#{name}_to_sym")[:"#{key}"] = node if to_sym?(key) end # Removes the given key from an index, including the string and symbol # versions of the index def remove_from_index(name, key) index(name).delete(key) index(:"#{name}_to_s").delete(key.to_s) index(:"#{name}_to_sym").delete(:"#{key}") if to_sym?(key) end # Updates the node for the given index, including the string and symbol # versions of the index def update_index(name, node) index = self.index(name) old_key = index.key(node) new_key = value(node, name) # Only replace the key if it's changed return unless old_key != new_key remove_from_index(name, old_key) add_to_index(name, new_key, node) end # Determines whether the given value can be converted to a symbol def to_sym?(value) "#{value}" != '' end # Evaluates the given context for a particular node. This will only # evaluate the context if the node matches. def eval_context(context, node) node.context(&context[:block]) if context[:nodes].matches?(node.name) end end end state_machines-0.100.4/lib/state_machines/options_validator.rb000066400000000000000000000062061507333401300244700ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines # Define the module if it doesn't exist yet # Module for validating options without monkey-patching Hash # Provides the same functionality as the Hash monkey patch but in a cleaner way module OptionsValidator class << self # Validates that all keys in the options hash are in the list of valid keys # # @param options [Hash] The options hash to validate # @param valid_keys [Array] List of valid key names # @param caller_info [String] Information about the calling method for better error messages # @raise [ArgumentError] If any invalid keys are found def assert_valid_keys!(options, *valid_keys, caller_info: nil) return if options.empty? valid_keys.flatten! invalid_keys = options.keys - valid_keys return if invalid_keys.empty? caller_context = caller_info ? " in #{caller_info}" : '' raise ArgumentError, "Unknown key#{'s' if invalid_keys.length > 1}: #{invalid_keys.map(&:inspect).join(', ')}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}#{caller_context}" end # Validates that at most one of the exclusive keys is present in the options hash # # @param options [Hash] The options hash to validate # @param exclusive_keys [Array] List of mutually exclusive keys # @param caller_info [String] Information about the calling method for better error messages # @raise [ArgumentError] If more than one exclusive key is found def assert_exclusive_keys!(options, *exclusive_keys, caller_info: nil) return if options.empty? conflicting_keys = exclusive_keys & options.keys return if conflicting_keys.length <= 1 caller_context = caller_info ? " in #{caller_info}" : '' raise ArgumentError, "Conflicting keys: #{conflicting_keys.join(', ')}#{caller_context}" end # Validates options using a more convenient interface that works with both # hash-style and kwargs-style method definitions # # @param valid_keys [Array] List of valid key names # @param exclusive_key_groups [Array>] Groups of mutually exclusive keys # @param caller_info [String] Information about the calling method # @return [Proc] A validation proc that can be called with options def validator(valid_keys: [], exclusive_key_groups: [], caller_info: nil) proc do |options| assert_valid_keys!(options, *valid_keys, caller_info: caller_info) unless valid_keys.empty? exclusive_key_groups.each do |group| assert_exclusive_keys!(options, *group, caller_info: caller_info) end end end # Helper method for backwards compatibility - allows gradual migration # from Hash monkey patch to this module # # @param options [Hash] The options to validate # @param valid_keys [Array] Valid keys # @return [Hash] The same options hash (for chaining) def validate_and_return(options, *valid_keys) assert_valid_keys!(options, *valid_keys) options end end end end state_machines-0.100.4/lib/state_machines/path.rb000066400000000000000000000102061507333401300216570ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'options_validator' module StateMachines # A path represents a sequence of transitions that can be run for a particular # object. Paths can walk to new transitions, revealing all of the possible # branches that can be encountered in the object's state machine. class Path < Array # The object whose state machine is being walked attr_reader :object # The state machine this path is walking attr_reader :machine # Creates a new transition path for the given object. Initially this is an # empty path. In order to start walking the path, it must be populated with # an initial transition. # # Configuration options: # * :target - The target state to end the path on # * :guard - Whether to guard transitions with the if/unless # conditionals defined for each one def initialize(object, machine, options = {}) StateMachines::OptionsValidator.assert_valid_keys!(options, :target, :guard) @object = object @machine = machine @target = options[:target] @guard = options[:guard] end def initialize_copy(orig) # :nodoc: super @transitions = nil end # The initial state name for this path def from_name first&.from_name end # Lists all of the from states that can be reached through this path. # # For example, # # path.to_states # => [:parked, :idling, :first_gear, ...] def from_states map { |transition| transition.from_name }.uniq end # The end state name for this path. If a target state was specified for # the path, then that will be returned if the path is complete. def to_name last&.to_name end # Lists all of the to states that can be reached through this path. # # For example, # # path.to_states # => [:parked, :idling, :first_gear, ...] def to_states map { |transition| transition.to_name }.uniq end # Lists all of the events that can be fired through this path. # # For example, # # path.events # => [:park, :ignite, :shift_up, ...] def events map { |transition| transition.event }.uniq end # Walks down the next transitions at the end of this path. This will only # walk down paths that are considered valid. def walk transitions.each { |transition| yield dup.push(transition) } end # Determines whether or not this path has completed. A path is considered # complete when one of the following conditions is met: # * The last transition in the path ends on the target state # * There are no more transitions remaining to walk and there is no target # state def complete? !empty? && (@target ? to_name == @target : transitions.empty?) end private # Calculates the number of times the given state has been walked to def times_walked_to(state) select { |transition| transition.to_name == state }.length end # Determines whether the given transition has been recently walked down in # this path. If a target is configured for this path, then this will only # look at transitions walked down since the target was last reached. def recently_walked?(transition) transitions = self if @target && @target != to_name && (target_transition = detect { |t| t.to_name == @target }) transitions = transitions[index(target_transition) + 1..-1] end transitions.include?(transition) end # Determines whether it's possible to walk to the given transition from # the current path. A transition can be walked to if: # * It has not been recently walked and # * If a target is specified, it has not been walked to twice yet def can_walk_to?(transition) !recently_walked?(transition) && (!@target || times_walked_to(@target) < 2) end # Get the next set of transitions that can be walked to starting from the # end of this path def transitions @transitions ||= empty? ? [] : machine.events.transitions_for(object, from: to_name, guard: @guard).select { |transition| can_walk_to?(transition) } end end end state_machines-0.100.4/lib/state_machines/path_collection.rb000066400000000000000000000054361507333401300241030ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'options_validator' module StateMachines # Represents a collection of paths that are generated based on a set of # requirements regarding what states to start and end on class PathCollection < Array # The object whose state machine is being walked attr_reader :object # The state machine these path are walking attr_reader :machine # The initial state to start each path from attr_reader :from_name # The target state for each path attr_reader :to_name # Creates a new collection of paths with the given requirements. # # Configuration options: # * :from - The initial state to start from # * :to - The target end state # * :deep - Whether to enable deep searches for the target state. # * :guard - Whether to guard transitions with the if/unless # conditionals defined for each one def initialize(object, machine, options = {}) options = { deep: false, from: machine.states.match!(object).name }.merge(options) StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :deep, :guard) @object = object @machine = machine @from_name = machine.states.fetch(options[:from]).name @to_name = options[:to] && machine.states.fetch(options[:to]).name @guard = options[:guard] @deep = options[:deep] initial_paths.each { |path| walk(path) } end # Lists all of the states that can be transitioned from through the paths in # this collection. # # For example, # # paths.from_states # => [:parked, :idling, :first_gear, ...] def from_states flat_map(&:from_states).uniq end # Lists all of the states that can be transitioned to through the paths in # this collection. # # For example, # # paths.to_states # => [:idling, :first_gear, :second_gear, ...] def to_states flat_map(&:to_states).uniq end # Lists all of the events that can be fired through the paths in this # collection. # # For example, # # paths.events # => [:park, :ignite, :shift_up, ...] def events flat_map(&:events).uniq end private # Gets the initial set of paths to walk def initial_paths machine.events.transitions_for(object, from: from_name, guard: @guard).map do |transition| path = Path.new(object, machine, target: to_name, guard: @guard) path << transition path end end # Walks down the given path. Each new path that matches the configured # requirements will be added to this collection. def walk(path) self << path if path.complete? path.walk { |next_path| walk(next_path) } unless to_name && path.complete? && !@deep end end end state_machines-0.100.4/lib/state_machines/state.rb000066400000000000000000000312651507333401300220530ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'options_validator' module StateMachines # A state defines a value that an attribute can be in after being transitioned # 0 or more times. States can represent a value of any type in Ruby, though # the most common (and default) type is String. # # In addition to defining the machine's value, a state can also define a # behavioral context for an object when that object is in the state. See # StateMachines::Machine#state for more information about how state-driven # behavior can be utilized. class State # The state machine for which this state is defined attr_reader :machine # The unique identifier for the state used in event and callback definitions attr_reader :name # The fully-qualified identifier for the state, scoped by the machine's # namespace attr_reader :qualified_name # The human-readable name for the state attr_writer :human_name # The value that is written to a machine's attribute when an object # transitions into this state attr_writer :value # Whether this state's value should be cached after being evaluated attr_accessor :cache # Whether or not this state is the initial state to use for new objects attr_accessor :initial alias initial? initial # A custom lambda block for determining whether a given value matches this # state attr_accessor :matcher # Creates a new state within the context of the given machine. # # Configuration options: # * :initial - Whether this state is the beginning state for the # machine. Default is false. # * :value - The value to store when an object transitions to this # state. Default is the name (stringified). # * :cache - If a dynamic value (via a lambda block) is being used, # then setting this to true will cache the evaluated result # * :if - Determines whether a value matches this state # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}). # By default, the configured value is matched. # * :human_name - The human-readable version of this state's name def initialize(machine, name, options = nil, initial: false, value: :__not_provided__, cache: nil, if: nil, human_name: nil, **extra_options) # :nodoc: # Handle both old hash style and new kwargs style for backward compatibility case options in Hash # Old style: initialize(machine, name, {initial: true, value: 'foo'}) StateMachines::OptionsValidator.assert_valid_keys!(options, :initial, :value, :cache, :if, :human_name) initial = options.fetch(:initial, false) value = options.include?(:value) ? options[:value] : :__not_provided__ cache = options[:cache] if_condition = options[:if] human_name = options[:human_name] in nil # New style: initialize(machine, name, initial: true, value: 'foo') StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :initial, :value, :cache, :if, :human_name) unless extra_options.empty? if_condition = binding.local_variable_get(:if) # 'if' is a keyword, need special handling else # Handle unexpected options raise ArgumentError, "Unexpected positional argument in State initialize: #{options.inspect}" end @machine = machine @name = name @qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name @human_name = human_name || (@name ? @name.to_s.tr('_', ' ') : 'nil') @value = value == :__not_provided__ ? name&.to_s : value @cache = cache @matcher = if_condition @initial = initial == true @context = StateContext.new(self) return unless name conflicting_machines = machine.owner_class.state_machines.select do |_other_name, other_machine| other_machine != machine && other_machine.states[qualified_name, :qualified_name] end # Output a warning if another machine has a conflicting qualified name # for a different attribute if (conflict = conflicting_machines.detect do |_other_name, other_machine| other_machine.attribute != machine.attribute end) _name, other_machine = conflict warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}" elsif conflicting_machines.empty? # Only bother adding predicates when another machine for the same # attribute hasn't already done so add_predicate end end # Creates a copy of this state, excluding the context to prevent conflicts # across different machines. def initialize_copy(orig) # :nodoc: super @context = StateContext.new(self) end def machine=(machine) @machine = machine @context = StateContext.new(self) end # Determines whether there are any states that can be transitioned to from # this state. If there are none, then this state is considered *final*. # Any objects in a final state will remain so forever given the current # machine's definition. def final? machine.events.none? do |event| event.branches.any? do |branch| branch.state_requirements.any? do |requirement| requirement[:from].matches?(name) && !requirement[:to].matches?(name, from: name) end end end end # Transforms the state name into a more human-readable format, such as # "first gear" instead of "first_gear" def human_name(klass = @machine.owner_class) @human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name end # Generates a human-readable description of this state's name / value: # # For example, # # State.new(machine, :parked).description # => "parked" # State.new(machine, :parked, :value => :parked).description # => "parked" # State.new(machine, :parked, :value => nil).description # => "parked (nil)" # State.new(machine, :parked, :value => 1).description # => "parked (1)" # State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*) # # Configuration options: # * :human_name - Whether to use this state's human name in the # description or just the internal name def description(options = {}) label = options[:human_name] ? human_name : name description = +(label ? label.to_s : label.inspect) description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s description end # The value that represents this state. This will optionally evaluate the # original block if it's a lambda block. Otherwise, the static value is # returned. # # For example, # # State.new(machine, :parked, :value => 1).value # => 1 # State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008 # State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => def value(eval = true) if @value.is_a?(Proc) && eval if cache_value? @value = @value.call machine.states.update(self) @value else @value.call end else @value end end # Determines whether this state matches the given value. If no matcher is # configured, then this will check whether the values are equivalent. # Otherwise, the matcher will determine the result. # # For example, # # # Without a matcher # state = State.new(machine, :parked, :value => 1) # state.matches?(1) # => true # state.matches?(2) # => false # # # With a matcher # state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?}) # state.matches?(nil) # => false # state.matches?(Time.now) # => true def matches?(other_value) matcher ? matcher.call(other_value) : other_value == value end # Defines a context for the state which will be enabled on instances of # the owner class when the machine is in this state. # # This can be called multiple times. Each time a new context is created, # a new module will be included in the owner class. def context(&) # Include the context context = @context machine.owner_class.class_eval { include context } # Evaluate the method definitions and track which ones were added old_methods = context_methods context.class_eval(&) new_methods = context_methods.to_a.reject { |(name, method)| old_methods[name] == method } # Alias new methods so that the only execute when the object is in this state new_methods.each do |(method_name, _method)| context_name = context_name_for(method_name) context.class_eval <<-END_EVAL, __FILE__, __LINE__ + 1 alias_method :"#{context_name}", :#{method_name} def #{method_name}(*args, &block) state = self.class.state_machine(#{machine.name.inspect}).states.fetch(#{name.inspect}) options = {:method_missing => lambda {super(*args, &block)}, :method_name => #{method_name.inspect}} state.call(self, :"#{context_name}", *(args + [options]), &block) end END_EVAL end true end # The list of methods that have been defined in this state's context def context_methods @context.instance_methods.inject({}) do |methods, name| methods.merge(name.to_sym => @context.instance_method(name)) end end # Calls a method defined in this state's context on the given object. All # arguments and any block will be passed into the method defined. # # If the method has never been defined for this state, then a NoMethodError # will be raised. def call(object, method, *args, &) options = args.last.is_a?(Hash) ? args.pop : {} options = { method_name: method }.merge(options) state = machine.states.match!(object) if state == self && object.respond_to?(method) object.send(method, *args, &) elsif (method_missing = options[:method_missing]) # Dispatch to the superclass since the object either isn't in this state # or this state doesn't handle the method begin method_missing.call rescue NoMethodError => e raise unless e.name.to_s == options[:method_name].to_s && e.args == args # No valid context for this method raise InvalidContext.new(object, "State #{state.name.inspect} for #{machine.name.inspect} is not a valid context for calling ##{options[:method_name]}") end end end def draw(graph, options = {}, io = $stdout) machine.renderer.draw_state(self, graph, options, io) end # Generates a nicely formatted description of this state's contents. # # For example, # # state = StateMachines::State.new(machine, :parked, :value => 1, :initial => true) # state # => # def inspect attributes = [[:name, name], [:value, @value], [:initial, initial?]] "#<#{self.class} #{attributes.map { |attr, value| "#{attr}=#{value.inspect}" } * ' '}>" end private # Should the value be cached after it's evaluated for the first time? def cache_value? @cache end # Adds a predicate method to the owner class so long as a name has # actually been configured for the state def add_predicate predicate_method = "#{qualified_name}?" if machine.send(:owner_class_ancestor_has_method?, :instance, predicate_method) warn_about_method_conflict(predicate_method, machine.owner_class.ancestors.first) elsif machine.send(:owner_class_has_method?, :instance, predicate_method) warn_about_method_conflict(predicate_method, machine.owner_class) else machine.define_helper(:instance, predicate_method) do |machine, object| machine.states.matches?(object, name) end end end # Generates the name of the method containing the actual implementation def context_name_for(method) :"__#{machine.name}_#{name}_#{method}_#{@context.object_id}__" end def warn_about_method_conflict(method, defined_in) return if StateMachines::Machine.ignore_method_conflicts warn "Instance method #{method.inspect} is already defined in #{defined_in.inspect}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true." end end end state_machines-0.100.4/lib/state_machines/state_collection.rb000066400000000000000000000077241507333401300242710ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines # Represents a collection of states in a state machine class StateCollection < NodeCollection def initialize(machine) # :nodoc: super(machine, index: %i[name qualified_name value]) end # Determines whether the given object is in a specific state. If the # object's current value doesn't match the state, then this will return # false, otherwise true. If the given state is unknown, then an IndexError # will be raised. # # == Examples # # class Vehicle # state_machine :initial => :parked do # other_states :idling # end # end # # states = Vehicle.state_machine.states # vehicle = Vehicle.new # => # # # states.matches?(vehicle, :parked) # => true # states.matches?(vehicle, :idling) # => false # states.matches?(vehicle, :invalid) # => IndexError: :invalid is an invalid key for :name index def matches?(object, name) fetch(name).matches?(machine.read(object, :state)) end # Determines the current state of the given object as configured by this # state machine. This will attempt to find a known state that matches # the value of the attribute on the object. # # == Examples # # class Vehicle # state_machine :initial => :parked do # other_states :idling # end # end # # states = Vehicle.state_machine.states # # vehicle = Vehicle.new # => # # states.match(vehicle) # => # # # vehicle.state = 'idling' # states.match(vehicle) # => # # # vehicle.state = 'invalid' # states.match(vehicle) # => nil def match(object) value = machine.read(object, :state) self[value, :value] || detect { |state| state.matches?(value) } end # Determines the current state of the given object as configured by this # state machine. If no state is found, then an ArgumentError will be # raised. # # == Examples # # class Vehicle # state_machine :initial => :parked do # other_states :idling # end # end # # states = Vehicle.state_machine.states # # vehicle = Vehicle.new # => # # states.match!(vehicle) # => # # # vehicle.state = 'invalid' # states.match!(vehicle) # => ArgumentError: "invalid" is not a known state value def match!(object) match(object) || raise(ArgumentError, "#{machine.read(object, :state).inspect} is not a known #{machine.name} value") end # Gets the order in which states should be displayed based on where they # were first referenced. This will order states in the following priority: # # 1. Initial state # 2. Event transitions (:from, :except_from, :to, :except_to options) # 3. States with behaviors # 4. States referenced via +state+ or +other_states+ # 5. States referenced in callbacks # # This order will determine how the GraphViz visualizations are rendered. def by_priority order = select { |state| state.initial }.map { |state| state.name } machine.events.each { |event| order += event.known_states } order += select { |state| state.context_methods.any? }.map { |state| state.name } order += keys(:name) - machine.callbacks.values.flatten.flat_map(&:known_states) order += keys(:name) order.uniq! order.map! { |name| self[name] } order end private # Gets the value for the given attribute on the node def value(node, attribute) attribute == :value ? node.value(false) : super end end end state_machines-0.100.4/lib/state_machines/state_context.rb000066400000000000000000000115101507333401300236060ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'options_validator' module StateMachines # Represents a module which will get evaluated within the context of a state. # # Class-level methods are proxied to the owner class, injecting a custom # :if condition along with method. This assumes that the method has # support for a set of configuration options, including :if. This # condition will check that the object's state matches this context's state. # # Instance-level methods are used to define state-driven behavior on the # state's owner class. # # == Examples # # class Vehicle # class << self # attr_accessor :validations # # def validate(options, &block) # validations << options # end # end # # self.validations = [] # attr_accessor :state, :simulate # # def moving? # self.class.validations.all? {|validation| validation[:if].call(self)} # end # end # # In the above class, a simple set of validation behaviors have been defined. # Each validation consists of a configuration like so: # # Vehicle.validate :unless => :simulate # Vehicle.validate :if => lambda {|vehicle| ...} # # In order to scope validations to a particular state context, the class-level # +validate+ method can be invoked like so: # # machine = StateMachines::Machine.new(Vehicle) # context = StateMachines::StateContext.new(machine.state(:first_gear)) # context.validate(:unless => :simulate) # # vehicle = Vehicle.new # => # # vehicle.moving? # => false # # vehicle.state = 'first_gear' # vehicle.moving? # => true # # vehicle.simulate = true # vehicle.moving? # => false class StateContext < Module include EvalHelpers # The state machine for which this context's state is defined attr_reader :machine # The state that must be present in an object for this context to be active attr_reader :state # Creates a new context for the given state def initialize(state) @state = state @machine = state.machine state_name = state.name machine_name = machine.name @condition = ->(object) { object.class.state_machine(machine_name).states.matches?(object, state_name) } end # Creates a new transition that determines what to change the current state # to when an event fires from this state. # # Since this transition is being defined within a state context, you do # *not* need to specify the :from option for the transition. For # example: # # state_machine do # state :parked do # transition :to => :idling, :on => [:ignite, :shift_up] # Transitions to :idling # transition :from => [:idling, :parked], :on => :park, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off # end # end # # See StateMachines::Machine#transition for a description of the possible # configurations for defining transitions. def transition(options) StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :on, :if, :unless) raise ArgumentError, 'Must specify :on event' unless options[:on] raise ArgumentError, 'Must specify either :to or :from state' unless !options[:to] ^ !options[:from] machine.transition(options.merge(options[:to] ? { from: state.name } : { to: state.name })) end # Hooks in condition-merging to methods that don't exist in this module def method_missing(*args, &) # Get the configuration if args.last.is_a?(Hash) options = args.last else args << options = {} end # Get any existing condition that may need to be merged if_condition = options.delete(:if) unless_condition = options.delete(:unless) # Provide scope access to configuration in case the block is evaluated # within the object instance proxy = self proxy_condition = @condition # Replace the configuration condition with the one configured for this # proxy, merging together any existing conditions options[:if] = lambda do |*condition_args| # Block may be executed within the context of the actual object, so # it'll either be the first argument or the executing context object = condition_args.first || self proxy.evaluate_method(object, proxy_condition) && Array(if_condition).all? { |condition| proxy.evaluate_method(object, condition) } && !Array(unless_condition).any? { |condition| proxy.evaluate_method(object, condition) } end # Evaluate the method on the owner class with the condition proxied # through machine.owner_class.send(*args, &) end end end state_machines-0.100.4/lib/state_machines/stdio_renderer.rb000066400000000000000000000043171507333401300237410ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines module STDIORenderer module_function def draw_machine(machine, io: $stdout) draw_class(machine: machine, io: io) draw_states(machine: machine, io: io) draw_events(machine: machine, io: io) end module_function def draw_class(machine:, io: $stdout) io.puts "Class: #{machine.owner_class.name}" end module_function def draw_states(machine:, io: $stdout) io.puts ' States:' if machine.states.to_a.empty? io.puts ' - None' else machine.states.each do |state| io.puts " - #{state.name}" end end end module_function def draw_event(event, _graph, options: {}, io: $stdout) io = io || options[:io] || $stdout io.puts " Event: #{event.name}" end module_function def draw_branch(branch, _graph, _event, options: {}, io: $stdout) io = io || options[:io] || $stdout io.puts " Branch: #{branch.inspect}" end module_function def draw_state(state, _graph, options: {}, io: $stdout) io = io || options[:io] || $stdout io.puts " State: #{state.name}" end module_function def draw_events(machine:, io: $stdout) io.puts ' Events:' if machine.events.to_a.empty? io.puts ' - None' else machine.events.each do |event| io.puts " - #{event.name}" event.branches.each do |branch| branch.state_requirements.each do |requirement| out = +' - ' out << "#{draw_requirement(requirement[:from])} => #{draw_requirement(requirement[:to])}" out << " IF #{branch.if_condition}" if branch.if_condition out << " UNLESS #{branch.unless_condition}" if branch.unless_condition io.puts out end end end end end module_function def draw_requirement(requirement) case requirement when StateMachines::BlacklistMatcher "ALL EXCEPT #{requirement.values.join(', ')}" when StateMachines::AllMatcher 'ALL' when StateMachines::LoopbackMatcher 'SAME' else requirement.values.join(', ') end end end end state_machines-0.100.4/lib/state_machines/syntax_validator.rb000066400000000000000000000027621507333401300243260ustar00rootroot00000000000000# frozen_string_literal: true require 'ripper' module StateMachines # Cross-platform syntax validation for eval strings # Supports CRuby, JRuby, TruffleRuby via pluggable backends module SyntaxValidator # Public API: raises SyntaxError if code is invalid def validate!(code, filename = '(eval)') backend.validate!(code, filename) end module_function :validate! private # Lazily pick the best backend for this platform # Prefer RubyVM for performance on CRuby, fallback to Ripper for compatibility def backend @backend ||= if RubyVmBackend.available? RubyVmBackend else RipperBackend end end module_function :backend # MRI backend using RubyVM::InstructionSequence module RubyVmBackend def available? RUBY_ENGINE == 'ruby' end module_function :available? def validate!(code, filename) # compile will raise a SyntaxError on bad syntax RubyVM::InstructionSequence.compile(code, filename) true end module_function :validate! end # Universal Ruby backend via Ripper module RipperBackend def validate!(code, filename) sexp = Ripper.sexp(code) if sexp.nil? # Ripper.sexp returns nil on a parse error, but no exception raise SyntaxError, "syntax error in #{filename}" end true end module_function :validate! end end end state_machines-0.100.4/lib/state_machines/test_helper.rb000066400000000000000000001107531507333401300232510ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines # Test helper module providing assertion methods for state machine testing # Designed to work with Minitest, RSpec, and future testing frameworks # # @example Basic usage with Minitest # class MyModelTest < Minitest::Test # include StateMachines::TestHelper # # def test_initial_state # model = MyModel.new # assert_state(model, :state_machine_name, :initial_state) # end # end # # @example Usage with RSpec # RSpec.describe MyModel do # include StateMachines::TestHelper # # it "starts in initial state" do # model = MyModel.new # assert_state(model, :state_machine_name, :initial_state) # end # end # # @since 0.10.0 module TestHelper # Assert that an object is in a specific state for a given state machine # # @param object [Object] The object with state machines # @param expected_state [Symbol] The expected state # @param machine_name [Symbol] The name of the state machine (defaults to :state) # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If the state doesn't match # # @example # user = User.new # assert_sm_state(user, :active) # Uses default :state machine # assert_sm_state(user, :active, machine_name: :status) # Uses :status machine def assert_sm_state(object, expected_state, machine_name: :state, message: nil) name_method = "#{machine_name}_name" # Handle the case where machine_name doesn't have a corresponding _name method unless object.respond_to?(name_method) available_machines = begin object.class.state_machines.keys rescue StandardError [] end raise ArgumentError, "No state machine '#{machine_name}' found. Available machines: #{available_machines.inspect}" end actual = object.send(name_method) default_message = "Expected #{object.class}##{machine_name} to be #{expected_state}, but was #{actual}" if defined?(::Minitest) assert_equal expected_state.to_s, actual.to_s, message || default_message elsif defined?(::RSpec) expect(actual.to_s).to eq(expected_state.to_s), message || default_message else raise "Expected #{expected_state}, but got #{actual}" unless expected_state.to_s == actual.to_s end end # Assert that an object can transition via a specific event # # @param object [Object] The object with state machines # @param event [Symbol] The event name # @param machine_name [Symbol] The name of the state machine (defaults to :state) # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If the transition is not available # # @example # user = User.new # assert_sm_can_transition(user, :activate) # Uses default :state machine # assert_sm_can_transition(user, :activate, machine_name: :status) # Uses :status machine def assert_sm_can_transition(object, event, machine_name: :state, message: nil) # Try different method naming patterns possible_methods = [ "can_#{event}?", # Default state machine or non-namespaced "can_#{event}_#{machine_name}?" # Namespaced events ] can_method = possible_methods.find { |method| object.respond_to?(method) } unless can_method available_methods = object.methods.grep(/^can_.*\?$/).sort raise ArgumentError, "No transition method found for event :#{event} on machine :#{machine_name}. Available methods: #{available_methods.first(10).inspect}" end default_message = "Expected to be able to trigger event :#{event} on #{machine_name}, but #{can_method} returned false" if defined?(::Minitest) assert object.send(can_method), message || default_message elsif defined?(::RSpec) expect(object.send(can_method)).to be_truthy, message || default_message else raise default_message unless object.send(can_method) end end # Assert that an object cannot transition via a specific event # # @param object [Object] The object with state machines # @param event [Symbol] The event name # @param machine_name [Symbol] The name of the state machine (defaults to :state) # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If the transition is available # # @example # user = User.new # assert_sm_cannot_transition(user, :delete) # Uses default :state machine # assert_sm_cannot_transition(user, :delete, machine_name: :status) # Uses :status machine def assert_sm_cannot_transition(object, event, machine_name: :state, message: nil) # Try different method naming patterns possible_methods = [ "can_#{event}?", # Default state machine or non-namespaced "can_#{event}_#{machine_name}?" # Namespaced events ] can_method = possible_methods.find { |method| object.respond_to?(method) } unless can_method available_methods = object.methods.grep(/^can_.*\?$/).sort raise ArgumentError, "No transition method found for event :#{event} on machine :#{machine_name}. Available methods: #{available_methods.first(10).inspect}" end default_message = "Expected not to be able to trigger event :#{event} on #{machine_name}, but #{can_method} returned true" if defined?(::Minitest) refute object.send(can_method), message || default_message elsif defined?(::RSpec) expect(object.send(can_method)).to be_falsy, message || default_message elsif object.send(can_method) raise default_message end end # Assert that triggering an event changes the object to the expected state # # @param object [Object] The object with state machines # @param event [Symbol] The event to trigger # @param expected_state [Symbol] The expected state after transition # @param machine_name [Symbol] The name of the state machine (defaults to :state) # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If the transition fails or results in wrong state # # @example # user = User.new # assert_sm_transition(user, :activate, :active) # Uses default :state machine # assert_sm_transition(user, :activate, :active, machine_name: :status) # Uses :status machine def assert_sm_transition(object, event, expected_state, machine_name: :state, message: nil) object.send("#{event}!") assert_sm_state(object, expected_state, machine_name: machine_name, message: message) end # === Extended State Machine Assertions === def assert_sm_states_list(machine, expected_states, message = nil) actual_states = machine.states.map(&:name).compact default_message = "Expected states #{expected_states} but got #{actual_states}" if defined?(::Minitest) assert_equal expected_states.sort, actual_states.sort, message || default_message elsif defined?(::RSpec) expect(actual_states.sort).to eq(expected_states.sort), message || default_message else raise default_message unless expected_states.sort == actual_states.sort end end def refute_sm_state_defined(machine, state, message = nil) state_exists = machine.states.any? { |s| s.name == state } default_message = "Expected state #{state} to not be defined in machine" if defined?(::Minitest) refute state_exists, message || default_message elsif defined?(::RSpec) expect(state_exists).to be_falsy, message || default_message elsif state_exists raise default_message end end alias assert_sm_state_not_defined refute_sm_state_defined def assert_sm_initial_state(machine, expected_state, message = nil) state_obj = machine.state(expected_state) is_initial = state_obj&.initial? default_message = "Expected state #{expected_state} to be the initial state" if defined?(::Minitest) assert is_initial, message || default_message elsif defined?(::RSpec) expect(is_initial).to be_truthy, message || default_message else raise default_message unless is_initial end end def assert_sm_final_state(machine, state, message = nil) state_obj = machine.states[state] is_final = state_obj&.final? default_message = "Expected state #{state} to be final" if defined?(::Minitest) assert is_final, message || default_message elsif defined?(::RSpec) expect(is_final).to be_truthy, message || default_message else raise default_message unless is_final end end def assert_sm_possible_transitions(machine, from:, expected_to_states:, message: nil) actual_transitions = machine.events.flat_map do |event| event.branches.select { |branch| branch.known_states.include?(from) } .map(&:to) end.uniq default_message = "Expected transitions from #{from} to #{expected_to_states} but got #{actual_transitions}" if defined?(::Minitest) assert_equal expected_to_states.sort, actual_transitions.sort, message || default_message elsif defined?(::RSpec) expect(actual_transitions.sort).to eq(expected_to_states.sort), message || default_message else raise default_message unless expected_to_states.sort == actual_transitions.sort end end def refute_sm_transition_allowed(machine, from:, to:, on:, message: nil) event = machine.events[on] is_allowed = event&.branches&.any? { |branch| branch.known_states.include?(from) && branch.to == to } default_message = "Expected transition from #{from} to #{to} on #{on} to not be allowed" if defined?(::Minitest) refute is_allowed, message || default_message elsif defined?(::RSpec) expect(is_allowed).to be_falsy, message || default_message elsif is_allowed raise default_message end end alias assert_sm_transition_not_allowed refute_sm_transition_allowed def assert_sm_event_triggers(object, event, machine_name = :state, message = nil) initial_state = object.send(machine_name) object.send("#{event}!") state_changed = initial_state != object.send(machine_name) default_message = "Expected event #{event} to trigger state change on #{machine_name}" if defined?(::Minitest) assert state_changed, message || default_message elsif defined?(::RSpec) expect(state_changed).to be_truthy, message || default_message else raise default_message unless state_changed end end def refute_sm_event_triggers(object, event, machine_name = :state, message = nil) initial_state = object.send(machine_name) begin object.send("#{event}!") state_unchanged = initial_state == object.send(machine_name) default_message = "Expected event #{event} to not trigger state change on #{machine_name}" if defined?(::Minitest) assert state_unchanged, message || default_message elsif defined?(::RSpec) expect(state_unchanged).to be_truthy, message || default_message else raise default_message unless state_unchanged end rescue StateMachines::InvalidTransition # Expected behavior - transition was blocked end end alias assert_sm_event_not_triggers refute_sm_event_triggers def assert_sm_event_raises_error(object, event, error_class, message = nil) default_message = "Expected event #{event} to raise #{error_class}" if defined?(::Minitest) assert_raises(error_class, message || default_message) do object.send("#{event}!") end elsif defined?(::RSpec) expect { object.send("#{event}!") }.to raise_error(error_class), message || default_message else begin object.send("#{event}!") raise default_message rescue error_class # Expected behavior end end end def assert_sm_callback_executed(object, callback_name, message = nil) callbacks_executed = object.instance_variable_get(:@_sm_callbacks_executed) || [] callback_was_executed = callbacks_executed.include?(callback_name) default_message = "Expected callback #{callback_name} to be executed" if defined?(::Minitest) assert callback_was_executed, message || default_message elsif defined?(::RSpec) expect(callback_was_executed).to be_truthy, message || default_message else raise default_message unless callback_was_executed end end def refute_sm_callback_executed(object, callback_name, message = nil) callbacks_executed = object.instance_variable_get(:@_sm_callbacks_executed) || [] callback_was_executed = callbacks_executed.include?(callback_name) default_message = "Expected callback #{callback_name} to not be executed" if defined?(::Minitest) refute callback_was_executed, message || default_message elsif defined?(::RSpec) expect(callback_was_executed).to be_falsy, message || default_message elsif callback_was_executed raise default_message end end alias assert_sm_callback_not_executed refute_sm_callback_executed # Assert that a record's state is persisted correctly for a specific state machine # # @param record [Object] The record to check (should respond to reload) # @param expected [String, Symbol] The expected persisted state # @param machine_name [Symbol] The name of the state machine (defaults to :state) # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If the persisted state doesn't match # # @example # # Default state machine # assert_sm_state_persisted(user, "active") # # # Specific state machine # assert_sm_state_persisted(ship, "up", :shields) # assert_sm_state_persisted(ship, "armed", :weapons) def assert_sm_state_persisted(record, expected, machine_name = :state, message = nil) record.reload if record.respond_to?(:reload) actual_state = record.send(machine_name) default_message = "Expected persisted state #{expected} for #{machine_name} but got #{actual_state}" if defined?(::Minitest) assert_equal expected, actual_state, message || default_message elsif defined?(::RSpec) expect(actual_state).to eq(expected), message || default_message else raise default_message unless expected == actual_state end end # Assert that executing a block triggers one or more expected events # # @param object [Object] The object with state machines # @param expected_events [Symbol, Array] The event(s) expected to be triggered # @param machine_name [Symbol] The name of the state machine (defaults to :state) # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If the expected events were not triggered # # @example # # Single event # assert_sm_triggers_event(vehicle, :crash) { vehicle.redline } # # # Multiple events # assert_sm_triggers_event(vehicle, [:crash, :emergency]) { vehicle.emergency_stop } # # # Specific machine # assert_sm_triggers_event(vehicle, :disable, machine_name: :alarm) { vehicle.turn_off_alarm } def assert_sm_triggers_event(object, expected_events, machine_name: :state, message: nil) expected_events = Array(expected_events) triggered_events = [] # Get the state machine machine = object.class.state_machines[machine_name] raise ArgumentError, "No state machine found for #{machine_name}" unless machine # Save original callbacks to restore later machine.callbacks[:before].dup # Add a temporary callback to track triggered events temp_callback = machine.before_transition do |_obj, transition| triggered_events << transition.event if transition.event end begin # Execute the block yield # Check if expected events were triggered missing_events = expected_events - triggered_events extra_events = triggered_events - expected_events unless missing_events.empty? && extra_events.empty? default_message = "Expected events #{expected_events.inspect} to be triggered, but got #{triggered_events.inspect}" default_message += ". Missing: #{missing_events.inspect}" if missing_events.any? default_message += ". Extra: #{extra_events.inspect}" if extra_events.any? if defined?(::Minitest) assert false, message || default_message elsif defined?(::RSpec) raise message || default_message else raise default_message end end ensure # Restore original callbacks by removing the temporary one machine.callbacks[:before].delete(temp_callback) end end # Assert that a before_transition callback is defined with expected arguments # # @param machine_or_class [StateMachines::Machine, Class] The machine or class to check # @param options [Hash] Expected callback options (on:, from:, to:, do:, if:, unless:) # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If the callback is not defined # # @example # # Check for specific transition callback # assert_before_transition(Vehicle, on: :crash, do: :emergency_stop) # # # Check with from/to states # assert_before_transition(Vehicle.state_machine, from: :parked, to: :idling, do: :start_engine) # # # Check with conditions # assert_before_transition(Vehicle, on: :ignite, if: :seatbelt_on?) def assert_before_transition(machine_or_class, options = {}, message = nil) _assert_transition_callback(:before, machine_or_class, options, message) end # Assert that an after_transition callback is defined with expected arguments # # @param machine_or_class [StateMachines::Machine, Class] The machine or class to check # @param options [Hash] Expected callback options (on:, from:, to:, do:, if:, unless:) # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If the callback is not defined # # @example # # Check for specific transition callback # assert_after_transition(Vehicle, on: :crash, do: :tow) # # # Check with from/to states # assert_after_transition(Vehicle.state_machine, from: :stalled, to: :parked, do: :log_repair) def assert_after_transition(machine_or_class, options = {}, message = nil) _assert_transition_callback(:after, machine_or_class, options, message) end # === Sync Mode Assertions === # Assert that a state machine is operating in synchronous mode # # @param object [Object] The object with state machines # @param machine_name [Symbol] The name of the state machine (defaults to :state) # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If the machine has async mode enabled # # @example # user = User.new # assert_sm_sync_mode(user) # Uses default :state machine # assert_sm_sync_mode(user, :status) # Uses :status machine def assert_sm_sync_mode(object, machine_name = :state, message = nil) machine = object.class.state_machines[machine_name] raise ArgumentError, "No state machine '#{machine_name}' found" unless machine async_enabled = machine.respond_to?(:async_mode_enabled?) && machine.async_mode_enabled? default_message = "Expected state machine '#{machine_name}' to be in sync mode, but async mode is enabled" if defined?(::Minitest) refute async_enabled, message || default_message elsif defined?(::RSpec) expect(async_enabled).to be_falsy, message || default_message elsif async_enabled raise default_message end end # Assert that async methods are not available on a sync-only object # # @param object [Object] The object with state machines # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If async methods are available # # @example # sync_only_car = Car.new # Car has no async: true machines # assert_sm_no_async_methods(sync_only_car) def assert_sm_no_async_methods(object, message = nil) async_methods = %i[fire_event_async fire_events_async fire_event_async! async_fire_event] available_async_methods = async_methods.select { |method| object.respond_to?(method) } default_message = "Expected no async methods to be available, but found: #{available_async_methods.inspect}" if defined?(::Minitest) assert_empty available_async_methods, message || default_message elsif defined?(::RSpec) expect(available_async_methods).to be_empty, message || default_message elsif available_async_methods.any? raise default_message end end # Assert that an object has no async-enabled state machines # # @param object [Object] The object with state machines # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If any machine has async mode enabled # # @example # sync_only_vehicle = Vehicle.new # All machines are sync-only # assert_sm_all_sync(sync_only_vehicle) def assert_sm_all_sync(object, message = nil) async_machines = [] object.class.state_machines.each do |name, machine| if machine.respond_to?(:async_mode_enabled?) && machine.async_mode_enabled? async_machines << name end end default_message = "Expected all state machines to be sync-only, but these have async enabled: #{async_machines.inspect}" if defined?(::Minitest) assert_empty async_machines, message || default_message elsif defined?(::RSpec) expect(async_machines).to be_empty, message || default_message elsif async_machines.any? raise default_message end end # Assert that synchronous event execution works correctly # # @param object [Object] The object with state machines # @param event [Symbol] The event to trigger # @param expected_state [Symbol] The expected state after transition # @param machine_name [Symbol] The name of the state machine (defaults to :state) # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If sync execution fails # # @example # car = Car.new # assert_sm_sync_execution(car, :start, :running) # assert_sm_sync_execution(car, :turn_on, :active, :alarm) def assert_sm_sync_execution(object, event, expected_state, machine_name = :state, message = nil) # Store initial state initial_state = object.send(machine_name) # Execute event synchronously result = object.send("#{event}!") # Verify immediate state change (no async delay) final_state = object.send(machine_name) # Check that transition succeeded state_changed = initial_state != final_state correct_final_state = final_state.to_s == expected_state.to_s default_message = "Expected sync execution of '#{event}' to change #{machine_name} from '#{initial_state}' to '#{expected_state}', but got '#{final_state}'" if defined?(::Minitest) assert result, "Event #{event} should return true on success" assert state_changed, "State should change from #{initial_state}" assert correct_final_state, message || default_message elsif defined?(::RSpec) expect(result).to be_truthy, "Event #{event} should return true on success" expect(state_changed).to be_truthy, "State should change from #{initial_state}" expect(correct_final_state).to be_truthy, message || default_message else raise "Event #{event} should return true on success" unless result raise "State should change from #{initial_state}" unless state_changed raise default_message unless correct_final_state end end # Assert that event execution is immediate (no async delay) # # @param object [Object] The object with state machines # @param event [Symbol] The event to trigger # @param machine_name [Symbol] The name of the state machine (defaults to :state) # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If execution appears to be async # # @example # car = Car.new # assert_sm_immediate_execution(car, :start) def assert_sm_immediate_execution(object, event, machine_name = :state, message = nil) initial_state = object.send(machine_name) # Record start time and execute start_time = Time.now object.send("#{event}!") execution_time = Time.now - start_time final_state = object.send(machine_name) state_changed = initial_state != final_state # Should complete very quickly (under 10ms for sync operations) is_immediate = execution_time < 0.01 default_message = "Expected immediate sync execution of '#{event}', but took #{execution_time}s (likely async)" if defined?(::Minitest) assert state_changed, "Event should trigger state change" assert is_immediate, message || default_message elsif defined?(::RSpec) expect(state_changed).to be_truthy, "Event should trigger state change" expect(is_immediate).to be_truthy, message || default_message else raise "Event should trigger state change" unless state_changed raise default_message unless is_immediate end end # === Async Mode Assertions === # Assert that a state machine is operating in asynchronous mode # # @param object [Object] The object with state machines # @param machine_name [Symbol] The name of the state machine (defaults to :state) # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If the machine doesn't have async mode enabled # # @example # drone = AutonomousDrone.new # assert_sm_async_mode(drone) # Uses default :state machine # assert_sm_async_mode(drone, :teleporter_status) # Uses :teleporter_status machine def assert_sm_async_mode(object, machine_name = :state, message = nil) machine = object.class.state_machines[machine_name] raise ArgumentError, "No state machine '#{machine_name}' found" unless machine async_enabled = machine.respond_to?(:async_mode_enabled?) && machine.async_mode_enabled? default_message = "Expected state machine '#{machine_name}' to have async mode enabled, but it's in sync mode" if defined?(::Minitest) assert async_enabled, message || default_message elsif defined?(::RSpec) expect(async_enabled).to be_truthy, message || default_message else raise default_message unless async_enabled end end # Assert that async methods are available on an async-enabled object # # @param object [Object] The object with state machines # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If async methods are not available # # @example # drone = AutonomousDrone.new # Has async: true machines # assert_sm_async_methods(drone) def assert_sm_async_methods(object, message = nil) async_methods = %i[fire_event_async fire_events_async fire_event_async! async_fire_event] available_async_methods = async_methods.select { |method| object.respond_to?(method) } default_message = "Expected async methods to be available, but found none" if defined?(::Minitest) refute_empty available_async_methods, message || default_message elsif defined?(::RSpec) expect(available_async_methods).not_to be_empty, message || default_message elsif available_async_methods.empty? raise default_message end end # Assert that an object has async-enabled state machines # # @param object [Object] The object with state machines # @param machine_names [Array] Expected async machine names # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If expected machines don't have async mode # # @example # drone = AutonomousDrone.new # assert_sm_has_async(drone, [:status, :teleporter_status, :shields]) def assert_sm_has_async(object, machine_names = nil, message = nil) if machine_names # Check specific machines non_async_machines = machine_names.reject do |name| machine = object.class.state_machines[name] machine&.respond_to?(:async_mode_enabled?) && machine.async_mode_enabled? end default_message = "Expected machines #{machine_names.inspect} to have async enabled, but these don't: #{non_async_machines.inspect}" if defined?(::Minitest) assert_empty non_async_machines, message || default_message elsif defined?(::RSpec) expect(non_async_machines).to be_empty, message || default_message elsif non_async_machines.any? raise default_message end else # Check that at least one machine has async async_machines = object.class.state_machines.select do |name, machine| machine.respond_to?(:async_mode_enabled?) && machine.async_mode_enabled? end default_message = "Expected at least one state machine to have async enabled, but none found" if defined?(::Minitest) refute_empty async_machines, message || default_message elsif defined?(::RSpec) expect(async_machines).not_to be_empty, message || default_message elsif async_machines.empty? raise default_message end end end # Assert that individual async event methods are available # # @param object [Object] The object with state machines # @param event [Symbol] The event name # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If async event methods are not available # # @example # drone = AutonomousDrone.new # assert_sm_async_event_methods(drone, :launch) # Checks launch_async and launch_async! def assert_sm_async_event_methods(object, event, message = nil) async_method = "#{event}_async".to_sym async_bang_method = "#{event}_async!".to_sym has_async = object.respond_to?(async_method) has_async_bang = object.respond_to?(async_bang_method) if defined?(::Minitest) assert has_async, "Missing #{async_method} method" assert has_async_bang, "Missing #{async_bang_method} method" elsif defined?(::RSpec) expect(has_async).to be_truthy, "Missing #{async_method} method" expect(has_async_bang).to be_truthy, "Missing #{async_bang_method} method" else raise "Missing #{async_method} method" unless has_async raise "Missing #{async_bang_method} method" unless has_async_bang end end # Assert that an object has thread-safe state methods when async is enabled # # @param object [Object] The object with state machines # @param message [String, nil] Custom failure message # @return [void] # @raise [AssertionError] If thread-safe methods are not available # # @example # drone = AutonomousDrone.new # assert_sm_thread_safe_methods(drone) def assert_sm_thread_safe_methods(object, message = nil) thread_safe_methods = %i[state_machine_mutex read_state_safely write_state_safely] missing_methods = thread_safe_methods.reject { |method| object.respond_to?(method) } default_message = "Expected thread-safe methods to be available, but missing: #{missing_methods.inspect}" if defined?(::Minitest) assert_empty missing_methods, message || default_message elsif defined?(::RSpec) expect(missing_methods).to be_empty, message || default_message elsif missing_methods.any? raise default_message end end # RSpec-style aliases for event triggering (for consistency with RSpec expectations) alias expect_to_trigger_event assert_sm_triggers_event alias have_triggered_event assert_sm_triggers_event private # Internal helper for checking transition callbacks def _assert_transition_callback(callback_type, machine_or_class, options, message) # Get the machine machine = machine_or_class.is_a?(StateMachines::Machine) ? machine_or_class : machine_or_class.state_machine raise ArgumentError, 'No state machine found' unless machine callbacks = machine.callbacks[callback_type] || [] # Extract expected conditions expected_event = options[:on] expected_from = options[:from] expected_to = options[:to] expected_method = options[:do] expected_if = options[:if] expected_unless = options[:unless] # Find matching callback matching_callback = callbacks.find do |callback| branch = callback.branch # Check event requirement if expected_event event_requirement = branch.event_requirement event_matches = if event_requirement && event_requirement.respond_to?(:values) event_requirement.values.include?(expected_event) else false end next false unless event_matches end # Check state requirements (from/to) if expected_from || expected_to state_matches = false branch.state_requirements.each do |req| from_matches = !expected_from || (req[:from] && req[:from].respond_to?(:values) && req[:from].values.include?(expected_from)) to_matches = !expected_to || (req[:to] && req[:to].respond_to?(:values) && req[:to].values.include?(expected_to)) if from_matches && to_matches state_matches = true break end end next false unless state_matches end # Check method requirement if expected_method methods = callback.instance_variable_get(:@methods) || [] method_matches = methods.any? do |method| (method.is_a?(Symbol) && method == expected_method) || (method.is_a?(String) && method.to_sym == expected_method) || (method.respond_to?(:call) && method.respond_to?(:source_location)) end next false unless method_matches end # Check if condition if expected_if if_condition = branch.if_condition if_matches = (if_condition.is_a?(Symbol) && if_condition == expected_if) || (if_condition.is_a?(String) && if_condition.to_sym == expected_if) || if_condition.respond_to?(:call) next false unless if_matches end # Check unless condition if expected_unless unless_condition = branch.unless_condition unless_matches = (unless_condition.is_a?(Symbol) && unless_condition == expected_unless) || (unless_condition.is_a?(String) && unless_condition.to_sym == expected_unless) || unless_condition.respond_to?(:call) next false unless unless_matches end true end return if matching_callback expected_parts = [] expected_parts << "on: #{expected_event.inspect}" if expected_event expected_parts << "from: #{expected_from.inspect}" if expected_from expected_parts << "to: #{expected_to.inspect}" if expected_to expected_parts << "do: #{expected_method.inspect}" if expected_method expected_parts << "if: #{expected_if.inspect}" if expected_if expected_parts << "unless: #{expected_unless.inspect}" if expected_unless default_message = "Expected #{callback_type}_transition callback with #{expected_parts.join(', ')} to be defined, but it was not found" if defined?(::Minitest) assert false, message || default_message elsif defined?(::RSpec) raise message || default_message else raise default_message end end end end state_machines-0.100.4/lib/state_machines/transition.rb000066400000000000000000000501511507333401300231200ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines # Module to extend Fiber instances for pausable state tracking module PausableFiber attr_accessor :state_machine_fiber_pausable end # A transition represents a state change for a specific attribute. # # Transitions consist of: # * An event # * A starting state # * An ending state class Transition # The object being transitioned attr_reader :object # The state machine for which this transition is defined attr_reader :machine # The original state value *before* the transition attr_reader :from # The new state value *after* the transition attr_reader :to # The arguments passed in to the event that triggered the transition # (does not include the +run_action+ boolean argument if specified) attr_accessor :args # The result of invoking the action associated with the machine attr_reader :result # Whether the transition is only existing temporarily for the object attr_writer :transient # Creates a new, specific transition def initialize(object, machine, event, from_name, to_name, read_state = true) # :nodoc: @object = object @machine = machine @args = [] @transient = false @paused_fiber = nil @resuming = false @continuation_block = nil @fiber_thread_storage = nil @event = machine.events.fetch(event) @from_state = machine.states.fetch(from_name) @from = read_state ? machine.read(object, :state) : @from_state.value @to_state = machine.states.fetch(to_name) @to = @to_state.value reset end # The attribute which this transition's machine is defined for def attribute machine.attribute end # The action that will be run when this transition is performed def action machine.action end # The event that triggered the transition def event @event.name end # The fully-qualified name of the event that triggered the transition def qualified_event @event.qualified_name end # The human-readable name of the event that triggered the transition def human_event @event.human_name(@object.class) end # The state name *before* the transition def from_name @from_state.name end # The fully-qualified state name *before* the transition def qualified_from_name @from_state.qualified_name end # The human-readable state name *before* the transition def human_from_name @from_state.human_name(@object.class) end # The new state name *after* the transition def to_name @to_state.name end # The new fully-qualified state name *after* the transition def qualified_to_name @to_state.qualified_name end # The new human-readable state name *after* the transition def human_to_name @to_state.human_name(@object.class) end # Does this transition represent a loopback (i.e. the from and to state # are the same) # # == Example # # machine = StateMachine.new(Vehicle) # StateMachines::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback? # => true # StateMachines::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback? # => false def loopback? from_name == to_name end # Is this transition existing for a short period only? If this is set, it # indicates that the transition (or the event backing it) should not be # written to the object if it fails. def transient? @transient end # A hash of all the core attributes defined for this transition with their # names as keys and values of the attributes as values. # # == Example # # machine = StateMachine.new(Vehicle) # transition = StateMachines::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling) # transition.attributes # => {:object => #, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'} def attributes @attributes ||= { object: object, attribute: attribute, event: event, from: from, to: to } end # Runs the actual transition and any before/after callbacks associated # with the transition. The action associated with the transition/machine # can be skipped by passing in +false+. # # == Examples # # class Vehicle # state_machine :action => :save do # ... # end # end # # vehicle = Vehicle.new # transition = StateMachines::Transition.new(vehicle, machine, :ignite, :parked, :idling) # transition.perform # => Runs the +save+ action after setting the state attribute # transition.perform(false) # => Only sets the state attribute # transition.perform(run_action: false) # => Only sets the state attribute # transition.perform(Time.now) # => Passes in additional arguments and runs the +save+ action # transition.perform(Time.now, false) # => Passes in additional arguments and only sets the state attribute # transition.perform(Time.now, run_action: false) # => Passes in additional arguments and only sets the state attribute def perform(*args) run_action = case args.last in true | false args.pop in { run_action: } args.last.delete(:run_action) else true end self.args = args # Run the transition !!TransitionCollection.new([self], { use_transactions: machine.use_transactions, actions: run_action }).perform end # Runs a block within a transaction for the object being transitioned. # By default, transactions are a no-op unless otherwise defined by the # machine's integration. def within_transaction(&) machine.within_transaction(object, &) end # Runs the before / after callbacks for this transition. If a block is # provided, then it will be executed between the before and after callbacks. # # Configuration options: # * +before+ - Whether to run before callbacks. # * +after+ - Whether to run after callbacks. If false, then any around # callbacks will be paused until called again with +after+ enabled. # Default is true. # # This will return true if all before callbacks gets executed. After # callbacks will not have an effect on the result. def run_callbacks(options = {}, &block) options = { before: true, after: true }.merge(options) # If we have a paused fiber and we're not trying to resume (after: false), # this is an idempotent call on an already-paused transition. Just return true. return true if @paused_fiber&.alive? && !options[:after] # Always use fibers for compatibility with existing pause/resume functionality # The fiber argument can still be used to explicitly control fiber usage pausable_options = options.key?(:fiber) ? { fiber: options[:fiber] } : {} # Check if we're resuming from a pause if @paused_fiber&.alive? && options[:after] # Resume the paused fiber # Don't reset @success when resuming - preserve the state from the pause # Store the block for later execution @continuation_block = block if block_given? halted = pausable(pausable_options) { true } else @success = false # For normal execution (not pause/resume), default to success # The action block will override this if needed halted = pausable(pausable_options) { before(options[:after], &block) } if options[:before] end # After callbacks are only run if: # * An around callback didn't halt after yielding OR the run failed # * They're enabled or the run didn't succeed after if (!(@before_run && halted) || !@success) && (options[:after] || !@success) @before_run end # Transitions the current value of the state to that specified by the # transition. Once the state is persisted, it cannot be persisted again # until this transition is reset. # # == Example # # class Vehicle # state_machine do # event :ignite do # transition :parked => :idling # end # end # end # # vehicle = Vehicle.new # transition = StateMachines::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling) # transition.persist # # vehicle.state # => 'idling' def persist return if @persisted machine.write(object, :state, to) @persisted = true end # Rolls back changes made to the object's state via this transition. This # will revert the state back to the +from+ value. # # == Example # # class Vehicle # state_machine :initial => :parked do # event :ignite do # transition :parked => :idling # end # end # end # # vehicle = Vehicle.new # => # # transition = StateMachines::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling) # # # Persist the new state # vehicle.state # => "parked" # transition.persist # vehicle.state # => "idling" # # # Roll back to the original state # transition.rollback # vehicle.state # => "parked" def rollback reset machine.write(object, :state, from) end # Resets any tracking of which callbacks have already been run and whether # the state has already been persisted def reset @before_run = @persisted = @after_run = false @paused_fiber = nil @resuming = false @continuation_block = nil @fiber_thread_storage = nil end # Determines equality of transitions by testing whether the object, states, # and event involved in the transition are equal def ==(other) other.instance_of?(self.class) && other.object == object && other.machine == machine && other.from_name == from_name && other.to_name == to_name && other.event == event end # Generates a nicely formatted description of this transitions's contents. # # For example, # # transition = StateMachines::Transition.new(object, machine, :ignite, :parked, :idling) # transition # => # def inspect "#<#{self.class} #{%w[attribute event from from_name to to_name].map { |attr| "#{attr}=#{send(attr).inspect}" } * ' '}>" end # Checks whether this transition is currently paused. # Returns true if there is a paused fiber, false otherwise. def paused? @paused_fiber&.alive? || false end # Checks whether this transition has a paused fiber that can be resumed. # Returns true if there is a paused fiber, false otherwise. # # Note: The actual resuming happens automatically when run_callbacks is called # again on a transition with a paused fiber. def resumable? paused? end # Manually resumes the execution of a previously paused callback. # Returns true if the transition was successfully resumed and completed, # false if there was no paused fiber, and raises an exception if the # transition was halted. def resume!(&block) return false unless paused? # Store continuation block if provided @continuation_block = block if block_given? # Run the pausable block which will resume the fiber halted = pausable { true } # Return whether the transition completed successfully !halted end private # Runs a block that may get paused. If the block doesn't pause, then # execution will continue as normal. If the block gets paused, then it # will take care of switching the execution context when it's resumed. # # This will return true if the given block halts for a reason other than # getting paused. # # Options: # * :fiber - Whether to use fiber-based execution (default: true) def pausable(options = {}) # If fiber is disabled, use simple synchronous execution if options[:fiber] == false halted = !catch(:halt) do yield true end return halted end if @paused_fiber # Resume the paused fiber @resuming = true begin result = @paused_fiber.resume rescue StandardError => e # Clean up on exception @resuming = false @paused_fiber = nil raise e end @resuming = false # Handle different result types case result when Array if result[0] == :error # Exception occurred inside the fiber @paused_fiber = nil raise result[1] else # Normal completion with thread storage export @fiber_thread_storage = result[1] if result.length == 2 && result[1].is_a?(Hash) result_value = result[0] end else # Direct result value (paused or simple completion) result_value = result end # Check if fiber is still alive after resume if @paused_fiber.alive? # Still paused, keep the fiber true else # Fiber completed @paused_fiber = nil result_value == :halted end else # Capture current fiber's Thread.current storage to preserve object identity # This is needed for compatibility but has limitations with dynamic assignments parent_fiber_locals = Thread.current.keys.each_with_object({}) do |key, storage| storage[key] = Thread.current[key] end # Create a new fiber to run the block fiber = Fiber.new do # Restore parent's Thread.current storage with exact same object references parent_fiber_locals.each do |key, value| Thread.current[key] = value end # Mark that we're inside a pausable fiber Fiber.current.extend(StateMachines::PausableFiber) Fiber.current.state_machine_fiber_pausable = true begin halted = !catch(:halt) do yield true end # Export the final thread storage state along with the result thread_storage = Thread.current.keys.each_with_object({}) do |key, storage| storage[key] = Thread.current[key] end [halted ? :halted : :completed, thread_storage] rescue StandardError => e # Store the exception for re-raising [:error, e] ensure # Clean up the flag Fiber.current.state_machine_fiber_pausable = false end end # Run the fiber result = fiber.resume # Handle different result types case result when Array if result[0] == :error # Exception occurred @paused_fiber = nil raise result[1] else # Normal completion - check if we have thread storage @fiber_thread_storage = result[1] if result.length == 2 && result[1].is_a?(Hash) result_value = result[0] end else # Direct result value (shouldn't happen with our new code) result_value = result end # Save if paused if fiber.alive? @paused_fiber = fiber # Return true to indicate paused (treated as halted for flow control) true else # Fiber completed, return whether it was halted result_value == :halted end end end # Pauses the current callback execution. This should only occur within # around callbacks when the remainder of the callback will be executed at # a later point in time. def pause # Don't pause if we're in the middle of resuming return if @resuming # Only yield if we're actually inside a fiber created by pausable # We use a module extension to track this current_fiber = Fiber.current return unless current_fiber.respond_to?(:state_machine_fiber_pausable) && current_fiber.state_machine_fiber_pausable Fiber.yield # When we resume from the pause, execute the continuation block if present return unless @continuation_block && !@result action = { success: true }.merge(@continuation_block.call) @result = action[:result] @success = action[:success] @continuation_block = nil end # Runs the machine's +before+ callbacks for this transition. Only # callbacks that are configured to match the event, from state, and to # state will be invoked. # # Once the callbacks are run, they cannot be run again until this transition # is reset. def before(complete = true, index = 0, &block) return if @before_run callback = machine.callbacks[:before][index] if callback # Check if callback matches this transition using branch if callback.branch.matches?(object, context) if callback.type == :around # Around callback: need to handle recursively. Execution only gets # paused if: # * The block fails and the callback doesn't run on failures OR # * The block succeeds, but after callbacks are disabled (in which # case a continuation is stored for later execution) callback.call(object, context, self) do before(complete, index + 1, &block) pause if @success && !complete # If the block failed (success is false), we should halt # the around callback from continuing throw :halt unless @success end else # Normal before callback callback.call(object, context, self) # Continue with next callback before(complete, index + 1, &block) end else # Skip to next callback if it doesn't match before(complete, index + 1, &block) end else # No more callbacks, execute the action block if at the end if block_given? action = { success: true }.merge(yield) @result = action[:result] @success = action[:success] else # No action block provided, default to success @success = true end @before_run = true end end # Runs the machine's +after+ callbacks for this transition. Only # callbacks that are configured to match the event, from state, and to # state will be invoked. # # Once the callbacks are run, they cannot be run again until this transition # is reset. # # == Halting # # If any callback throws a :halt exception, it will be caught # and the callback chain will be automatically stopped. However, this # exception will not bubble up to the caller since +after+ callbacks # should never halt the execution of a +perform+. def after return if @after_run # Restore the fiber's thread storage to ensure consistency # This preserves Thread.current state from before/around callbacks to after callbacks @fiber_thread_storage.each { |key, value| Thread.current[key] = value } if @fiber_thread_storage catch(:halt) do type = @success ? :after : :failure machine.callbacks[type].each { |callback| callback.call(object, context, self) } end @after_run = true end # Gets a hash of the context defining this unique transition (including # event, from state, and to state). # # == Example # # machine = StateMachine.new(Vehicle) # transition = StateMachines::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling) # transition.context # => {:on => :ignite, :from => :parked, :to => :idling} def context @context ||= { on: event, from: from_name, to: to_name } end end end state_machines-0.100.4/lib/state_machines/transition_collection.rb000066400000000000000000000221541507333401300253350ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'options_validator' module StateMachines # Represents a collection of transitions in a state machine class TransitionCollection < Array # Whether to skip running the action for each transition's machine attr_reader :skip_actions # Whether to skip running the after callbacks attr_reader :skip_after # Whether transitions should wrapped around a transaction block attr_reader :use_transactions # Options passed to the collection attr_reader :options # Creates a new collection of transitions that can be run in parallel. Each # transition *must* be for a different attribute. # # Configuration options: # * :actions - Whether to run the action configured for each transition # * :after - Whether to run after callbacks # * :transaction - Whether to wrap transitions within a transaction def initialize(transitions = [], options = {}) super(transitions) # Determine the validity of the transitions as a whole @valid = all? reject!(&:!) attributes = map(&:attribute).uniq raise ArgumentError, 'Cannot perform multiple transitions in parallel for the same state machine attribute' if attributes.length != length StateMachines::OptionsValidator.assert_valid_keys!(options, :actions, :after, :use_transactions, :fiber) options = { actions: true, after: true, use_transactions: true }.merge(options) @skip_actions = !options[:actions] @skip_after = !options[:after] @use_transactions = options[:use_transactions] @options = options # Reset transitions when creating a new collection # But preserve paused transitions to allow resuming each do |transition| transition.reset unless transition.paused? end end # Runs each of the collection's transitions in parallel. # # All transitions will run through the following steps: # 1. Before callbacks # 2. Persist state # 3. Invoke action # 4. After callbacks (if configured) # 5. Rollback (if action is unsuccessful) # # If a block is passed to this method, that block will be called instead # of invoking each transition's action. def perform(&block) reset if valid? if use_event_attributes? && !block_given? each do |transition| transition.transient = true transition.machine.write(object, :event_transition, transition) end run_actions else within_transaction do catch(:halt) { run_callbacks(&block) } rollback unless success? end end end if actions.length == 1 && results.include?(actions.first) results[actions.first] else success? end end protected attr_reader :results # :nodoc: private # Is this a valid set of transitions? If the collection was creating with # any +false+ values for transitions, then the the collection will be # marked as invalid. def valid? @valid end # Did each transition perform successfully? This will only be true if the # following requirements are met: # * No +before+ callbacks halt # * All actions run successfully (always true if skipping actions) def success? @success end # Gets the object being transitioned def object first.object end # Gets the list of actions to run. If configured to skip actions, then # this will return an empty collection. def actions empty? ? [nil] : map(&:action).uniq end # Determines whether an event attribute be used to trigger the transitions # in this collection or whether the transitions be run directly *outside* # of the action. def use_event_attributes? !skip_actions && !skip_after && actions.all? && actions.length == 1 && first.machine.action_hook? end # Resets any information tracked from previous attempts to perform the # collection def reset @results = {} @success = false end # Runs each transition's callbacks recursively. Once all before callbacks # have been executed, the transitions will then be persisted and the # configured actions will be run. # # If any transition fails to run its callbacks, :halt will be thrown. def run_callbacks(index = 0, &block) if (transition = self[index]) # Pass through any options that affect callback execution (e.g., fiber: false) callback_options = { after: !skip_after } callback_options[:fiber] = options[:fiber] if options.key?(:fiber) callback_result = transition.run_callbacks(callback_options) do run_callbacks(index + 1, &block) { result: results[transition.action], success: success? } end # If we're skipping after callbacks and the transition is paused, # consider it successful (the pause was intentional) @success = true if skip_after && transition.paused? throw :halt unless callback_result else persist run_actions(&block) end end # Transitions the current value of the object's states to those specified by # each transition def persist each(&:persist) end # Runs the actions for each transition. If a block is given method, then it # will be called instead of invoking each transition's action. # # The results of the actions will be used to determine #success?. def run_actions catch_exceptions do @success = if block_given? result = yield actions.each { |action| results[action] = result } !!result else actions.compact.each { |action| !skip_actions && (results[action] = object.send(action)) } results.values.all? end end end # Rolls back changes made to the object's states via each transition def rollback each(&:rollback) end # Wraps the given block with a rescue handler so that any exceptions that # occur will automatically result in the transition rolling back any changes # that were made to the object involved. def catch_exceptions yield rescue StandardError rollback raise end # Runs a block within a transaction for the object being transitioned. If # transactions are disabled, then this is a no-op. def within_transaction if use_transactions && !empty? first.within_transaction do yield success? end else yield end end end # Represents a collection of transitions that were generated from attribute- # based events class AttributeTransitionCollection < TransitionCollection def initialize(transitions = [], options = {}) # :nodoc: super(transitions, { use_transactions: false, actions: false }.merge(options)) end private # Hooks into running transition callbacks so that event / event transition # attributes can be properly updated def run_callbacks(index = 0) if index.zero? # Clears any traces of the event attribute to prevent it from being # evaluated multiple times if actions are nested each do |transition| transition.machine.write(object, :event, nil) transition.machine.write(object, :event_transition, nil) end # Clear stored transitions hash for new cycle (issue #91) if !empty? && (obj = first.object) obj.instance_variable_set(:@_state_machine_event_transitions, nil) end # Rollback only if exceptions occur during before callbacks begin super rescue StandardError rollback unless @before_run @success = nil # mimics ActiveRecord.save behavior on rollback raise end # Persists transitions on the object if partial transition was successful. # This allows us to reference them later to complete the transition with # after callbacks. if skip_after && success? each { |transition| transition.machine.write(object, :event_transition, transition) } # Store transitions in a hash by machine name to avoid overwriting (issue #91) unless empty? transitions_by_machine = object.instance_variable_get(:@_state_machine_event_transitions) || {} each { |transition| transitions_by_machine[transition.machine.name] = transition } object.instance_variable_set(:@_state_machine_event_transitions, transitions_by_machine) end end else super end end # Tracks that before callbacks have now completed def persist @before_run = true super end # Resets callback tracking def reset super @before_run = false end # Resets the event attribute so it can be re-evaluated if attempted again def rollback super each { |transition| transition.machine.write(object, :event, transition.event) unless transition.transient? } end end end state_machines-0.100.4/lib/state_machines/version.rb000066400000000000000000000001161507333401300224070ustar00rootroot00000000000000# frozen_string_literal: true module StateMachines VERSION = '0.100.4' end state_machines-0.100.4/release-please-config.json000066400000000000000000000003231507333401300216650ustar00rootroot00000000000000{ "packages": { ".": { "release-type": "ruby", "package-name": "state_machines", "version-file": "lib/state_machines/version.rb", "bump-patch-for-minor-pre-major": true } } } state_machines-0.100.4/scripts/000077500000000000000000000000001507333401300163315ustar00rootroot00000000000000state_machines-0.100.4/scripts/update_coss_version.sh000077500000000000000000000010041507333401300227410ustar00rootroot00000000000000#!/bin/bash # Simple shell script to update version in coss.toml set -e VERSION=$(grep "VERSION = " lib/state_machines/version.rb | sed "s/.*'\(.*\)'.*/\1/") if [ -z "$VERSION" ]; then echo "Error: Could not extract version from lib/state_machines/version.rb" exit 1 fi if [ ! -f "coss.toml" ]; then echo "Error: coss.toml not found" exit 1 fi # Update version in coss.toml sed -i.bak "s/^version = .*/version = \"$VERSION\"/" coss.toml rm -f coss.toml.bak echo "Updated coss.toml version to $VERSION" state_machines-0.100.4/state_machines.gemspec000066400000000000000000000021511507333401300211750ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'lib/state_machines/version' Gem::Specification.new do |spec| spec.name = 'state_machines' spec.version = StateMachines::VERSION spec.authors = ['Abdelkader Boudih', 'Aaron Pfeifer'] spec.email = %w[terminale@gmail.com] spec.summary = 'State machines for attributes' spec.description = 'Adds support for creating state machines for attributes on any Ruby class' spec.homepage = 'https://github.com/state-machines/state_machines' spec.license = 'MIT' spec.metadata['changelog_uri'] = 'https://github.com/state-machines/state_machines/blob/master/CHANGELOG.md' spec.metadata['homepage_uri'] = spec.homepage spec.metadata['source_code_uri'] = spec.homepage spec.required_ruby_version = '>= 3.2.0' spec.files = Dir.glob('{lib}/**/*') + %w[LICENSE.txt README.md] spec.require_paths = ['lib'] spec.add_development_dependency 'bundler', '>= 1.7.6' spec.add_development_dependency 'minitest', '>= 5.4' spec.add_development_dependency 'rake' spec.metadata['rubygems_mfa_required'] = 'true' end state_machines-0.100.4/test/000077500000000000000000000000001507333401300156215ustar00rootroot00000000000000state_machines-0.100.4/test/files/000077500000000000000000000000001507333401300167235ustar00rootroot00000000000000state_machines-0.100.4/test/files/integrations/000077500000000000000000000000001507333401300214315ustar00rootroot00000000000000state_machines-0.100.4/test/files/integrations/event_on_failure_integration.rb000066400000000000000000000004431507333401300277060ustar00rootroot00000000000000# frozen_string_literal: true module EventOnFailureIntegration include StateMachines::Integrations::Base def invalidate(object, _attribute, message, values = []) (object.errors ||= []) << generate_message(message, values) end def reset(object) object.errors = [] end end state_machines-0.100.4/test/files/integrations/vehicle.rb000066400000000000000000000002341507333401300233740ustar00rootroot00000000000000# frozen_string_literal: true module VehicleIntegration include StateMachines::Integrations::Base def self.matching_ancestors [Vehicle] end end state_machines-0.100.4/test/files/models/000077500000000000000000000000001507333401300202065ustar00rootroot00000000000000state_machines-0.100.4/test/files/models/auto_shop.rb000066400000000000000000000012271507333401300225360ustar00rootroot00000000000000# frozen_string_literal: true class AutoShop attr_accessor :num_customers def initialize @num_customers = 0 super end state_machine initial: :available do after_transition available: any, do: :increment_customers after_transition busy: any, do: :decrement_customers event :tow_vehicle do transition available: :busy end event :fix_vehicle do transition busy: :available end end # Increments the number of customers in service def increment_customers self.num_customers += 1 end # Decrements the number of customers in service def decrement_customers self.num_customers -= 1 end end state_machines-0.100.4/test/files/models/autonomous_drone.rb000066400000000000000000000071731507333401300241430ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'starfleet_ship' # Autonomous drone with async capabilities # Demonstrates async: true parameter for autonomous systems class AutonomousDrone < StarfleetShip attr_accessor :teleporter_status, :teleporter_charge_level, :callback_log, :autonomous_mode def initialize(attributes = {}) attributes = { teleporter_status: :offline, teleporter_charge_level: 0, callback_log: [], autonomous_mode: true }.merge(attributes) attributes.each { |attr, value| send("#{attr}=", value) } super end # Override main status machine to be async (autonomous operation) state_machine :status, async: true do before_transition any => :flying do |drone| drone.callback_log << 'Autonomous flight sequence initiated...' end after_transition any => :flying do |drone| drone.callback_log << 'Drone airborne - autonomous navigation active!' end event :launch do transition docked: :flying end event :enter_warp do transition flying: :warping end event :exit_warp do transition warping: :flying end event :land do transition flying: :docked end end # Teleporter system with async capabilities (takes 1 second to turn on) state_machine :teleporter_status, initial: :offline, async: true do before_transition offline: :charging do |drone| drone.callback_log << 'Initializing quantum teleporter matrix...' drone.teleporter_charge_level = 0 end after_transition charging: :ready do |drone| drone.callback_log << 'Teleporter matrix stabilized and ready!' drone.teleporter_charge_level = 100 end before_transition ready: :teleporting do |drone| drone.callback_log << 'Engaging quantum teleportation beam...' end after_transition teleporting: :ready do |drone| drone.callback_log << 'Quantum teleportation sequence complete!' end event :power_up do transition offline: :charging end event :charge_complete do transition charging: :ready end event :teleport do transition ready: :teleporting end event :teleport_complete do transition teleporting: :ready end event :shutdown do transition %i[charging ready teleporting] => :offline end end # Use inherited weapons machine from StarfleetShip (remains sync for safety) # The inherited :weapons machine has events: arm, target, fire, reload, stand_down # Override shields to be async state_machine :shields, async: true do end # Simulate the 1-second teleporter startup process def start_teleporter_sequence power_up_teleporter_status! # Simulate 1-second startup time for quantum matrix stabilization sleep(0.1) # Reduced for testing charge_complete_teleporter_status! end # Perform autonomous teleportation sequence def perform_teleportation return false unless teleporter_status_ready? teleport_teleporter_status! # Simulate quantum teleportation process sleep(0.05) # Brief teleport time teleport_complete_teleporter_status! true end # Autonomous launch sequence using async capabilities def autonomous_launch_sequence if respond_to?(:fire_event_async) fire_event_async(:launch) else launch! end end # Emergency shutdown for autonomous systems def emergency_shutdown super # Call StarfleetShip emergency_shutdown shutdown_teleporter_status! if teleporter_status_ready? || teleporter_status_charging? self.autonomous_mode = false end # Check if drone is operating autonomously def autonomous? autonomous_mode end end state_machines-0.100.4/test/files/models/car.rb000066400000000000000000000006221507333401300213000ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'vehicle' class Car < Vehicle state_machine do event :reverse do transition %i[parked idling first_gear] => :backing_up end event :park do transition backing_up: :parked end event :idle do transition backing_up: :idling end event :shift_up do transition backing_up: :first_gear end end end state_machines-0.100.4/test/files/models/driver.rb000066400000000000000000000004111507333401300220220ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'model_base' class Driver < ModelBase state_machine :status, initial: :parked do event :park do transition idling: :parked end event :ignite do transition parked: :idling end end end state_machines-0.100.4/test/files/models/hybrid_car.rb000066400000000000000000000026321507333401300226440ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'vehicle' class HybridCar < Vehicle attr_accessor :propulsion_mode, :driving_profile, :target_year, :energy_source, :universe, :destination state_machine :propulsion_mode, initial: :gas do event :go_green do transition electric: :electric transition flux_capacitor: :electric transition gas: :electric end event :go_gas do transition electric: :gas transition flux_capacitor: :gas transition gas: :gas end event :go_back_in_time do transition electric: :flux_capacitor transition flux_capacitor: :flux_capacitor transition gas: :flux_capacitor end event :teleport do transition electric: :teleported transition flux_capacitor: :teleported transition gas: :teleported end end def go_green(driving_profile = nil) self.driving_profile = driving_profile if driving_profile super() end def go_gas(driving_profile:) self.driving_profile = driving_profile super() end def go_back_in_time(target_year, _flux_capacitor_setting = {}, driving_profile:) self.target_year = target_year self.driving_profile = driving_profile super() end def teleport(destination, energy_settings, universe_settings) self.destination = destination self.energy_source = energy_settings self.universe = universe_settings super() end end state_machines-0.100.4/test/files/models/model_base.rb000066400000000000000000000001371507333401300226260ustar00rootroot00000000000000# frozen_string_literal: true class ModelBase def save @saved = true self end end state_machines-0.100.4/test/files/models/motorcycle.rb000066400000000000000000000004371507333401300227170ustar00rootroot00000000000000# frozen_string_literal: true require 'files/models/vehicle' class Motorcycle < Vehicle def self.example_class_method(args = {}); end state_machine initial: :idling do state :first_gear do def decibels 1.0 end example_class_method end end end state_machines-0.100.4/test/files/models/starfleet_ship.rb000066400000000000000000000131721507333401300235530ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'model_base' class StarfleetShip < ModelBase attr_accessor :warp_core_temperature, :shield_strength, :crew_count, :captain_on_bridge, :red_alert_triggered, :emergency_protocols_active, :docking_sequence_complete def initialize(attributes = {}) attributes = { warp_core_temperature: 1000, shield_strength: 100, crew_count: 430, captain_on_bridge: true, red_alert_triggered: false, emergency_protocols_active: false, docking_sequence_complete: false }.merge(attributes) attributes.each { |attr, value| send("#{attr}=", value) } super() end # Main ship status state machine state_machine :status, initial: :docked do before_transition docked: any, do: :departure_checklist before_transition any => :warp, do: :engage_warp_core after_transition on: :red_alert, do: :sound_battle_stations after_transition on: :all_clear, do: :stand_down_alert after_transition any => :docked, do: :shutdown_warp_core event :undock do transition docked: :impulse end event :engage_warp do transition impulse: :warp, if: :warp_core_stable? end event :drop_to_impulse do transition warp: :impulse end event :dock do transition impulse: :docked end event :red_alert do transition %i[impulse warp] => :battle_stations end event :all_clear do transition battle_stations: :impulse end event :emergency_stop do transition %i[warp impulse] => :emergency end end # Shield system state machine state_machine :shields, initial: :down do before_transition down: :up, do: :power_up_shields after_transition up: :down, do: :reroute_power after_transition on: :modulate, do: :adjust_frequency event :raise_shields do transition down: :up end event :lower_shields do transition up: :down end event :modulate do transition up: :up # Self-transition to adjust frequency end event :overload do transition up: :down end end # Weapons system state machine state_machine :weapons, initial: :standby, namespace: 'weapons' do before_transition standby: any, do: :arm_weapons_systems after_transition on: :fire, do: :log_weapons_discharge after_transition on: :target, do: :lock_onto_target event :arm do transition standby: :armed end event :target do transition armed: :targeted end event :fire do transition targeted: :firing end event :reload do transition firing: :armed end event :stand_down do transition %i[armed targeted firing] => :standby end end # Custom methods that can trigger events indirectly def engage_combat_mode red_alert! raise_shields! arm_weapons! end def emergency_shutdown emergency_stop! lower_shields! stand_down_weapons! end def begin_battle_sequence if shields_down? || weapons_standby? raise_shields! arm_weapons! end target_weapons! end # Helper method for shield state def shields_down? shields_name == :down end # Fire all weapons (triggers multiple events) def fire_all_weapons fire_weapons! if weapons_targeted? reload_weapons! if weapons_firing? end private def departure_checklist # Pre-flight checks end def engage_warp_core self.warp_core_temperature += 500 end def shutdown_warp_core self.warp_core_temperature = 1000 end def sound_battle_stations self.red_alert_triggered = true end def stand_down_alert self.red_alert_triggered = false end def power_up_shields # Shield activation sequence end def reroute_power # Power management end def adjust_frequency # Shield modulation end def arm_weapons_systems # Weapons activation end def log_weapons_discharge # Tactical log entry end def lock_onto_target # Targeting computer engagement end def warp_core_stable? warp_core_temperature < 1800 end end # Experimental Moroccan ship class for testing guard arguments - RMNS Atlas Monkey! 🇲🇦🐒🚀 class RmnsAtlasMonkey < StarfleetShip # Override the engage_warp event to demonstrate emergency override state_machine :status do event :engage_warp do # Emergency override allows warp even if core is unstable transition impulse: :warp, if: lambda { |ship, *args| ship.send(:warp_core_stable?) || args.include?(:emergency_override) } end # Event with mixed guard types event :emergency_warp do # Multi-param lambda guard (new behavior) - needs to be first for specificity transition impulse: :warp, if: lambda { |_ship, *args| # Check if first arg is authorization code and second is :confirmed args.length >= 2 && args[0] == 'omega-3-7' && args[1] == :confirmed } # Symbol guard (existing behavior) transition impulse: :warp, if: :warp_core_stable? # Single-param lambda guard (existing behavior) transition impulse: :warp, if: ->(ship) { ship.captain_on_bridge } end end # Add new weapons event to demonstrate target-specific firing state_machine :weapons do event :fire_at_target do transition targeted: :firing, if: lambda { |ship, target_type, *args| case target_type when :asteroid true # Can always fire at asteroids when :enemy_ship ship.shields_name == :up # Need shields up for combat when :photon_torpedo args.include?(:full_spread) # Special firing pattern for torpedoes else false end } end end end state_machines-0.100.4/test/files/models/thread_storage.rb000066400000000000000000000012401507333401300235230ustar00rootroot00000000000000class ThreadStorage class << self def store Thread.current[:store] ||= [] end def flush! Thread.current[:store] = nil end end state_machine :state, initial: :stopped do event :start do transition stopped: :running end before_transition do ThreadStorage.store << :before_transition end after_transition do ThreadStorage.store << :after_transition end around_transition do |_, _, block| ThreadStorage.store << :before_around_transition block.call ThreadStorage.store << :after_around_transition end end attr_accessor :state def initialize super end end state_machines-0.100.4/test/files/models/traffic_light.rb000066400000000000000000000013701507333401300233410ustar00rootroot00000000000000# frozen_string_literal: true class TrafficLight state_machine initial: :stop do event :cycle do transition stop: :proceed, proceed: :caution, caution: :stop end state :stop do def color(transform) value = +'red' if block_given? yield value else value.send(transform) end value end end state all - :proceed do def capture_violations? true end end state :proceed do def color(_transform) 'green' end def capture_violations? false end end state :caution do def color(_transform) 'yellow' end end end def color(transform = :to_s) super end end state_machines-0.100.4/test/files/models/vehicle.rb000066400000000000000000000061641507333401300221610ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'model_base' require_relative 'auto_shop' class Vehicle < ModelBase attr_accessor :auto_shop, :seatbelt_on, :insurance_premium, :force_idle, :callbacks, :saved, :time_elapsed, :last_transition_args def initialize(attributes = {}) attributes = { auto_shop: AutoShop.new, seatbelt_on: false, insurance_premium: 50, force_idle: false, callbacks: [], saved: false }.merge(attributes) attributes.each { |attr, value| send("#{attr}=", value) } super() end # Defines the state machine for the state of the vehicled state_machine initial: ->(vehicle) { vehicle.force_idle ? :idling : :parked }, action: :save do before_transition { |vehicle, transition| vehicle.last_transition_args = transition.args } before_transition parked: any, do: :put_on_seatbelt before_transition any => :stalled, :do => :increase_insurance_premium after_transition any => :parked, :do => ->(vehicle) { vehicle.seatbelt_on = false } after_transition on: :crash, do: :tow after_transition on: :repair, do: :fix # Callback tracking for initial state callbacks after_transition any => :parked, :do => ->(vehicle) { vehicle.callbacks << 'before_enter_parked' } before_transition any => :idling, :do => ->(vehicle) { vehicle.callbacks << 'before_enter_idling' } around_transition do |vehicle, _transition, block| time = Time.now block.call vehicle.time_elapsed = Time.now - time end event all do transition locked: :parked end event :park do transition %i[idling first_gear] => :parked end event :ignite do transition stalled: :stalled transition parked: :idling end event :idle do transition first_gear: :idling end event :shift_up do transition idling: :first_gear, first_gear: :second_gear, second_gear: :third_gear end event :shift_down do transition third_gear: :second_gear transition second_gear: :first_gear end event :crash do transition %i[first_gear second_gear third_gear] => :stalled, :if => ->(vehicle) { vehicle.auto_shop.available? } end event :repair do transition stalled: :parked, if: :auto_shop_busy? end end state_machine :insurance_state, initial: :inactive, namespace: 'insurance' do event :buy do transition inactive: :active end event :cancel do transition active: :inactive end end def save super end def new_record? @saved == false end def park super end # Tows the vehicle to the auto shop def tow auto_shop.tow_vehicle end # Fixes the vehicle; it will no longer be in the auto shop def fix auto_shop.fix_vehicle end def decibels 0.0 end private # Safety first! Puts on our seatbelt def put_on_seatbelt self.seatbelt_on = true end # We crashed! Increase the insurance premium on the vehicle def increase_insurance_premium self.insurance_premium += 100 end # Is the auto shop currently servicing another customer? def auto_shop_busy? auto_shop.busy? end end state_machines-0.100.4/test/files/node.rb000066400000000000000000000001621507333401300201740ustar00rootroot00000000000000# frozen_string_literal: true class Node < Struct.new(:name, :value, :machine) def context yield end end state_machines-0.100.4/test/files/switch.rb000066400000000000000000000004011507333401300205440ustar00rootroot00000000000000# frozen_string_literal: true class Switch def self.name @name ||= "Switch_#{rand(1_000_000)}" end state_machine do event :turn_on do transition all => :on end event :turn_off do transition all => :off end end end state_machines-0.100.4/test/functional/000077500000000000000000000000001507333401300177635ustar00rootroot00000000000000state_machines-0.100.4/test/functional/auto_shop_available_test.rb000066400000000000000000000006641507333401300253560ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/auto_shop' class AutoShopAvailableTest < Minitest::Test def setup @auto_shop = AutoShop.new end def test_should_be_in_available_state assert_equal 'available', @auto_shop.state end def test_should_allow_tow_vehicle assert @auto_shop.tow_vehicle end def test_should_not_allow_fix_vehicle refute @auto_shop.fix_vehicle end end state_machines-0.100.4/test/functional/auto_shop_busy_test.rb000066400000000000000000000010531507333401300244110ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/auto_shop' class AutoShopBusyTest < Minitest::Test def setup @auto_shop = AutoShop.new @auto_shop.tow_vehicle end def test_should_be_in_busy_state assert_equal 'busy', @auto_shop.state end def test_should_have_incremented_number_of_customers assert_equal 1, @auto_shop.num_customers end def test_should_not_allow_tow_vehicle refute @auto_shop.tow_vehicle end def test_should_allow_fix_vehicle assert @auto_shop.fix_vehicle end end state_machines-0.100.4/test/functional/car_backing_up_test.rb000066400000000000000000000014151507333401300242770ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/car' class CarBackingUpTest < Minitest::Test def setup @car = Car.new @car.reverse end def test_should_be_in_backing_up_state assert_equal 'backing_up', @car.state end def test_should_allow_park assert @car.park end def test_should_not_allow_ignite refute @car.ignite end def test_should_allow_idle assert @car.idle end def test_should_allow_shift_up assert @car.shift_up end def test_should_not_allow_shift_down refute @car.shift_down end def test_should_not_allow_crash refute @car.crash end def test_should_not_allow_repair refute @car.repair end def test_should_not_allow_reverse refute @car.reverse end end state_machines-0.100.4/test/functional/car_test.rb000066400000000000000000000017741507333401300221250ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/car' class CarTest < StateMachinesTest def setup @car = Car.new end def test_should_be_in_parked_state assert_sm_state(@car, :parked) end def test_should_not_have_the_seatbelt_on refute @car.seatbelt_on end def test_should_not_allow_park assert_sm_cannot_transition(@car, :park) end def test_should_allow_ignite assert_sm_transition(@car, :ignite, :idling) end def test_should_not_allow_idle assert_sm_cannot_transition(@car, :idle) end def test_should_not_allow_shift_up assert_sm_cannot_transition(@car, :shift_up) end def test_should_not_allow_shift_down assert_sm_cannot_transition(@car, :shift_down) end def test_should_not_allow_crash assert_sm_cannot_transition(@car, :crash) end def test_should_not_allow_repair assert_sm_cannot_transition(@car, :repair) end def test_should_allow_reverse assert_sm_can_transition(@car, :reverse) end end state_machines-0.100.4/test/functional/driver_default_nonstandard_test.rb000066400000000000000000000004621507333401300267430ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/driver' class DriverNonstandardTest < Minitest::Test def setup @driver = Driver.new @events = Driver.state_machine.events end def test_should_have assert_equal 1, @events.transitions_for(@driver).size end end state_machines-0.100.4/test/functional/event_guard_arguments_integration_test.rb000066400000000000000000000103321507333401300303410ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require_relative '../files/models/starfleet_ship' # Integration test for event guard arguments functionality # Uses the RMNS Atlas Monkey, our brave Moroccan test ship built on vibes and shukrans. class EventGuardArgumentsIntegrationTest < StateMachinesTest include StateMachines::TestHelper def setup @ship = RmnsAtlasMonkey.new end def test_event_arguments_allow_emergency_override_in_guards # Normal operation: should work when warp core is stable @ship.undock @ship.warp_core_temperature = 1500 # Stable assert_sm_can_transition(@ship, :engage_warp, machine_name: :status) assert @ship.engage_warp assert_sm_state(@ship, :warp, machine_name: :status) # Reset ship @ship.drop_to_impulse assert_sm_state(@ship, :impulse, machine_name: :status) # Unstable core: should fail without override @ship.warp_core_temperature = 2000 # Unstable assert_sm_cannot_transition(@ship, :engage_warp, machine_name: :status) refute @ship.engage_warp assert_sm_state(@ship, :impulse, machine_name: :status) # Unstable core with emergency override: should succeed assert @ship.engage_warp(:emergency_override) assert_sm_state(@ship, :warp, machine_name: :status) end def test_event_arguments_support_conditional_logic_in_guards # Setup: arm and target weapons @ship.arm_weapons @ship.target_weapons # Test firing at asteroid (always allowed) assert @ship.fire_at_target_weapons(:asteroid) assert_sm_state(@ship, :firing, machine_name: :weapons) # Reset weapons @ship.reload_weapons @ship.target_weapons # Test firing at enemy ship without shields (should fail) # Shields start as :down, so this should fail refute @ship.fire_at_target_weapons(:enemy_ship) assert_sm_state(@ship, :targeted, machine_name: :weapons) # Test firing at enemy ship with shields (should succeed) @ship.raise_shields assert @ship.fire_at_target_weapons(:enemy_ship) assert_sm_state(@ship, :firing, machine_name: :weapons) # Reset weapons @ship.reload_weapons @ship.target_weapons # Test photon torpedo without full spread (should fail) refute @ship.fire_at_target_weapons(:photon_torpedo) assert_sm_state(@ship, :targeted, machine_name: :weapons) # Test photon torpedo with full spread (should succeed) assert @ship.fire_at_target_weapons(:photon_torpedo, :full_spread) assert_sm_state(@ship, :firing, machine_name: :weapons) end def test_backward_compatibility_with_existing_guards # Use the original StarfleetShip to verify existing guards still work original_ship = StarfleetShip.new original_ship.undock original_ship.warp_core_temperature = 1500 # Stable # The existing warp_core_stable? guard should still work assert_sm_can_transition(original_ship, :engage_warp, machine_name: :status) assert original_ship.engage_warp assert_sm_state(original_ship, :warp, machine_name: :status) original_ship.drop_to_impulse original_ship.warp_core_temperature = 2000 # Unstable # Should fail when core is unstable assert_sm_cannot_transition(original_ship, :engage_warp, machine_name: :status) refute original_ship.engage_warp assert_sm_state(original_ship, :impulse, machine_name: :status) end def test_mixed_guard_types_with_and_without_event_arguments @ship.undock # Test symbol guard path @ship.warp_core_temperature = 1500 assert @ship.emergency_warp assert_sm_state(@ship, :warp, machine_name: :status) # Reset @ship.drop_to_impulse @ship.warp_core_temperature = 2000 # Test single-param lambda guard path @ship.captain_on_bridge = true assert @ship.emergency_warp assert_sm_state(@ship, :warp, machine_name: :status) # Reset @ship.drop_to_impulse @ship.captain_on_bridge = false # Test multi-param lambda guard path (should fail without proper args) refute @ship.emergency_warp('wrong-code') assert_sm_state(@ship, :impulse, machine_name: :status) # Test multi-param lambda guard path (should succeed with proper args) assert @ship.emergency_warp('omega-3-7', :confirmed) assert_sm_state(@ship, :warp, machine_name: :status) end end state_machines-0.100.4/test/functional/hybrid_car_test.rb000066400000000000000000000044371507333401300234650ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/hybrid_car' class HybridCarTest < Minitest::Test def setup @hybrid_car = HybridCar.new end def test_should_accept_positional_argument assert @hybrid_car.go_green(:eco) assert_predicate @hybrid_car, :electric? assert_equal 'electric', @hybrid_car.propulsion_mode assert_equal :eco, @hybrid_car.driving_profile end def test_should_accept_keyword_argument assert @hybrid_car.go_gas(driving_profile: :sport) assert_predicate @hybrid_car, :gas? assert_equal 'gas', @hybrid_car.propulsion_mode assert_equal :sport, @hybrid_car.driving_profile end def test_should_accept_positional_and_keyword_arguments assert @hybrid_car.go_back_in_time(1995, driving_profile: '1.21 gigawatts') assert_predicate @hybrid_car, :flux_capacitor? assert_equal 1995, @hybrid_car.target_year assert_equal 'flux_capacitor', @hybrid_car.propulsion_mode assert_equal '1.21 gigawatts', @hybrid_car.driving_profile end def test_should_accept_positional_arguments_in_unsafe_method assert @hybrid_car.go_green!(:eco) assert_predicate @hybrid_car, :electric? assert_equal 'electric', @hybrid_car.propulsion_mode assert_equal :eco, @hybrid_car.driving_profile end def test_should_accept_keyword_argument_in_unsafe_method assert @hybrid_car.go_gas!(driving_profile: :sport) assert_predicate @hybrid_car, :gas? assert_equal 'gas', @hybrid_car.propulsion_mode assert_equal :sport, @hybrid_car.driving_profile end def test_should_accept_positional_and_keyword_arguments_in_unsafe_method assert @hybrid_car.go_back_in_time!(1995, driving_profile: '1.21 gigawatts') assert_predicate @hybrid_car, :flux_capacitor? assert_equal 1995, @hybrid_car.target_year assert_equal 'flux_capacitor', @hybrid_car.propulsion_mode assert_equal '1.21 gigawatts', @hybrid_car.driving_profile end def test_should_accept_hashes_as_option assert @hybrid_car.teleport('wakanda', { engine: :nuclear }, { world: :parallel }) assert_equal 'wakanda', @hybrid_car.destination assert_equal({ engine: :nuclear }, @hybrid_car.energy_source) assert_equal({ world: :parallel }, @hybrid_car.universe) end end state_machines-0.100.4/test/functional/modern_callback_api_test.rb000066400000000000000000000173531507333401300253110ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ModernCallbackApiTest < StateMachinesTest def setup @model = Class.new do attr_accessor :submitted_at, :approved_at, :rejected_at, :published_at, :callback_log def initialize @callback_log = [] super end state_machine :status, initial: :draft do event :submit do transition draft: :pending end event :approve do transition pending: :approved end event :reject do transition pending: :rejected end event :publish do transition approved: :published end # Modern keyword argument style - cleaner and more explicit before_transition(from: :draft, to: :pending, do: :validate_submission) before_transition(from: :pending, to: :approved, if: :meets_criteria?, do: :validate_approval) # Pattern matching in callback blocks showcases Ruby 3.2+ features before_transition do |object, transition| case [transition.from_name, transition.to_name, transition.event] when %i[draft pending submit] object.submitted_at = Time.now object.callback_log << 'submitting for review' when %i[pending approved approve] object.approved_at = Time.now object.callback_log << 'approving submission' when %i[pending rejected reject] object.rejected_at = Time.now object.callback_log << 'rejecting submission' when %i[approved published publish] object.published_at = Time.now object.callback_log << 'publishing content' else object.callback_log << "transition: #{transition.from} -> #{transition.to} via #{transition.event}" end end # Mixed style: legacy positional args with modern pattern matching after_transition :notify_stakeholders do |object, transition| # Modern pattern matching for notification logic object.callback_log << case transition.to_name when :published 'sending publication notifications' when :rejected 'sending rejection notifications' else "status changed to #{transition.to_name}" end end # Modern keyword style with multiple conditions around_transition(from: :pending, on: %i[approve reject]) do |object, _transition, block| object.callback_log << 'starting review process' start_time = Time.now block.call # Execute the transition duration = Time.now - start_time object.callback_log << "review completed in #{duration.round(2)} seconds" end end private def validate_submission callback_log << 'validating submission' end def meets_criteria? callback_log << 'checking approval criteria' true # Simplified for test end def validate_approval callback_log << 'validating approval' end def notify_stakeholders callback_log << 'notifying stakeholders' end end @workflow = @model.new end def test_should_support_modern_keyword_arguments assert_equal 'draft', @workflow.status assert_empty @workflow.callback_log # Test submit transition with modern callbacks @workflow.submit assert_equal 'pending', @workflow.status assert_includes @workflow.callback_log, 'validating submission' assert_includes @workflow.callback_log, 'submitting for review' assert_includes @workflow.callback_log, 'notifying stakeholders' assert_includes @workflow.callback_log, 'status changed to pending' refute_nil @workflow.submitted_at end def test_should_support_pattern_matching_in_callbacks @workflow.submit @workflow.approve assert_equal 'approved', @workflow.status assert_includes @workflow.callback_log, 'checking approval criteria' assert_includes @workflow.callback_log, 'validating approval' assert_includes @workflow.callback_log, 'approving submission' assert_includes @workflow.callback_log, 'starting review process' assert(@workflow.callback_log.any? { |log| log.start_with?('review completed in') }) refute_nil @workflow.approved_at end def test_should_support_mixed_callback_styles @workflow.submit @workflow.approve @workflow.publish assert_equal 'published', @workflow.status assert_includes @workflow.callback_log, 'publishing content' assert_includes @workflow.callback_log, 'sending publication notifications' refute_nil @workflow.published_at end def test_should_maintain_backward_compatibility_with_legacy_callbacks model = Class.new do attr_accessor :callback_log def initialize @callback_log = [] super end state_machine :status, initial: :draft do event :submit do transition draft: :pending end # All legacy callback styles should still work before_transition :log_before_any before_transition draft: :pending, do: :log_submit before_transition on: :submit, do: [:log_event_submit] before_transition from: :draft, to: :pending, if: :should_log?, do: :log_conditional end private def log_before_any callback_log << 'before any' end def log_submit callback_log << 'submit transition' end def log_event_submit callback_log << 'submit event' end def log_conditional callback_log << 'conditional callback' end def should_log? true end end workflow = model.new workflow.submit assert_equal 'pending', workflow.status assert_includes workflow.callback_log, 'before any' assert_includes workflow.callback_log, 'submit transition' assert_includes workflow.callback_log, 'submit event' assert_includes workflow.callback_log, 'conditional callback' end def test_should_support_block_only_callbacks model = Class.new do attr_accessor :callback_log def initialize @callback_log = [] super end state_machine :status, initial: :draft do event :submit do transition draft: :pending end # Block-only callback should work before_transition do |object| object.callback_log << 'block only callback' end end end workflow = model.new workflow.submit assert_equal 'pending', workflow.status assert_includes workflow.callback_log, 'block only callback' end def test_should_support_pure_keyword_style_without_positional_args model = Class.new do attr_accessor :callback_log def initialize @callback_log = [] super end state_machine :status, initial: :draft do event :submit do transition draft: :pending end # Pure keyword arguments without any positional args before_transition(from: :draft, to: :pending, do: :log_pure_keyword) before_transition(on: :submit, if: :should_log?, do: :log_event_keyword) end private def log_pure_keyword callback_log << 'pure keyword callback' end def log_event_keyword callback_log << 'event keyword callback' end def should_log? true end end workflow = model.new workflow.submit assert_equal 'pending', workflow.status assert_includes workflow.callback_log, 'pure keyword callback' assert_includes workflow.callback_log, 'event keyword callback' end end state_machines-0.100.4/test/functional/motorcycle_test.rb000066400000000000000000000021371507333401300235320ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/motorcycle' class MotorcycleTest < Minitest::Test def setup @motorcycle = Motorcycle.new end def test_should_be_in_idling_state assert_equal 'idling', @motorcycle.state end def test_should_allow_park assert @motorcycle.park end def test_should_not_allow_ignite refute @motorcycle.ignite end def test_should_allow_shift_up assert @motorcycle.shift_up end def test_should_not_allow_shift_down refute @motorcycle.shift_down end def test_should_not_allow_crash refute @motorcycle.crash end def test_should_not_allow_repair refute @motorcycle.repair end def test_should_inherit_decibels_from_superclass @motorcycle.park assert_in_delta(0.0, @motorcycle.decibels) end def test_should_use_decibels_defined_in_state @motorcycle.shift_up assert_in_delta(1.0, @motorcycle.decibels) end def test_should_not_inherit_from_superclass_if_value_is_set vehicle = Vehicle.new @motorcycle.shift_up assert_in_delta(0.0, vehicle.decibels) end end state_machines-0.100.4/test/functional/starfleet_ship_test.rb000066400000000000000000000126541507333401300243730ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/starfleet_ship' class StarfleetShipTest < StateMachinesTest def setup @ship = StarfleetShip.new end # Test initial states across all state machines def test_should_be_in_initial_states assert_sm_state(@ship, :docked, machine_name: :status) assert_sm_state(@ship, :down, machine_name: :shields) assert_sm_state(@ship, :standby, machine_name: :weapons) end # Test single state machine transitions def test_should_allow_undocking assert_sm_transition(@ship, :undock, :impulse, machine_name: :status) end def test_should_allow_raising_shields assert_sm_transition(@ship, :raise_shields, :up, machine_name: :shields) end def test_should_allow_arming_weapons assert_sm_transition(@ship, :arm_weapons, :armed, machine_name: :weapons) end # Test transition capabilities across different machines def test_should_not_allow_warp_when_docked assert_sm_cannot_transition(@ship, :engage_warp) end def test_should_allow_lowering_shields_when_up @ship.raise_shields! assert_sm_can_transition(@ship, :lower_shields, machine_name: :shields) end def test_should_allow_weapons_targeting_when_armed @ship.arm_weapons! assert_sm_can_transition(@ship, :target_weapons, machine_name: :weapons) end # Test callback definitions across multiple machines def test_should_have_departure_checklist_callback assert_before_transition(StarfleetShip, from: :docked, do: :departure_checklist) end def test_should_have_battle_stations_callback assert_after_transition(StarfleetShip, on: :red_alert, do: :sound_battle_stations) end def test_should_have_shield_power_up_callback # Test on shields machine specifically shields_machine = StarfleetShip.state_machine(:shields) assert_before_transition(shields_machine, from: :down, to: :up, do: :power_up_shields) end def test_should_have_weapons_arming_callback # Test on weapons machine specifically weapons_machine = StarfleetShip.state_machine(:weapons) assert_before_transition(weapons_machine, from: :standby, do: :arm_weapons_systems) end # Test indirect event triggering across multiple machines def test_engage_combat_mode_should_trigger_multiple_events @ship.undock! # Get ship ready for combat # This method should trigger events on multiple state machines assert_sm_triggers_event(@ship, :red_alert, machine_name: :status) do @ship.engage_combat_mode end end def test_engage_combat_mode_should_trigger_shield_events @ship.undock! # Get ship ready for combat assert_sm_triggers_event(@ship, :raise_shields, machine_name: :shields) do @ship.engage_combat_mode end end def test_engage_combat_mode_should_trigger_weapons_events @ship.undock! # Get ship ready for combat assert_sm_triggers_event(@ship, :arm, machine_name: :weapons) do @ship.engage_combat_mode end end # Test complex multi-machine scenarios def test_emergency_shutdown_affects_all_systems # Set up ship in active state @ship.undock! @ship.raise_shields! @ship.arm_weapons! # Verify active states assert_sm_state(@ship, :impulse, machine_name: :status) assert_sm_state(@ship, :up, machine_name: :shields) assert_sm_state(@ship, :armed, machine_name: :weapons) # Emergency shutdown should affect multiple systems @ship.emergency_shutdown assert_sm_state(@ship, :emergency, machine_name: :status) assert_sm_state(@ship, :down, machine_name: :shields) assert_sm_state(@ship, :standby, machine_name: :weapons) end def test_battle_sequence_coordination @ship.undock! # Test coordinated battle preparation @ship.begin_battle_sequence assert_sm_state(@ship, :up, machine_name: :shields) assert_sm_state(@ship, :targeted, machine_name: :weapons) end # Test state machine specific event triggering def test_shield_modulation_self_transition @ship.raise_shields! # For self-transitions, we test that the event can be triggered, not that state changes assert_sm_can_transition(@ship, :modulate, machine_name: :shields) @ship.modulate! assert_sm_state(@ship, :up, machine_name: :shields) # Should remain up after modulation end def test_weapons_fire_sequence @ship.arm_weapons! @ship.target_weapons! # The fire_all_weapons method triggers both fire and reload events assert_sm_triggers_event(@ship, %i[fire reload], machine_name: :weapons) do @ship.fire_all_weapons end end # Test persisted states (if persistence is implemented) def test_should_persist_multiple_machine_states @ship.undock! @ship.raise_shields! @ship.arm_weapons! # Test persistence for each machine (assuming persistence is enabled) assert_sm_state_persisted(@ship, 'impulse', :status) assert_sm_state_persisted(@ship, 'up', :shields) assert_sm_state_persisted(@ship, 'armed', :weapons) end # Test callback execution verification def test_warp_core_engagement_callback_executes @ship.undock! initial_temp = @ship.warp_core_temperature @ship.engage_warp! assert_operator @ship.warp_core_temperature, :>, initial_temp, 'Expected warp core temperature to increase' end def test_red_alert_callback_executes @ship.undock! refute @ship.red_alert_triggered @ship.red_alert! assert @ship.red_alert_triggered, 'Expected red alert to be triggered' end end state_machines-0.100.4/test/functional/thread_storage_test.rb000066400000000000000000000014341507333401300243440ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/thread_storage' # Tests related to https://github.com/state-machines/state_machines/issues/152 class ThreadStorageTest < Minitest::Test def setup ThreadStorage.flush! @machine = ThreadStorage.new end # A state transition should not change context to a different Thread. This # also includes keeping around things added to the store during the transition # https://github.com/state-machines/state_machines/issues/152#issuecomment-3329710026 def test_should_not_change_object_id_in_thread_current @machine.fire_state_event :start assert_equal %i[ before_transition before_around_transition after_around_transition after_transition ], ThreadStorage.store end end state_machines-0.100.4/test/functional/traffic_light_caution_test.rb000066400000000000000000000006141507333401300256770ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/traffic_light' class TrafficLightCautionTest < Minitest::Test def setup @light = TrafficLight.new @light.state = 'caution' end def test_should_use_caution_color assert_equal 'yellow', @light.color end def test_should_use_caution_capture_violations assert @light.capture_violations? end end state_machines-0.100.4/test/functional/traffic_light_proceed_test.rb000066400000000000000000000006271507333401300256620ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/traffic_light' class TrafficLightProceedTest < Minitest::Test def setup @light = TrafficLight.new @light.state = 'proceed' end def test_should_use_proceed_color assert_equal 'green', @light.color end def test_should_use_proceed_capture_violations refute_predicate @light, :capture_violations? end end state_machines-0.100.4/test/functional/traffic_light_stop_test.rb000066400000000000000000000011321507333401300252160ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/traffic_light' class TrafficLightStopTest < Minitest::Test def setup @light = TrafficLight.new @light.state = 'stop' end def test_should_use_stop_color assert_equal 'red', @light.color end def test_should_pass_arguments_through assert_equal 'RED', @light.color(:upcase!) end def test_should_pass_block_through color = @light.color { |value| value.upcase! } assert_equal 'RED', color end def test_should_use_stop_capture_violations assert @light.capture_violations? end end state_machines-0.100.4/test/functional/vehicle_callbacks_test.rb000066400000000000000000000046471507333401300250000ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class VehicleCallbacksTest < StateMachinesTest def setup @vehicle = Vehicle.new end # Test that the Vehicle class has the expected callback definitions def test_should_have_before_transition_put_on_seatbelt assert_before_transition(Vehicle, from: :parked, do: :put_on_seatbelt) end def test_should_have_before_transition_increase_insurance_premium assert_before_transition(Vehicle, to: :stalled, do: :increase_insurance_premium) end def test_should_have_after_transition_tow_on_crash assert_after_transition(Vehicle, on: :crash, do: :tow) end def test_should_have_after_transition_fix_on_repair assert_after_transition(Vehicle, on: :repair, do: :fix) end # Test that events actually trigger through method calls (indirect event testing) def test_crash_should_trigger_crash_event @vehicle.ignite # Get to first gear @vehicle.shift_up # Ensure auto shop is available (required condition for crash) assert_predicate @vehicle.auto_shop, :available?, 'Auto shop should be available for crash event' # The crash method should trigger the crash event assert_sm_triggers_event(@vehicle, :crash) do @vehicle.crash! end end def test_ignite_should_trigger_ignite_event_from_parked # Test direct event triggering assert_sm_triggers_event(@vehicle, :ignite) do @vehicle.ignite! end end def test_park_should_trigger_park_event_from_idling @vehicle.ignite # Get to idling state assert_sm_triggers_event(@vehicle, :park) do @vehicle.park! end end # Test callback execution (the callbacks actually work) def test_put_on_seatbelt_callback_executes refute @vehicle.seatbelt_on @vehicle.ignite assert @vehicle.seatbelt_on, 'Expected seatbelt to be on after leaving parked state' end def test_remove_seatbelt_callback_executes @vehicle.ignite # Put seatbelt on assert @vehicle.seatbelt_on @vehicle.park # Should remove seatbelt refute @vehicle.seatbelt_on, 'Expected seatbelt to be off after parking' end def test_insurance_premium_increases_when_stalling original_premium = @vehicle.insurance_premium @vehicle.ignite @vehicle.shift_up # Get to first gear @vehicle.crash! # Should transition to stalled and increase premium assert_operator @vehicle.insurance_premium, :>, original_premium end end state_machines-0.100.4/test/functional/vehicle_first_gear_test.rb000066400000000000000000000014351507333401300251760ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class VehicleFirstGearTest < Minitest::Test def setup @vehicle = Vehicle.new @vehicle.ignite @vehicle.shift_up end def test_should_be_in_first_gear_state assert_equal 'first_gear', @vehicle.state end def test_should_be_first_gear assert_predicate @vehicle, :first_gear? end def test_should_allow_park assert @vehicle.park end def test_should_allow_idle assert @vehicle.idle end def test_should_allow_shift_up assert @vehicle.shift_up end def test_should_not_allow_shift_down refute @vehicle.shift_down end def test_should_allow_crash assert @vehicle.crash end def test_should_not_allow_repair refute @vehicle.repair end end state_machines-0.100.4/test/functional/vehicle_idling_test.rb000066400000000000000000000023031507333401300243120ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class VehicleIdlingTest < StateMachinesTest def setup @vehicle = Vehicle.new @vehicle.ignite end def test_should_be_in_idling_state assert_sm_state(@vehicle, :idling) end def test_should_be_idling assert_predicate @vehicle, :idling? end def test_should_have_seatbelt_on assert @vehicle.seatbelt_on end def test_should_track_time_elapsed refute_nil @vehicle.time_elapsed end def test_should_allow_park assert_sm_can_transition(@vehicle, :park) end def test_should_call_park_with_bang_action class << @vehicle def park super && 1 end end assert_equal 1, @vehicle.park! end def test_should_not_allow_idle assert_sm_cannot_transition(@vehicle, :idle) end def test_should_allow_shift_up assert_sm_can_transition(@vehicle, :shift_up) end def test_should_not_allow_shift_down assert_sm_cannot_transition(@vehicle, :shift_down) end def test_should_not_allow_crash assert_sm_cannot_transition(@vehicle, :crash) end def test_should_not_allow_repair assert_sm_cannot_transition(@vehicle, :repair) end end state_machines-0.100.4/test/functional/vehicle_locked_test.rb000066400000000000000000000012111507333401300243020ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class VehicleLockedTest < Minitest::Test def setup @vehicle = Vehicle.new @vehicle.state = 'locked' end def test_should_be_parked_after_park @vehicle.park assert_predicate @vehicle, :parked? end def test_should_be_parked_after_ignite @vehicle.ignite assert_predicate @vehicle, :parked? end def test_should_be_parked_after_shift_up @vehicle.shift_up assert_predicate @vehicle, :parked? end def test_should_be_parked_after_shift_down @vehicle.shift_down assert_predicate @vehicle, :parked? end end state_machines-0.100.4/test/functional/vehicle_parked_test.rb000066400000000000000000000021711507333401300243150ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class VehicleParkedTest < StateMachinesTest def setup @vehicle = Vehicle.new end def test_should_be_in_parked_state assert_equal 'parked', @vehicle.state end def test_should_not_have_the_seatbelt_on refute @vehicle.seatbelt_on end def test_should_not_allow_park refute @vehicle.park end def test_should_allow_ignite assert_sm_event_triggers(@vehicle, :ignite) assert_equal 'idling', @vehicle.state end def test_should_not_allow_idle refute_sm_event_triggers(@vehicle, :idle) end def test_should_not_allow_shift_up refute_sm_event_triggers(@vehicle, :shift_up) end def test_should_not_allow_shift_down refute_sm_event_triggers(@vehicle, :shift_down) end def test_should_not_allow_crash refute_sm_event_triggers(@vehicle, :crash) end def test_should_not_allow_repair refute_sm_event_triggers(@vehicle, :repair) end def test_should_raise_exception_if_repair_not_allowed! assert_sm_event_raises_error(@vehicle, :repair, StateMachines::InvalidTransition) end end state_machines-0.100.4/test/functional/vehicle_repaired_test.rb000066400000000000000000000007001507333401300246360ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class VehicleRepairedTest < Minitest::Test def setup @vehicle = Vehicle.new @vehicle.ignite @vehicle.shift_up @vehicle.crash @vehicle.repair end def test_should_be_in_parked_state assert_equal 'parked', @vehicle.state end def test_should_not_have_a_busy_auto_shop assert_predicate @vehicle.auto_shop, :available? end end state_machines-0.100.4/test/functional/vehicle_second_gear_test.rb000066400000000000000000000014621507333401300253220ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class VehicleSecondGearTest < Minitest::Test def setup @vehicle = Vehicle.new @vehicle.ignite 2.times { @vehicle.shift_up } end def test_should_be_in_second_gear_state assert_equal 'second_gear', @vehicle.state end def test_should_be_second_gear assert_predicate @vehicle, :second_gear? end def test_should_not_allow_park refute @vehicle.park end def test_should_not_allow_idle refute @vehicle.idle end def test_should_allow_shift_up assert @vehicle.shift_up end def test_should_allow_shift_down assert @vehicle.shift_down end def test_should_allow_crash assert @vehicle.crash end def test_should_not_allow_repair refute @vehicle.repair end end state_machines-0.100.4/test/functional/vehicle_stalled_test.rb000066400000000000000000000025361507333401300245040ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class VehicleStalledTest < Minitest::Test def setup @vehicle = Vehicle.new @vehicle.ignite @vehicle.shift_up @vehicle.crash end def test_should_be_in_stalled_state assert_equal 'stalled', @vehicle.state end def test_should_be_stalled assert_predicate @vehicle, :stalled? end def test_should_be_towed assert_predicate @vehicle.auto_shop, :busy? assert_equal 1, @vehicle.auto_shop.num_customers end def test_should_have_an_increased_insurance_premium assert_equal 150, @vehicle.insurance_premium end def test_should_not_allow_park refute @vehicle.park end def test_should_allow_ignite assert @vehicle.ignite end def test_should_not_change_state_when_ignited assert_equal 'stalled', @vehicle.state end def test_should_not_allow_idle refute @vehicle.idle end def test_should_now_allow_shift_up refute @vehicle.shift_up end def test_should_not_allow_shift_down refute @vehicle.shift_down end def test_should_not_allow_crash refute @vehicle.crash end def test_should_allow_repair_if_auto_shop_is_busy assert @vehicle.repair end def test_should_not_allow_repair_if_auto_shop_is_available @vehicle.auto_shop.fix_vehicle refute @vehicle.repair end end state_machines-0.100.4/test/functional/vehicle_test.rb000066400000000000000000000007611507333401300227720ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class VehicleTest < Minitest::Test def setup @vehicle = Vehicle.new end def test_should_not_allow_access_to_subclass_events refute_respond_to @vehicle, :reverse end def test_should_have_human_state_names assert_equal 'parked', Vehicle.human_state_name(:parked) end def test_should_have_human_state_event_names assert_equal 'park', Vehicle.human_state_event_name(:park) end end state_machines-0.100.4/test/functional/vehicle_third_gear_test.rb000066400000000000000000000014611507333401300251600ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class VehicleThirdGearTest < Minitest::Test def setup @vehicle = Vehicle.new @vehicle.ignite 3.times { @vehicle.shift_up } end def test_should_be_in_third_gear_state assert_equal 'third_gear', @vehicle.state end def test_should_be_third_gear assert_predicate @vehicle, :third_gear? end def test_should_not_allow_park refute @vehicle.park end def test_should_not_allow_idle refute @vehicle.idle end def test_should_not_allow_shift_up refute @vehicle.shift_up end def test_should_allow_shift_down assert @vehicle.shift_down end def test_should_allow_crash assert @vehicle.crash end def test_should_not_allow_repair refute @vehicle.repair end end state_machines-0.100.4/test/functional/vehicle_unsaved_test.rb000066400000000000000000000113051507333401300245130ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class VehicleUnsavedTest < Minitest::Test def setup @vehicle = Vehicle.new end def test_should_be_in_parked_state assert_equal 'parked', @vehicle.state end def test_should_raise_exception_if_checking_invalid_state assert_raises(IndexError) { @vehicle.state?(:invalid) } end def test_should_raise_exception_if_getting_name_of_invalid_state @vehicle.state = 'invalid' assert_raises(ArgumentError) { @vehicle.state_name } end def test_should_be_parked assert_predicate @vehicle, :parked? assert @vehicle.state?(:parked) assert_equal :parked, @vehicle.state_name assert_equal 'parked', @vehicle.human_state_name end def test_should_not_be_idling refute_predicate @vehicle, :idling? end def test_should_not_be_first_gear refute_predicate @vehicle, :first_gear? end def test_should_not_be_second_gear refute_predicate @vehicle, :second_gear? end def test_should_not_be_stalled refute_predicate @vehicle, :stalled? end def test_should_not_be_able_to_park refute_predicate @vehicle, :can_park? end def test_should_not_have_a_transition_for_park assert_nil @vehicle.park_transition end def test_should_not_allow_park refute @vehicle.park end def test_should_be_able_to_ignite assert_predicate @vehicle, :can_ignite? end def test_should_have_a_transition_for_ignite transition = @vehicle.ignite_transition refute_nil transition assert_equal 'parked', transition.from assert_equal 'idling', transition.to assert_equal :ignite, transition.event assert_equal :state, transition.attribute assert_equal @vehicle, transition.object end def test_should_have_a_list_of_possible_events assert_equal [:ignite], @vehicle.state_events end def test_should_have_a_list_of_possible_transitions assert_equal([{ object: @vehicle, attribute: :state, event: :ignite, from: 'parked', to: 'idling' }], @vehicle.state_transitions.map { |transition| transition.attributes }) end def test_should_have_a_list_of_possible_paths assert_equal [[ StateMachines::Transition.new(@vehicle, Vehicle.state_machine, :ignite, :parked, :idling), StateMachines::Transition.new(@vehicle, Vehicle.state_machine, :shift_up, :idling, :first_gear) ]], @vehicle.state_paths(to: :first_gear) end def test_should_allow_generic_event_to_fire assert @vehicle.fire_state_event(:ignite) assert_equal 'idling', @vehicle.state end def test_should_pass_arguments_through_to_generic_event_runner @vehicle.fire_state_event(:ignite, 1, 2, 3) assert_equal [1, 2, 3], @vehicle.last_transition_args end def test_should_allow_skipping_action_through_generic_event_runner @vehicle.fire_state_event(:ignite, false) refute @vehicle.saved end def test_should_raise_error_with_invalid_event_through_generic_event_runer assert_raises(IndexError) { @vehicle.fire_state_event(:invalid) } end def test_should_allow_ignite assert @vehicle.ignite assert_equal 'idling', @vehicle.state end def test_should_allow_ignite_with_skipped_action assert @vehicle.ignite(false) assert_predicate @vehicle, :new_record? end def test_should_allow_ignite_bang assert @vehicle.ignite! end def test_should_allow_ignite_bang_with_skipped_action assert @vehicle.ignite!(false) assert_predicate @vehicle, :new_record? end def test_should_be_saved_after_successful_event @vehicle.ignite refute_predicate @vehicle, :new_record? end def test_should_not_allow_idle refute @vehicle.idle end def test_should_not_allow_shift_up refute @vehicle.shift_up end def test_should_not_allow_shift_down refute @vehicle.shift_down end def test_should_not_allow_crash refute @vehicle.crash end def test_should_not_allow_repair refute @vehicle.repair end def test_should_be_insurance_inactive assert_predicate @vehicle, :insurance_inactive? end def test_should_be_able_to_buy assert_predicate @vehicle, :can_buy_insurance? end def test_should_allow_buying_insurance assert @vehicle.buy_insurance end def test_should_allow_buying_insurance_bang assert @vehicle.buy_insurance! end def test_should_allow_ignite_buying_insurance_with_skipped_action assert @vehicle.buy_insurance!(false) assert_predicate @vehicle, :new_record? end def test_should_not_be_insurance_active refute_predicate @vehicle, :insurance_active? end def test_should_not_be_able_to_cancel refute_predicate @vehicle, :can_cancel_insurance? end def test_should_not_allow_cancelling_insurance refute @vehicle.cancel_insurance end end state_machines-0.100.4/test/functional/vehicle_with_event_attributes_test.rb000066400000000000000000000013661507333401300274760ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class VehicleWithEventAttributesTest < Minitest::Test def setup @vehicle = Vehicle.new @vehicle.state_event = 'ignite' end def test_should_fail_if_event_is_invalid @vehicle.state_event = 'invalid' refute @vehicle.save assert_equal 'parked', @vehicle.state end def test_should_fail_if_event_has_no_transition @vehicle.state_event = 'park' refute @vehicle.save assert_equal 'parked', @vehicle.state end def test_should_return_original_action_value_on_success assert_equal @vehicle, @vehicle.save end def test_should_transition_state_on_success @vehicle.save assert_equal 'idling', @vehicle.state end end state_machines-0.100.4/test/functional/vehicle_with_parallel_events_test.rb000066400000000000000000000047261507333401300272720ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class VehicleWithParallelEventsTest < StateMachinesTest def setup @vehicle = Vehicle.new end def test_should_fail_if_any_event_cannot_transition # Cannot cancel insurance when inactive assert_sm_cannot_transition(@vehicle, :cancel_insurance, machine_name: :insurance_state) refute @vehicle.fire_events(:ignite, :cancel_insurance) end def test_should_be_successful_if_all_events_transition # Both events should be possible assert_sm_can_transition(@vehicle, :ignite) assert_sm_can_transition(@vehicle, :buy_insurance, machine_name: :insurance_state) assert @vehicle.fire_events(:ignite, :buy_insurance) # Verify final states on both machines assert_sm_state(@vehicle, :idling) assert_sm_state(@vehicle, :active, machine_name: :insurance_state) end def test_should_not_save_if_skipping_action assert @vehicle.fire_events(:ignite, :buy_insurance, false) refute @vehicle.saved # States should still have changed even without saving assert_sm_state(@vehicle, :idling) assert_sm_state(@vehicle, :active, machine_name: :insurance_state) end def test_should_raise_exception_if_any_event_cannot_transition_on_bang # Use TestHelper to verify preconditions assert_sm_can_transition(@vehicle, :ignite) assert_sm_cannot_transition(@vehicle, :cancel_insurance, machine_name: :insurance_state) exception = assert_raises(StateMachines::InvalidParallelTransition) { @vehicle.fire_events!(:ignite, :cancel_insurance) } assert_equal @vehicle, exception.object assert_equal %i[ignite cancel_insurance], exception.events end def test_should_not_raise_exception_if_all_events_transition_on_bang # Verify both transitions are possible before attempting assert_sm_can_transition(@vehicle, :ignite) assert_sm_can_transition(@vehicle, :buy_insurance, machine_name: :insurance_state) assert @vehicle.fire_events!(:ignite, :buy_insurance) # Verify final states using TestHelper assert_sm_state(@vehicle, :idling) assert_sm_state(@vehicle, :active, machine_name: :insurance_state) end def test_should_not_save_if_skipping_action_on_bang assert @vehicle.fire_events!(:ignite, :buy_insurance, false) refute @vehicle.saved # Use multi-FSM assertions to verify both state changes assert_sm_state(@vehicle, :idling) assert_sm_state(@vehicle, :active, machine_name: :insurance_state) end end state_machines-0.100.4/test/test_helper.rb000066400000000000000000000006371507333401300204720ustar00rootroot00000000000000# frozen_string_literal: true require 'state_machines' require 'state_machines/test_helper' require 'minitest/autorun' require 'debug' if RUBY_ENGINE == 'ruby' require 'minitest/reporters' Minitest::Reporters.use! [Minitest::Reporters::ProgressReporter.new] class StateMachinesTest < Minitest::Test include StateMachines::TestHelper def before_setup super StateMachines::Integrations.reset end end state_machines-0.100.4/test/unit/000077500000000000000000000000001507333401300166005ustar00rootroot00000000000000state_machines-0.100.4/test/unit/assertions/000077500000000000000000000000001507333401300207725ustar00rootroot00000000000000state_machines-0.100.4/test/unit/assertions/options_validator_exclusive_keys_test.rb000066400000000000000000000027031507333401300312420ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'state_machines/options_validator' class OptionsValidatorExclusiveKeysTest < StateMachinesTest def test_should_not_raise_exception_if_no_keys_found StateMachines::OptionsValidator.assert_exclusive_keys!({ on: :park }, :only, :except) end def test_should_not_raise_exception_if_one_key_found StateMachines::OptionsValidator.assert_exclusive_keys!({ only: :parked }, :only, :except) StateMachines::OptionsValidator.assert_exclusive_keys!({ except: :parked }, :only, :except) end def test_should_raise_exception_if_two_keys_found exception = assert_raises(ArgumentError) { StateMachines::OptionsValidator.assert_exclusive_keys!({ only: :parked, except: :parked }, :only, :except) } assert_equal 'Conflicting keys: only, except', exception.message end def test_should_raise_exception_if_multiple_keys_found exception = assert_raises(ArgumentError) { StateMachines::OptionsValidator.assert_exclusive_keys!({ only: :parked, except: :parked, on: :park }, :only, :except, :with) } assert_equal 'Conflicting keys: only, except', exception.message end def test_should_include_caller_info_in_error_message exception = assert_raises(ArgumentError) { StateMachines::OptionsValidator.assert_exclusive_keys!({ only: :parked, except: :parked }, :only, :except, caller_info: 'TestClass#test_method') } assert_match(/in TestClass#test_method/, exception.message) end end state_machines-0.100.4/test/unit/assertions/options_validator_test.rb000066400000000000000000000016471507333401300261260ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'state_machines/options_validator' class OptionsValidatorTest < StateMachinesTest def test_should_not_raise_exception_if_key_is_valid StateMachines::OptionsValidator.assert_valid_keys!({ name: 'foo', value: 'bar' }, :name, :value, :force) end def test_should_raise_exception_if_key_is_invalid exception = assert_raises(ArgumentError) { StateMachines::OptionsValidator.assert_valid_keys!({ name: 'foo', value: 'bar', invalid: true }, :name, :value, :force) } assert_equal 'Unknown key: :invalid. Valid keys are: :name, :value, :force', exception.message end def test_should_include_caller_info_in_error_message exception = assert_raises(ArgumentError) { StateMachines::OptionsValidator.assert_valid_keys!({ invalid: true }, :valid, caller_info: 'TestClass#test_method') } assert_match(/in TestClass#test_method/, exception.message) end end state_machines-0.100.4/test/unit/async/000077500000000000000000000000001507333401300177155ustar00rootroot00000000000000state_machines-0.100.4/test/unit/async/spaceship_async_test.rb000066400000000000000000000224111507333401300244550ustar00rootroot00000000000000# frozen_string_literal: true require File.expand_path('../../test_helper', __dir__) require File.expand_path('../../files/models/autonomous_drone', __dir__) class SpaceShipAsyncModeTest < Minitest::Test include StateMachines::TestHelper def setup # Skip async tests on unsupported Ruby engines where gems aren't available skip "Async tests not supported on #{RUBY_ENGINE} - async gems not available on this platform" if RUBY_ENGINE == 'jruby' || RUBY_ENGINE == 'truffleruby' @spaceship = AutonomousDrone.new end def test_async_mode_configuration # Test that specific machines have async mode enabled assert_sm_async_mode(@spaceship, :status) assert_sm_async_mode(@spaceship, :shields) assert_sm_async_mode(@spaceship, :teleporter_status) # Test that weapons machine is sync-only assert_sm_sync_mode(@spaceship, :weapons) # Test bulk async machine checking assert_sm_has_async(@spaceship, %i[status teleporter_status shields]) end def test_thread_safe_methods_included_for_async_machines # AsyncMode machines should have thread-safe methods assert_sm_thread_safe_methods(@spaceship) # Async event methods should be available assert_sm_async_methods(@spaceship) end def test_spaceship_launch_sequence_sync # Standard synchronous operation still works assert_equal 'docked', @spaceship.status assert_equal 'standby', @spaceship.weapons assert_equal 'down', @spaceship.shields # Launch sequence result = @spaceship.launch assert result assert_equal 'flying', @spaceship.status # Arm weapons (sync only machine) result = @spaceship.arm_weapons! assert result assert_equal 'armed', @spaceship.weapons # Raise shields result = @spaceship.raise_shields assert result assert_equal 'up', @spaceship.shields end def test_spaceship_launch_sequence_async require 'async' Async do # Test async launch sequence assert_equal 'docked', @spaceship.status result = @spaceship.fire_event_async(:launch) assert result assert_equal 'flying', @spaceship.status # Test async shield raising result = @spaceship.fire_event_async(:raise_shields) assert result assert_equal 'up', @spaceship.shields # Weapons system is sync-only, so should use regular method result = @spaceship.arm_weapons! assert result assert_equal 'armed', @spaceship.weapons end end def test_concurrent_spaceship_operations require 'async' # Test multiple spaceships operating concurrently ships = 3.times.map { AutonomousDrone.new } Async do # Launch all ships concurrently launch_tasks = ships.map do |ship| ship.async_fire_event(:launch) end # Wait for all launches to complete results = launch_tasks.map(&:wait) assert_equal [true, true, true], results # All ships should be flying ships.each do |ship| assert_equal 'flying', ship.status end # Raise shields on all ships concurrently shield_tasks = ships.map do |ship| ship.async_fire_event(:raise_shields) end shield_results = shield_tasks.map(&:wait) assert_equal [true, true, true], shield_results # All shields should be up ships.each do |ship| assert_equal 'up', ship.shields end end end def test_thread_safety_with_multiple_spaceships # Test thread safety with multiple threads accessing same spaceship threads = [] results = [] results_mutex = Mutex.new # Multiple threads trying to launch the same spaceship 5.times do |i| threads << Thread.new do result = @spaceship.fire_event_async(:launch) results_mutex.synchronize do results << { thread: i, result: result, status: @spaceship.status } end rescue StandardError => e results_mutex.synchronize do results << { thread: i, error: e.message } end end end threads.each(&:join) # Only one thread should successfully launch, others should fail successful_launches = results.count { |r| r[:result] == true } assert_equal 1, successful_launches, 'Only one thread should successfully launch' assert_equal 'flying', @spaceship.status end def test_callbacks_work_with_async_mode assert_empty @spaceship.callback_log # Test that callbacks work with async operations result = @spaceship.fire_event_async(:launch) assert result assert_equal 'flying', @spaceship.status # Check that callbacks were executed assert_includes @spaceship.callback_log, 'Autonomous flight sequence initiated...' assert_includes @spaceship.callback_log, 'Drone airborne - autonomous navigation active!' end def test_mixed_sync_and_async_operations # Launch (async-enabled machine) launch_result = @spaceship.fire_event_async(:launch) assert launch_result assert_equal 'flying', @spaceship.status # Arm weapons (sync-only machine) - should work normally weapons_result = @spaceship.arm_weapons! assert weapons_result assert_equal 'armed', @spaceship.weapons # Raise shields (async-enabled machine) shields_result = @spaceship.fire_event_async(:raise_shields) assert shields_result assert_equal 'up', @spaceship.shields end def test_spaceship_emergency_procedures require 'async' # Get to warping state first @spaceship.launch! @spaceship.enter_warp! assert_equal 'warping', @spaceship.status Async do # Exit warp should work async emergency_task = @spaceship.async_fire_event(:exit_warp) result = emergency_task.wait assert result assert_equal 'flying', @spaceship.status end end def test_backward_compatibility_not_broken # All standard sync methods should still work exactly as before assert_equal 'docked', @spaceship.status # Launch using regular sync method result = @spaceship.launch! assert result assert_equal 'flying', @spaceship.status # Enter warp using regular sync method result = @spaceship.enter_warp! assert result assert_equal 'warping', @spaceship.status # Land using regular sync method result = @spaceship.exit_warp! assert result assert_equal 'flying', @spaceship.status result = @spaceship.land! assert result assert_equal 'docked', @spaceship.status end def test_async_bang_methods_raise_exceptions_on_invalid_transitions require 'async' # Try to launch from flying state (invalid transition) @spaceship.launch! # First get to flying state assert_equal 'flying', @spaceship.status Async do # This should raise an exception when awaited because launch is invalid from flying begin task = @spaceship.launch_async! task.wait # This should raise StateMachines::InvalidTransition flunk 'Expected StateMachines::InvalidTransition to be raised' rescue StateMachines::InvalidTransition => e assert_match(/launch/, e.message) assert_includes e.message, 'flying' end # Test that fire_event_async! also raises exceptions begin @spaceship.fire_event_async!(:launch) flunk 'Expected StateMachines::InvalidTransition to be raised' rescue StateMachines::InvalidTransition => e assert_match(/launch/, e.message) end end end def test_async_bang_methods_succeed_on_valid_transitions require 'async' # Test valid transitions don't raise exceptions assert_equal 'docked', @spaceship.status Async do # Valid transition should work fine task = @spaceship.launch_async! result = task.wait assert result assert_equal 'flying', @spaceship.status # Test fire_event_async! with valid transition result = @spaceship.fire_event_async!(:enter_warp) assert result assert_equal 'warping', @spaceship.status end end def test_individual_event_async_methods_are_generated # Check that async versions of individual events are generated for async machines assert_sm_async_event_methods(@spaceship, :launch) assert_sm_async_event_methods(@spaceship, :enter_warp) assert_sm_async_event_methods(@spaceship, :raise_shields) # Weapons machine is sync-only, so should NOT have async versions refute_respond_to @spaceship, :arm_weapons_async, 'Should NOT have arm_weapons_async method (weapons is sync-only)' refute_respond_to @spaceship, :arm_weapons_async!, 'Should NOT have arm_weapons_async! method (weapons is sync-only)' end def test_async_mode_enables_per_machine_not_globally # Create a spaceship class without any async mode sync_only_spaceship_class = Class.new do attr_accessor :status state_machine :status, initial: :docked do # No async: true parameter event :launch do transition docked: :flying end end def initialize super end end sync_ship = sync_only_spaceship_class.new # This machine should NOT have async mode enabled assert_sm_sync_mode(sync_ship, :status) # Should not have async methods assert_sm_no_async_methods(sync_ship) assert_sm_all_sync(sync_ship) # But regular sync methods should work assert_sm_sync_execution(sync_ship, :launch, :flying, :status) end end state_machines-0.100.4/test/unit/branch/000077500000000000000000000000001507333401300200355ustar00rootroot00000000000000state_machines-0.100.4/test/unit/branch/branch_test.rb000066400000000000000000000015271507333401300226630ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchTest < StateMachinesTest def setup @branch = StateMachines::Branch.new(from: :parked, to: :idling) end def test_should_not_raise_exception_if_implicit_option_specified StateMachines::Branch.new(invalid: :valid) end def test_should_not_have_an_if_condition assert_nil @branch.if_condition end def test_should_not_have_an_unless_condition assert_nil @branch.unless_condition end def test_should_have_a_state_requirement assert_equal 1, @branch.state_requirements.length end def test_should_raise_an_exception_if_invalid_match_option_specified exception = assert_raises(ArgumentError) { @branch.match(Object.new, invalid: true) } assert_equal 'Unknown key: :invalid. Valid keys are: :from, :to, :on, :guard', exception.message end end state_machines-0.100.4/test/unit/branch/branch_with_conflicting_conditionals_test.rb000066400000000000000000000015651507333401300310450ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithConflictingConditionalsTest < StateMachinesTest def setup @object = Object.new end def test_should_match_if_if_is_true_and_unless_is_false branch = StateMachines::Branch.new(if: -> { true }, unless: -> { false }) assert_match branch, @object end def test_should_not_match_if_if_is_false_and_unless_is_true branch = StateMachines::Branch.new(if: -> { false }, unless: -> { true }) refute_match branch, @object end def test_should_not_match_if_if_is_false_and_unless_is_false branch = StateMachines::Branch.new(if: -> { false }, unless: -> { false }) refute_match branch, @object end def test_should_not_match_if_if_is_true_and_unless_is_true branch = StateMachines::Branch.new(if: -> { true }, unless: -> { true }) refute_match branch, @object end end state_machines-0.100.4/test/unit/branch/branch_with_conflicting_from_requirements_test.rb000066400000000000000000000005431507333401300321200ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithConflictingFromRequirementsTest < StateMachinesTest def test_should_raise_an_exception exception = assert_raises(ArgumentError) { StateMachines::Branch.new(from: :parked, except_from: :parked) } assert_equal 'Conflicting keys: from, except_from', exception.message end end state_machines-0.100.4/test/unit/branch/branch_with_conflicting_on_requirements_test.rb000066400000000000000000000005311507333401300315660ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithConflictingOnRequirementsTest < StateMachinesTest def test_should_raise_an_exception exception = assert_raises(ArgumentError) { StateMachines::Branch.new(on: :ignite, except_on: :ignite) } assert_equal 'Conflicting keys: on, except_on', exception.message end end state_machines-0.100.4/test/unit/branch/branch_with_conflicting_to_requirements_test.rb000066400000000000000000000005311507333401300315740ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithConflictingToRequirementsTest < StateMachinesTest def test_should_raise_an_exception exception = assert_raises(ArgumentError) { StateMachines::Branch.new(to: :idling, except_to: :idling) } assert_equal 'Conflicting keys: to, except_to', exception.message end end state_machines-0.100.4/test/unit/branch/branch_with_different_requirements_test.rb000066400000000000000000000022461507333401300305460ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithDifferentRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(from: :parked, to: :idling, on: :ignite) end def test_should_match_empty_query assert @branch.matches?(@object) end def test_should_match_if_all_requirements_match assert @branch.matches?(@object, from: :parked, to: :idling, on: :ignite) end def test_should_not_match_if_from_not_included refute @branch.matches?(@object, from: :idling) end def test_should_not_match_if_to_not_included refute @branch.matches?(@object, to: :parked) end def test_should_not_match_if_on_not_included refute @branch.matches?(@object, on: :park) end def test_should_be_nil_if_unmatched assert_nil @branch.match(@object, from: :parked, to: :idling, on: :park) end def test_should_include_all_known_states assert_equal %i[parked idling], @branch.known_states end def test_should_not_duplicate_known_statse branch = StateMachines::Branch.new(except_from: :idling, to: :idling, on: :ignite) assert_equal [:idling], branch.known_states end end state_machines-0.100.4/test/unit/branch/branch_with_except_from_matcher_requirement_test.rb000066400000000000000000000006071507333401300324320ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithExceptFromMatcherRequirementTest < StateMachinesTest def test_should_raise_an_exception exception = assert_raises(ArgumentError) { StateMachines::Branch.new(except_from: StateMachines::AllMatcher.instance) } assert_equal ':except_from option cannot use matchers; use :from instead', exception.message end end state_machines-0.100.4/test/unit/branch/branch_with_except_from_requirement_test.rb000066400000000000000000000016741507333401300307340ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithExceptFromRequirementTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(except_from: :parked) end def test_should_use_a_blacklist_matcher assert_instance_of StateMachines::BlacklistMatcher, @branch.state_requirements.first[:from] end def test_should_match_if_not_included assert @branch.matches?(@object, from: :idling) end def test_should_not_match_if_included refute @branch.matches?(@object, from: :parked) end def test_should_match_if_nil assert @branch.matches?(@object, from: nil) end def test_should_ignore_to assert @branch.matches?(@object, from: :idling, to: :parked) end def test_should_ignore_on assert @branch.matches?(@object, from: :idling, on: :ignite) end def test_should_be_included_in_known_states assert_equal [:parked], @branch.known_states end end state_machines-0.100.4/test/unit/branch/branch_with_except_on_matcher_requirement_test.rb000066400000000000000000000005771507333401300321110ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithExceptOnMatcherRequirementTest < StateMachinesTest def test_should_raise_an_exception exception = assert_raises(ArgumentError) { StateMachines::Branch.new(except_on: StateMachines::AllMatcher.instance) } assert_equal ':except_on option cannot use matchers; use :on instead', exception.message end end state_machines-0.100.4/test/unit/branch/branch_with_except_on_requirement_test.rb000066400000000000000000000016271507333401300304030ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithExceptOnRequirementTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(except_on: :ignite) end def test_should_use_a_blacklist_matcher assert_instance_of StateMachines::BlacklistMatcher, @branch.event_requirement end def test_should_match_if_not_included assert @branch.matches?(@object, on: :park) end def test_should_not_match_if_included refute @branch.matches?(@object, on: :ignite) end def test_should_match_if_nil assert @branch.matches?(@object, on: nil) end def test_should_ignore_to assert @branch.matches?(@object, on: :park, to: :idling) end def test_should_ignore_from assert @branch.matches?(@object, on: :park, from: :parked) end def test_should_not_be_included_in_known_states assert_empty @branch.known_states end end state_machines-0.100.4/test/unit/branch/branch_with_except_to_matcher_requirement_test.rb000066400000000000000000000005771507333401300321170ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithExceptToMatcherRequirementTest < StateMachinesTest def test_should_raise_an_exception exception = assert_raises(ArgumentError) { StateMachines::Branch.new(except_to: StateMachines::AllMatcher.instance) } assert_equal ':except_to option cannot use matchers; use :to instead', exception.message end end state_machines-0.100.4/test/unit/branch/branch_with_except_to_requirement_test.rb000066400000000000000000000016601507333401300304060ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithExceptToRequirementTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(except_to: :idling) end def test_should_use_a_blacklist_matcher assert_instance_of StateMachines::BlacklistMatcher, @branch.state_requirements.first[:to] end def test_should_match_if_not_included assert @branch.matches?(@object, to: :parked) end def test_should_not_match_if_included refute @branch.matches?(@object, to: :idling) end def test_should_match_if_nil assert @branch.matches?(@object, to: nil) end def test_should_ignore_from assert @branch.matches?(@object, to: :parked, from: :idling) end def test_should_ignore_on assert @branch.matches?(@object, to: :parked, on: :ignite) end def test_should_be_included_in_known_states assert_equal [:idling], @branch.known_states end end state_machines-0.100.4/test/unit/branch/branch_with_from_matcher_requirement_test.rb000066400000000000000000000010761507333401300310630ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithFromMatcherRequirementTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(from: StateMachines::BlacklistMatcher.new(%i[idling parked])) end def test_should_match_if_included assert @branch.matches?(@object, from: :first_gear) end def test_should_not_match_if_not_included refute @branch.matches?(@object, from: :idling) end def test_include_values_in_known_states assert_equal %i[idling parked], @branch.known_states end end state_machines-0.100.4/test/unit/branch/branch_with_from_requirement_test.rb000066400000000000000000000023051507333401300273540ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithFromRequirementTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(from: :parked) end def test_should_use_a_whitelist_matcher assert_instance_of StateMachines::WhitelistMatcher, @branch.state_requirements.first[:from] end def test_should_match_if_not_specified assert @branch.matches?(@object, to: :idling) end def test_should_match_if_included assert @branch.matches?(@object, from: :parked) end def test_should_not_match_if_not_included refute @branch.matches?(@object, from: :idling) end def test_should_not_match_if_nil refute @branch.matches?(@object, from: nil) end def test_should_ignore_to assert @branch.matches?(@object, from: :parked, to: :idling) end def test_should_ignore_on assert @branch.matches?(@object, from: :parked, on: :ignite) end def test_should_be_included_in_known_states assert_equal [:parked], @branch.known_states end def test_should_include_requirement_in_match match = @branch.match(@object, from: :parked) assert_equal @branch.state_requirements.first[:from], match[:from] end end state_machines-0.100.4/test/unit/branch/branch_with_if_conditional_test.rb000066400000000000000000000012771507333401300267610ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithIfConditionalTest < StateMachinesTest def setup @object = Object.new end def test_should_have_an_if_condition branch = StateMachines::Branch.new(if: -> { true }) refute_nil branch.if_condition end def test_should_match_if_true branch = StateMachines::Branch.new(if: -> { true }) assert branch.matches?(@object) end def test_should_not_match_if_false branch = StateMachines::Branch.new(if: -> { false }) refute branch.matches?(@object) end def test_should_be_nil_if_unmatched branch = StateMachines::Branch.new(if: -> { false }) assert_nil branch.match(@object) end end state_machines-0.100.4/test/unit/branch/branch_with_implicit_and_explicit_requirements_test.rb000066400000000000000000000015201507333401300331270ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithImplicitAndExplicitRequirementsTest < StateMachinesTest def setup @branch = StateMachines::Branch.new(parked: :idling, from: :parked) end def test_should_create_multiple_requirements assert_equal 2, @branch.state_requirements.length end def test_should_create_implicit_requirements_for_implicit_options assert(@branch.state_requirements.any? do |state_requirement| state_requirement[:from].values == [:parked] && state_requirement[:to].values == [:idling] end) end def test_should_create_implicit_requirements_for_explicit_options assert(@branch.state_requirements.any? do |state_requirement| state_requirement[:from].values == [:from] && state_requirement[:to].values == [:parked] end) end end state_machines-0.100.4/test/unit/branch/branch_with_implicit_from_requirement_matcher_test.rb000066400000000000000000000010371507333401300327520ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithMultipleFromRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(from: %i[idling parked]) end def test_should_match_if_included assert @branch.matches?(@object, from: :idling) end def test_should_not_match_if_not_included refute @branch.matches?(@object, from: :first_gear) end def test_should_be_included_in_known_states assert_equal %i[idling parked], @branch.known_states end end state_machines-0.100.4/test/unit/branch/branch_with_implicit_requirement_test.rb000066400000000000000000000012721507333401300302250ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithImplicitRequirementTest < StateMachinesTest def setup @branch = StateMachines::Branch.new(parked: :idling, on: :ignite) end def test_should_create_an_event_requirement assert_instance_of StateMachines::WhitelistMatcher, @branch.event_requirement assert_equal [:ignite], @branch.event_requirement.values end def test_should_use_a_whitelist_from_matcher assert_instance_of StateMachines::WhitelistMatcher, @branch.state_requirements.first[:from] end def test_should_use_a_whitelist_to_matcher assert_instance_of StateMachines::WhitelistMatcher, @branch.state_requirements.first[:to] end end state_machines-0.100.4/test/unit/branch/branch_with_implicit_to_requirement_matcher_test.rb000066400000000000000000000010401507333401300324230ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithImplicitToRequirementMatcherTest < StateMachinesTest def setup @matcher = StateMachines::BlacklistMatcher.new(:idling) @branch = StateMachines::Branch.new(parked: @matcher) end def test_should_convert_from_to_whitelist_matcher assert_instance_of StateMachines::WhitelistMatcher, @branch.state_requirements.first[:from] end def test_should_not_convert_to_to_whitelist_matcher assert_equal @matcher, @branch.state_requirements.first[:to] end end state_machines-0.100.4/test/unit/branch/branch_with_invalid_state_guards_test.rb000066400000000000000000000031471507333401300301710ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../../test_helper' class BranchWithInvalidStateGuardsTest < Minitest::Test def setup @klass = Class.new do def self.name 'Vehicle' end state_machine :state1, initial: :parked state_machine :state2, initial: :off end @object = @klass.new end def test_should_raise_error_for_nonexistent_machine branch = StateMachines::Branch.new(if_state: { nonexistent_machine: :on }) exception = assert_raises(ArgumentError) { branch.matches?(@object) } assert_equal "State machine 'nonexistent_machine' is not defined for Vehicle", exception.message end def test_should_raise_error_for_nonexistent_state branch = StateMachines::Branch.new(if_state: { state1: :nonexistent_state }) exception = assert_raises(ArgumentError) { branch.matches?(@object) } assert_equal "State 'nonexistent_state' is not defined in state machine 'state1'", exception.message end def test_should_raise_error_for_nonexistent_machine_in_unless branch = StateMachines::Branch.new(unless_state: { nonexistent_machine: :on }) exception = assert_raises(ArgumentError) { branch.matches?(@object) } assert_equal "State machine 'nonexistent_machine' is not defined for Vehicle", exception.message end def test_should_raise_error_for_nonexistent_state_in_unless branch = StateMachines::Branch.new(unless_state: { state1: :nonexistent_state }) exception = assert_raises(ArgumentError) { branch.matches?(@object) } assert_equal "State 'nonexistent_state' is not defined in state machine 'state1'", exception.message end end state_machines-0.100.4/test/unit/branch/branch_with_multiple_except_from_requirements_test.rb000066400000000000000000000010541507333401300330220ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithMultipleExceptFromRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(except_from: %i[idling parked]) end def test_should_match_if_not_included assert @branch.matches?(@object, from: :first_gear) end def test_should_not_match_if_included refute @branch.matches?(@object, from: :idling) end def test_should_be_included_in_known_states assert_equal %i[idling parked], @branch.known_states end end state_machines-0.100.4/test/unit/branch/branch_with_multiple_except_on_requirements_test.rb000066400000000000000000000006621507333401300324770ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithMultipleExceptOnRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(except_on: %i[ignite park]) end def test_should_match_if_not_included assert @branch.matches?(@object, on: :shift_up) end def test_should_not_match_if_included refute @branch.matches?(@object, on: :ignite) end end state_machines-0.100.4/test/unit/branch/branch_with_multiple_except_to_requirements_test.rb000066400000000000000000000010441507333401300325000ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithMultipleExceptToRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(except_to: %i[idling parked]) end def test_should_match_if_not_included assert @branch.matches?(@object, to: :first_gear) end def test_should_not_match_if_included refute @branch.matches?(@object, to: :idling) end def test_should_be_included_in_known_states assert_equal %i[idling parked], @branch.known_states end end state_machines-0.100.4/test/unit/branch/branch_with_multiple_from_requirements_test.rb000066400000000000000000000010451507333401300314520ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithImplicitFromRequirementMatcherTest < StateMachinesTest def setup @matcher = StateMachines::BlacklistMatcher.new(:parked) @branch = StateMachines::Branch.new(@matcher => :idling) end def test_should_not_convert_from_to_whitelist_matcher assert_equal @matcher, @branch.state_requirements.first[:from] end def test_should_convert_to_to_whitelist_matcher assert_instance_of StateMachines::WhitelistMatcher, @branch.state_requirements.first[:to] end end state_machines-0.100.4/test/unit/branch/branch_with_multiple_if_conditionals_test.rb000066400000000000000000000011051507333401300310450ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithMultipleIfConditionalsTest < StateMachinesTest def setup @object = Object.new end def test_should_match_if_all_are_true branch = StateMachines::Branch.new(if: [-> { true }, -> { true }]) assert_match branch, @object end def test_should_not_match_if_any_are_false branch = StateMachines::Branch.new(if: [-> { true }, -> { false }]) refute_match branch, @object branch = StateMachines::Branch.new(if: [-> { false }, -> { true }]) refute_match branch, @object end end state_machines-0.100.4/test/unit/branch/branch_with_multiple_implicit_requirements_test.rb000066400000000000000000000034761507333401300323330ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithMultipleImplicitRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(parked: :idling, idling: :first_gear, on: :ignite) end def test_should_create_multiple_state_requirements assert_equal 2, @branch.state_requirements.length end def test_should_not_match_event_as_state_requirement refute @branch.matches?(@object, from: :on, to: :ignite) end def test_should_match_if_from_included_in_any assert @branch.matches?(@object, from: :parked) assert @branch.matches?(@object, from: :idling) end def test_should_not_match_if_from_not_included_in_any refute @branch.matches?(@object, from: :first_gear) end def test_should_match_if_to_included_in_any assert @branch.matches?(@object, to: :idling) assert @branch.matches?(@object, to: :first_gear) end def test_should_not_match_if_to_not_included_in_any refute @branch.matches?(@object, to: :parked) end def test_should_match_if_all_options_match assert @branch.matches?(@object, from: :parked, to: :idling, on: :ignite) assert @branch.matches?(@object, from: :idling, to: :first_gear, on: :ignite) end def test_should_not_match_if_any_options_do_not_match refute @branch.matches?(@object, from: :parked, to: :idling, on: :park) refute @branch.matches?(@object, from: :parked, to: :first_gear, on: :park) end def test_should_include_all_known_states assert_equal(%i[first_gear idling parked], @branch.known_states.sort_by { |state| state.to_s }) end def test_should_not_duplicate_known_statse branch = StateMachines::Branch.new(parked: :idling, first_gear: :idling) assert_equal(%i[first_gear idling parked], branch.known_states.sort_by { |state| state.to_s }) end end state_machines-0.100.4/test/unit/branch/branch_with_multiple_to_requirements_test.rb000066400000000000000000000010271507333401300311310ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithMultipleToRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(to: %i[idling parked]) end def test_should_match_if_included assert @branch.matches?(@object, to: :idling) end def test_should_not_match_if_not_included refute @branch.matches?(@object, to: :first_gear) end def test_should_be_included_in_known_states assert_equal %i[idling parked], @branch.known_states end end state_machines-0.100.4/test/unit/branch/branch_with_multiple_unless_conditionals_test.rb000066400000000000000000000011271507333401300317640ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithMultipleUnlessConditionalsTest < StateMachinesTest def setup @object = Object.new end def test_should_match_if_all_are_false branch = StateMachines::Branch.new(unless: [-> { false }, -> { false }]) assert_match branch, @object end def test_should_not_match_if_any_are_true branch = StateMachines::Branch.new(unless: [-> { true }, -> { false }]) refute_match branch, @object branch = StateMachines::Branch.new(unless: [-> { false }, -> { true }]) refute_match branch, @object end end state_machines-0.100.4/test/unit/branch/branch_with_nil_requirements_test.rb000066400000000000000000000013161507333401300273570ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithNilRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(from: nil, to: nil) end def test_should_match_empty_query assert @branch.matches?(@object) end def test_should_match_if_all_requirements_match assert @branch.matches?(@object, from: nil, to: nil) end def test_should_not_match_if_from_not_included refute @branch.matches?(@object, from: :parked) end def test_should_not_match_if_to_not_included refute @branch.matches?(@object, to: :idling) end def test_should_include_all_known_states assert_equal [nil], @branch.known_states end end state_machines-0.100.4/test/unit/branch/branch_with_no_requirements_test.rb000066400000000000000000000022051507333401300272070ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithNoRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new end def test_should_use_all_matcher_for_event_requirement assert_equal StateMachines::AllMatcher.instance, @branch.event_requirement end def test_should_use_all_matcher_for_from_state_requirement assert_equal StateMachines::AllMatcher.instance, @branch.state_requirements.first[:from] end def test_should_use_all_matcher_for_to_state_requirement assert_equal StateMachines::AllMatcher.instance, @branch.state_requirements.first[:to] end def test_should_match_empty_query assert @branch.matches?(@object, {}) end def test_should_match_non_empty_query assert @branch.matches?(@object, to: :idling, from: :parked, on: :ignite) end def test_should_include_all_requirements_in_match match = @branch.match(@object, {}) assert_equal @branch.state_requirements.first[:from], match[:from] assert_equal @branch.state_requirements.first[:to], match[:to] assert_equal @branch.event_requirement, match[:on] end end state_machines-0.100.4/test/unit/branch/branch_with_on_matcher_requirement_test.rb000066400000000000000000000007101507333401300305260ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithOnMatcherRequirementTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(on: StateMachines::BlacklistMatcher.new(%i[ignite park])) end def test_should_match_if_included assert @branch.matches?(@object, on: :shift_up) end def test_should_not_match_if_not_included refute @branch.matches?(@object, on: :ignite) end end state_machines-0.100.4/test/unit/branch/branch_with_on_requirement_test.rb000066400000000000000000000022241507333401300270250ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithOnRequirementTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(on: :ignite) end def test_should_use_a_whitelist_matcher assert_instance_of StateMachines::WhitelistMatcher, @branch.event_requirement end def test_should_match_if_not_specified assert @branch.matches?(@object, from: :parked) end def test_should_match_if_included assert @branch.matches?(@object, on: :ignite) end def test_should_not_match_if_not_included refute @branch.matches?(@object, on: :park) end def test_should_not_match_if_nil refute @branch.matches?(@object, on: nil) end def test_should_ignore_to assert @branch.matches?(@object, on: :ignite, to: :parked) end def test_should_ignore_from assert @branch.matches?(@object, on: :ignite, from: :parked) end def test_should_not_be_included_in_known_states assert_empty @branch.known_states end def test_should_include_requirement_in_match match = @branch.match(@object, on: :ignite) assert_equal @branch.event_requirement, match[:on] end end state_machines-0.100.4/test/unit/branch/branch_with_state_guards_test.rb000066400000000000000000000241671507333401300264700ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithStateGuardsTest < StateMachinesTest def setup @klass = Class.new do attr_accessor :state1, :state2 def initialize @state1 = 'parked' @state2 = 'off' super end state_machine :state1, initial: :parked do event :ignite do transition parked: :idling end end state_machine :state2, initial: :off do event :turn_on do transition off: :on end end # A simple method for testing standard :if guards def condition_met? true end end @object = @klass.new end # --- :if_state --- def test_if_state_allows_transition_when_state_matches # Setup: The dependent machine IS in the required state. @object.state2 = 'on' # Action: Create a branch with an :if_state guard. branch = StateMachines::Branch.new(if_state: { state2: :on }) # Assert: The branch should match. assert branch.matches?(@object), "Branch should match when state2 is 'on'" end def test_if_state_prevents_transition_when_state_does_not_match # Setup: The dependent machine is NOT in the required state. @object.state2 = 'off' # Action: Create a branch with an :if_state guard. branch = StateMachines::Branch.new(if_state: { state2: :on }) # Assert: The branch should NOT match. refute branch.matches?(@object), "Branch should not match when state2 is 'off' but 'on' is required" end # --- :unless_state --- def test_unless_state_allows_transition_when_state_does_not_match # Setup: The dependent machine is NOT in the guarded state. @object.state2 = 'off' # Action: Create a branch with an :unless_state guard. branch = StateMachines::Branch.new(unless_state: { state2: :on }) # Assert: The branch should match. assert branch.matches?(@object), "Branch should match when state2 is not 'on'" end def test_unless_state_prevents_transition_when_state_matches # Setup: The dependent machine IS in the guarded state. @object.state2 = 'on' # Action: Create a branch with an :unless_state guard. branch = StateMachines::Branch.new(unless_state: { state2: :on }) # Assert: The branch should NOT match. refute branch.matches?(@object), "Branch should not match when state2 is 'on' and that state is guarded against" end # --- :if_all_states --- def test_if_all_states_allows_transition_when_all_states_match # Setup: ALL dependent machines are in the required states. @object.state1 = 'idling' @object.state2 = 'on' # Action: Create a branch with :if_all_states. branch = StateMachines::Branch.new(if_all_states: { state1: :idling, state2: :on }) # Assert: The branch should match. assert branch.matches?(@object), 'Branch should match when all required states are met' end def test_if_all_states_prevents_transition_when_one_state_does_not_match # Setup: AT LEAST ONE dependent machine is NOT in the required state. @object.state1 = 'idling' # This matches @object.state2 = 'off' # This does NOT match # Action: Create a branch with :if_all_states. branch = StateMachines::Branch.new(if_all_states: { state1: :idling, state2: :on }) # Assert: The branch should NOT match. refute branch.matches?(@object), 'Branch should not match when not all required states are met' end # --- :unless_all_states --- def test_unless_all_states_allows_transition_when_not_all_states_match # Setup: NOT ALL dependent machines are in the specified states. @object.state1 = 'idling' # This matches @object.state2 = 'off' # This does NOT match # Action: Create a branch with :unless_all_states. branch = StateMachines::Branch.new(unless_all_states: { state1: :idling, state2: :on }) # Assert: The branch should match. assert branch.matches?(@object), 'Branch should match when not all specified states are met' end def test_unless_all_states_prevents_transition_when_all_states_match # Setup: ALL dependent machines are in the specified states. @object.state1 = 'idling' @object.state2 = 'on' # Action: Create a branch with :unless_all_states. branch = StateMachines::Branch.new(unless_all_states: { state1: :idling, state2: :on }) # Assert: The branch should NOT match. refute branch.matches?(@object), 'Branch should not match when all specified states are met' end # --- :if_any_state --- def test_if_any_state_allows_transition_when_one_state_matches # Setup: AT LEAST ONE dependent machine IS in a required state. @object.state1 = 'parked' # This does NOT match @object.state2 = 'on' # This matches # Action: Create a branch with :if_any_state. branch = StateMachines::Branch.new(if_any_state: { state1: :idling, state2: :on }) # Assert: The branch should match. assert branch.matches?(@object), 'Branch should match when at least one required state is met' end def test_if_any_state_prevents_transition_when_no_states_match # Setup: NONE of the dependent machines are in the required states. @object.state1 = 'parked' # This does NOT match (needs idling) @object.state2 = 'off' # This does NOT match (needs on) # Action: Create a branch with :if_any_state. branch = StateMachines::Branch.new(if_any_state: { state1: :idling, state2: :on }) # Assert: The branch should NOT match. refute branch.matches?(@object), 'Branch should not match when no required states are met' end # --- :unless_any_state --- def test_unless_any_state_allows_transition_when_no_states_match # Setup: NONE of the dependent machines are in the specified states. @object.state1 = 'parked' # This does NOT match @object.state2 = 'off' # This does NOT match # Action: Create a branch with :unless_any_state. branch = StateMachines::Branch.new(unless_any_state: { state1: :idling, state2: :on }) # Assert: The branch should match. assert branch.matches?(@object), 'Branch should match when none of the specified states are met' end def test_unless_any_state_prevents_transition_when_one_state_matches # Setup: AT LEAST ONE dependent machine IS in a specified state. @object.state1 = 'parked' # This does NOT match @object.state2 = 'on' # This matches # Action: Create a branch with :unless_any_state. branch = StateMachines::Branch.new(unless_any_state: { state1: :idling, state2: :on }) # Assert: The branch should NOT match. refute branch.matches?(@object), 'Branch should not match when at least one specified state is met' end # --- Combination with :if --- def test_allows_transition_when_both_if_and_if_state_are_met # Setup: The standard :if condition is true AND the :if_state condition is met. @object.state2 = 'on' # Action: Create a branch with both :if and :if_state guards. branch = StateMachines::Branch.new(if: :condition_met?, if_state: { state2: :on }) # Assert: The branch should match. assert branch.matches?(@object), 'Branch should match when both :if and :if_state conditions are met' end def test_prevents_transition_when_if_is_met_but_if_state_is_not # Setup: The standard :if condition is true BUT the :if_state condition is NOT met. @object.state2 = 'off' # This does NOT meet the :if_state condition # Action: Create a branch with both :if and :if_state guards. branch = StateMachines::Branch.new(if: :condition_met?, if_state: { state2: :on }) # Assert: The branch should NOT match. refute branch.matches?(@object), 'Branch should not match when :if is met but :if_state is not' end def test_prevents_transition_when_if_state_is_met_but_if_is_not # Setup: The :if_state condition is met BUT the standard :if condition is false. @object.state2 = 'on' # This meets the :if_state condition # Action: Create a branch with both :if and :if_state guards where :if returns false. branch = StateMachines::Branch.new(if: proc { false }, if_state: { state2: :on }) # Assert: The branch should NOT match. refute branch.matches?(@object), 'Branch should not match when :if_state is met but :if is not' end # --- Error Handling --- def test_raises_error_for_nonexistent_machine # Action: Create a branch referencing a machine that doesn't exist. branch = StateMachines::Branch.new(if_state: { nonexistent_machine: :some_state }) # Assert: It should raise an ArgumentError with a specific message. error = assert_raises(ArgumentError) do branch.matches?(@object) end assert_match(/State machine 'nonexistent_machine' is not defined/, error.message) end def test_raises_error_for_nonexistent_state # Action: Create a branch referencing a state that doesn't exist on a valid machine. branch = StateMachines::Branch.new(if_state: { state1: :nonexistent_state }) # Assert: It should raise an ArgumentError with a specific message. error = assert_raises(ArgumentError) do branch.matches?(@object) end assert_match(/State 'nonexistent_state' is not defined in state machine 'state1'/, error.message) end # --- Additional Edge Cases --- def test_multiple_guard_types_work_together # Test that different guard types can be combined successfully @object.state1 = 'idling' @object.state2 = 'on' branch = StateMachines::Branch.new( if_state: { state1: :idling }, unless_state: { state2: :off } # state2 is 'on', so this should pass ) assert branch.matches?(@object), 'Multiple guard types should work together' end def test_empty_state_guards_always_match # Test that when no state guards are specified, the branch matches branch = StateMachines::Branch.new({}) assert branch.matches?(@object), 'Branch with no guards should always match' end def test_guard_false_bypasses_state_guards # Test that when guard: false is specified, state guards are bypassed branch = StateMachines::Branch.new(if_state: { nonexistent_machine: :some_state }) # This should not raise an error because guard: false bypasses the checks assert branch.matches?(@object, guard: false), 'guard: false should bypass state guard validation' end end state_machines-0.100.4/test/unit/branch/branch_with_to_matcher_requirement_test.rb000066400000000000000000000010661507333401300305410ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithToMatcherRequirementTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(to: StateMachines::BlacklistMatcher.new(%i[idling parked])) end def test_should_match_if_included assert @branch.matches?(@object, to: :first_gear) end def test_should_not_match_if_not_included refute @branch.matches?(@object, to: :idling) end def test_include_values_in_known_states assert_equal %i[idling parked], @branch.known_states end end state_machines-0.100.4/test/unit/branch/branch_with_to_requirement_test.rb000066400000000000000000000022651507333401300270400ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithToRequirementTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(to: :idling) end def test_should_use_a_whitelist_matcher assert_instance_of StateMachines::WhitelistMatcher, @branch.state_requirements.first[:to] end def test_should_match_if_not_specified assert @branch.matches?(@object, from: :parked) end def test_should_match_if_included assert @branch.matches?(@object, to: :idling) end def test_should_not_match_if_not_included refute @branch.matches?(@object, to: :parked) end def test_should_not_match_if_nil refute @branch.matches?(@object, to: nil) end def test_should_ignore_from assert @branch.matches?(@object, to: :idling, from: :parked) end def test_should_ignore_on assert @branch.matches?(@object, to: :idling, on: :ignite) end def test_should_be_included_in_known_states assert_equal [:idling], @branch.known_states end def test_should_include_requirement_in_match match = @branch.match(@object, to: :idling) assert_equal @branch.state_requirements.first[:to], match[:to] end end state_machines-0.100.4/test/unit/branch/branch_with_unless_conditional_test.rb000066400000000000000000000013321507333401300276640ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithUnlessConditionalTest < StateMachinesTest def setup @object = Object.new end def test_should_have_an_unless_condition branch = StateMachines::Branch.new(unless: -> { true }) refute_nil branch.unless_condition end def test_should_match_if_false branch = StateMachines::Branch.new(unless: -> { false }) assert branch.matches?(@object) end def test_should_not_match_if_true branch = StateMachines::Branch.new(unless: -> { true }) refute branch.matches?(@object) end def test_should_be_nil_if_unmatched branch = StateMachines::Branch.new(unless: -> { true }) assert_nil branch.match(@object) end end state_machines-0.100.4/test/unit/branch/branch_without_guards_test.rb000066400000000000000000000014131507333401300260050ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BranchWithoutGuardsTest < StateMachinesTest def setup @object = Object.new end def test_should_match_if_if_is_false branch = StateMachines::Branch.new(if: -> { false }) assert branch.matches?(@object, guard: false) end def test_should_match_if_if_is_true branch = StateMachines::Branch.new(if: -> { true }) assert branch.matches?(@object, guard: false) end def test_should_match_if_unless_is_false branch = StateMachines::Branch.new(unless: -> { false }) assert branch.matches?(@object, guard: false) end def test_should_match_if_unless_is_true branch = StateMachines::Branch.new(unless: -> { true }) assert branch.matches?(@object, guard: false) end end state_machines-0.100.4/test/unit/callback/000077500000000000000000000000001507333401300203345ustar00rootroot00000000000000state_machines-0.100.4/test/unit/callback/callback_by_default_test.rb000066400000000000000000000014251507333401300256540ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackByDefaultTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before) {} end def test_should_have_type assert_equal :before, @callback.type end def test_should_not_have_a_terminator assert_nil @callback.terminator end def test_should_have_a_branch_with_all_matcher_requirements assert_equal StateMachines::AllMatcher.instance, @callback.branch.event_requirement assert_equal StateMachines::AllMatcher.instance, @callback.branch.state_requirements.first[:from] assert_equal StateMachines::AllMatcher.instance, @callback.branch.state_requirements.first[:to] end def test_should_not_have_any_known_states assert_empty @callback.known_states end end state_machines-0.100.4/test/unit/callback/callback_test.rb000066400000000000000000000032731507333401300234610ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackTest < StateMachinesTest def test_should_raise_exception_if_invalid_type_specified exception = assert_raises(ArgumentError) { StateMachines::Callback.new(:invalid) {} } assert_equal 'Type must be :before, :after, :around, or :failure', exception.message end def test_should_not_raise_exception_if_using_before_type StateMachines::Callback.new(:before) {} end def test_should_not_raise_exception_if_using_after_type StateMachines::Callback.new(:after) {} end def test_should_not_raise_exception_if_using_around_type StateMachines::Callback.new(:around) {} end def test_should_not_raise_exception_if_using_failure_type StateMachines::Callback.new(:failure) {} end def test_should_raise_exception_if_no_methods_specified exception = assert_raises(ArgumentError) { StateMachines::Callback.new(:before) } assert_equal 'Method(s) for callback must be specified', exception.message end def test_should_not_raise_exception_if_method_specified_in_do_option StateMachines::Callback.new(:before, do: :run) end def test_should_not_raise_exception_if_method_specified_as_argument StateMachines::Callback.new(:before, :run) end def test_should_not_raise_exception_if_method_specified_as_block StateMachines::Callback.new(:before, :run) {} end def test_should_not_raise_exception_if_implicit_option_specified StateMachines::Callback.new(:before, do: :run, invalid: :valid) end def test_should_not_bind_to_objects refute StateMachines::Callback.bind_to_object end def test_should_not_have_a_terminator assert_nil StateMachines::Callback.terminator end end state_machines-0.100.4/test/unit/callback/callback_with_application_bound_object_test.rb000066400000000000000000000012021507333401300316020ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithApplicationBoundObjectTest < StateMachinesTest def setup @original_bind_to_object = StateMachines::Callback.bind_to_object StateMachines::Callback.bind_to_object = true context = nil @callback = StateMachines::Callback.new(:before, do: ->(*_args) { context = self }) @object = Object.new @callback.call(@object) @context = context end def teardown StateMachines::Callback.bind_to_object = @original_bind_to_object end def test_should_call_method_within_the_context_of_the_object assert_equal @object, @context end end state_machines-0.100.4/test/unit/callback/callback_with_application_terminator_test.rb000066400000000000000000000013151507333401300313360ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithApplicationTerminatorTest < StateMachinesTest def setup @original_terminator = StateMachines::Callback.terminator StateMachines::Callback.terminator = ->(result) { result == false } @object = Object.new end def teardown StateMachines::Callback.terminator = @original_terminator end def test_should_not_halt_if_terminator_does_not_match callback = StateMachines::Callback.new(:before, do: -> { true }) callback.call(@object) end def test_should_halt_if_terminator_matches callback = StateMachines::Callback.new(:before, do: -> { false }) assert_throws(:halt) { callback.call(@object) } end end state_machines-0.100.4/test/unit/callback/callback_with_arguments_test.rb000066400000000000000000000005731507333401300266010ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithArgumentsTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, do: ->(*args) { @args = args }) @object = Object.new @callback.call(@object, {}, 1, 2, 3) end def test_should_call_method_with_all_arguments assert_equal [@object, 1, 2, 3], @args end end state_machines-0.100.4/test/unit/callback/callback_with_around_type_and_arguments_test.rb000066400000000000000000000017251507333401300320340ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithAroundTypeAndArgumentsTest < StateMachinesTest def setup @object = Object.new end def test_should_include_object_if_specified callback = StateMachines::Callback.new(:around, lambda { |object, block| @args = [object] block.call }) callback.call(@object) assert_equal [@object], @args end def test_should_include_arguments_if_specified callback = StateMachines::Callback.new(:around, lambda { |object, arg1, arg2, arg3, block| @args = [object, arg1, arg2, arg3] block.call }) callback.call(@object, {}, 1, 2, 3) assert_equal [@object, 1, 2, 3], @args end def test_should_include_arguments_if_splat_used callback = StateMachines::Callback.new(:around, lambda { |*args| block = args.pop @args = args block.call }) callback.call(@object, {}, 1, 2, 3) assert_equal [@object, 1, 2, 3], @args end end state_machines-0.100.4/test/unit/callback/callback_with_around_type_and_block_test.rb000066400000000000000000000032371507333401300311210ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithAroundTypeAndBlockTest < StateMachinesTest def setup @object = Object.new @callbacks = [] end def test_should_evaluate_before_without_after callback = StateMachines::Callback.new(:around, lambda { |*args| block = args.pop @args = args block.call }) assert callback.call(@object) assert_equal [@object], @args end def test_should_evaluate_after_without_before callback = StateMachines::Callback.new(:around, lambda { |*args| block = args.pop block.call @args = args }) assert callback.call(@object) assert_equal [@object], @args end def test_should_halt_if_not_yielded callback = StateMachines::Callback.new(:around, ->(_block) {}) assert_throws(:halt) { callback.call(@object) } end def test_should_call_block_after_before callback = StateMachines::Callback.new(:around, lambda { |block| @callbacks << :before block.call }) assert callback.call(@object) { @callbacks << :block } assert_equal %i[before block], @callbacks end def test_should_call_block_before_after @callbacks = [] callback = StateMachines::Callback.new(:around, lambda { |block| block.call @callbacks << :after }) assert callback.call(@object) { @callbacks << :block } assert_equal %i[block after], @callbacks end def test_should_halt_if_block_halts callback = StateMachines::Callback.new(:around, lambda { |block| block.call @callbacks << :after }) assert_throws(:halt) { callback.call(@object) { throw :halt } } assert_empty @callbacks end end state_machines-0.100.4/test/unit/callback/callback_with_around_type_and_bound_method_test.rb000066400000000000000000000014211507333401300324670ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithAroundTypeAndBoundMethodTest < StateMachinesTest def setup @object = Object.new end def test_should_call_method_within_the_context_of_the_object context = nil callback = StateMachines::Callback.new(:around, do: lambda { |block| context = self block.call }, bind_to_object: true) callback.call(@object, {}, 1, 2, 3) assert_equal @object, context end def test_should_include_arguments_if_specified context = nil callback = StateMachines::Callback.new(:around, do: lambda { |*args| block = args.pop context = args block.call }, bind_to_object: true) callback.call(@object, {}, 1, 2, 3) assert_equal [1, 2, 3], context end end state_machines-0.100.4/test/unit/callback/callback_with_around_type_and_multiple_methods_test.rb000066400000000000000000000046551507333401300334120ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithAroundTypeAndMultipleMethodsTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:around, :run_1, :run_2) class << @object = Object.new attr_accessor :before_callbacks, :after_callbacks def run_1 (@before_callbacks ||= []) << :run_1 yield (@after_callbacks ||= []) << :run_1 end def run_2 (@before_callbacks ||= []) << :run_2 yield (@after_callbacks ||= []) << :run_2 end end end def test_should_succeed assert @callback.call(@object) end def test_should_evaluate_before_callbacks_in_order @callback.call(@object) assert_equal %i[run_1 run_2], @object.before_callbacks end def test_should_evaluate_after_callbacks_in_reverse_order @callback.call(@object) assert_equal %i[run_2 run_1], @object.after_callbacks end def test_should_call_block_after_before_callbacks @callback.call(@object) { (@object.before_callbacks ||= []) << :block } assert_equal %i[run_1 run_2 block], @object.before_callbacks end def test_should_call_block_before_after_callbacks @callback.call(@object) { (@object.after_callbacks ||= []) << :block } assert_equal %i[block run_2 run_1], @object.after_callbacks end def test_should_halt_if_first_doesnt_yield class << @object remove_method :run_1 def run_1 (@before_callbacks ||= []) << :run_1 end end catch(:halt) do @callback.call(@object) { (@object.before_callbacks ||= []) << :block } end assert_equal [:run_1], @object.before_callbacks assert_nil @object.after_callbacks end def test_should_halt_if_last_doesnt_yield class << @object remove_method :run_2 def run_2 (@before_callbacks ||= []) << :run_2 end end catch(:halt) { @callback.call(@object) } assert_equal %i[run_1 run_2], @object.before_callbacks assert_nil @object.after_callbacks end def test_should_not_evaluate_further_methods_if_after_halts class << @object remove_method :run_2 def run_2 (@before_callbacks ||= []) << :run_2 yield (@after_callbacks ||= []) << :run_2 throw :halt end end catch(:halt) { @callback.call(@object) } assert_equal %i[run_1 run_2], @object.before_callbacks assert_equal [:run_2], @object.after_callbacks end end state_machines-0.100.4/test/unit/callback/callback_with_around_type_and_terminator_test.rb000066400000000000000000000012231507333401300322040ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithAroundTypeAndTerminatorTest < StateMachinesTest def setup @object = Object.new end def test_should_not_halt_if_terminator_does_not_match callback = StateMachines::Callback.new(:around, do: lambda { |block| block.call(false) false }, terminator: ->(result) { result == true }) callback.call(@object) end def test_should_not_halt_if_terminator_matches callback = StateMachines::Callback.new(:around, do: lambda { |block| block.call(false) false }, terminator: ->(result) { result == false }) callback.call(@object) end end state_machines-0.100.4/test/unit/callback/callback_with_block_test.rb000066400000000000000000000006441507333401300256650ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithBlockTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before) do |*args| @args = args end @object = Object.new @result = @callback.call(@object) end def test_should_be_successful assert @result end def test_should_call_with_empty_context assert_equal [@object], @args end end state_machines-0.100.4/test/unit/callback/callback_with_bound_method_and_arguments_test.rb000066400000000000000000000017161507333401300321520ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithBoundMethodAndArgumentsTest < StateMachinesTest def setup @object = Object.new end def test_should_include_single_argument_if_specified context = nil callback = StateMachines::Callback.new(:before, do: ->(arg1) { context = [arg1] }, bind_to_object: true) callback.call(@object, {}, 1) assert_equal [1], context end def test_should_include_multiple_arguments_if_specified context = nil callback = StateMachines::Callback.new(:before, do: ->(arg1, arg2, arg3) { context = [arg1, arg2, arg3] }, bind_to_object: true) callback.call(@object, {}, 1, 2, 3) assert_equal [1, 2, 3], context end def test_should_include_arguments_if_splat_used context = nil callback = StateMachines::Callback.new(:before, do: ->(*args) { context = args }, bind_to_object: true) callback.call(@object, {}, 1, 2, 3) assert_equal [1, 2, 3], context end end state_machines-0.100.4/test/unit/callback/callback_with_bound_method_test.rb000066400000000000000000000017301507333401300272370ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithBoundMethodTest < StateMachinesTest def setup @object = Object.new end def test_should_call_method_within_the_context_of_the_object_for_block_methods context = nil callback = StateMachines::Callback.new(:before, do: ->(*args) { context = [self] + args }, bind_to_object: true) callback.call(@object, {}, 1, 2, 3) assert_equal [@object, 1, 2, 3], context end def test_should_ignore_option_for_symbolic_methods class << @object attr_reader :context def after_ignite(*args) @context = args end end callback = StateMachines::Callback.new(:before, do: :after_ignite, bind_to_object: true) callback.call(@object) assert_empty @object.context end def test_should_ignore_option_for_string_methods callback = StateMachines::Callback.new(:before, do: '[1, 2, 3]', bind_to_object: true) assert callback.call(@object) end end state_machines-0.100.4/test/unit/callback/callback_with_do_method_test.rb000066400000000000000000000006411507333401300265320ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithDoMethodTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, do: ->(*args) { @args = args }) @object = Object.new @result = @callback.call(@object) end def test_should_be_successful assert @result end def test_should_call_with_empty_context assert_equal [@object], @args end end state_machines-0.100.4/test/unit/callback/callback_with_explicit_requirements_test.rb000066400000000000000000000016041507333401300312140ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithExplicitRequirementsTest < StateMachinesTest def setup @object = Object.new @callback = StateMachines::Callback.new(:before, from: :parked, to: :idling, on: :ignite, do: -> {}) end def test_should_call_with_empty_context assert @callback.call(@object, {}) end def test_should_not_call_if_from_not_included refute @callback.call(@object, from: :idling) end def test_should_not_call_if_to_not_included refute @callback.call(@object, to: :parked) end def test_should_not_call_if_on_not_included refute @callback.call(@object, on: :park) end def test_should_call_if_all_requirements_met assert @callback.call(@object, from: :parked, to: :idling, on: :ignite) end def test_should_include_in_known_states assert_equal %i[parked idling], @callback.known_states end end state_machines-0.100.4/test/unit/callback/callback_with_if_condition_test.rb000066400000000000000000000007231507333401300272350ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithIfConditionTest < StateMachinesTest def setup @object = Object.new end def test_should_call_if_true callback = StateMachines::Callback.new(:before, if: -> { true }, do: -> {}) assert callback.call(@object) end def test_should_not_call_if_false callback = StateMachines::Callback.new(:before, if: -> { false }, do: -> {}) refute callback.call(@object) end end state_machines-0.100.4/test/unit/callback/callback_with_implicit_requirements_test.rb000066400000000000000000000015711507333401300312100ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithImplicitRequirementsTest < StateMachinesTest def setup @object = Object.new @callback = StateMachines::Callback.new(:before, parked: :idling, on: :ignite, do: -> {}) end def test_should_call_with_empty_context assert @callback.call(@object, {}) end def test_should_not_call_if_from_not_included refute @callback.call(@object, from: :idling) end def test_should_not_call_if_to_not_included refute @callback.call(@object, to: :parked) end def test_should_not_call_if_on_not_included refute @callback.call(@object, on: :park) end def test_should_call_if_all_requirements_met assert @callback.call(@object, from: :parked, to: :idling, on: :ignite) end def test_should_include_in_known_states assert_equal %i[parked idling], @callback.known_states end end state_machines-0.100.4/test/unit/callback/callback_with_method_argument_test.rb000066400000000000000000000006431507333401300277540ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithMethodArgumentTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, ->(*args) { @args = args }) @object = Object.new @result = @callback.call(@object) end def test_should_be_successful assert @result end def test_should_call_with_empty_context assert_equal [@object], @args end end state_machines-0.100.4/test/unit/callback/callback_with_mixed_methods_test.rb000066400000000000000000000012551507333401300274230ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithMixedMethodsTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, :run_argument, do: :run_do) do |object| object.callbacks << :block end class << @object = Object.new attr_accessor :callbacks def run_argument (@callbacks ||= []) << :argument end def run_do (@callbacks ||= []) << :do end end @result = @callback.call(@object) end def test_should_be_successful assert @result end def test_should_call_each_callback_in_order assert_equal %i[argument do block], @object.callbacks end end state_machines-0.100.4/test/unit/callback/callback_with_multiple_bound_methods_test.rb000066400000000000000000000011461507333401300313360ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithMultipleBoundMethodsTest < StateMachinesTest def setup @object = Object.new first_context = nil second_context = nil @callback = StateMachines::Callback.new(:before, do: [-> { first_context = self }, -> { second_context = self }], bind_to_object: true) @callback.call(@object) @first_context = first_context @second_context = second_context end def test_should_call_each_method_within_the_context_of_the_object assert_equal @object, @first_context assert_equal @object, @second_context end end state_machines-0.100.4/test/unit/callback/callback_with_multiple_do_methods_test.rb000066400000000000000000000011501507333401300306240ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithMultipleDoMethodsTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, do: %i[run_1 run_2]) class << @object = Object.new attr_accessor :callbacks def run_1 (@callbacks ||= []) << :run_1 end def run_2 (@callbacks ||= []) << :run_2 end end @result = @callback.call(@object) end def test_should_be_successful assert @result end def test_should_call_each_callback_in_order assert_equal %i[run_1 run_2], @object.callbacks end end state_machines-0.100.4/test/unit/callback/callback_with_multiple_method_arguments_test.rb000066400000000000000000000011511507333401300320450ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithMultipleMethodArgumentsTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, :run_1, :run_2) class << @object = Object.new attr_accessor :callbacks def run_1 (@callbacks ||= []) << :run_1 end def run_2 (@callbacks ||= []) << :run_2 end end @result = @callback.call(@object) end def test_should_be_successful assert @result end def test_should_call_each_callback_in_order assert_equal %i[run_1 run_2], @object.callbacks end end state_machines-0.100.4/test/unit/callback/callback_with_terminator_test.rb000066400000000000000000000014631507333401300267570ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithTerminatorTest < StateMachinesTest def setup @object = Object.new end def test_should_not_halt_if_terminator_does_not_match callback = StateMachines::Callback.new(:before, do: -> { false }, terminator: ->(result) { result == true }) callback.call(@object) end def test_should_halt_if_terminator_matches callback = StateMachines::Callback.new(:before, do: -> { false }, terminator: ->(result) { result == false }) assert_throws(:halt) { callback.call(@object) } end def test_should_halt_if_terminator_matches_any_method callback = StateMachines::Callback.new(:before, do: [-> { true }, -> { false }], terminator: ->(result) { result == false }) assert_throws(:halt) { callback.call(@object) } end end state_machines-0.100.4/test/unit/callback/callback_with_unbound_method_test.rb000066400000000000000000000006501507333401300276020ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithUnboundMethodTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, do: ->(*args) { @context = args.unshift(self) }) @object = Object.new @callback.call(@object, {}, 1, 2, 3) end def test_should_call_method_outside_the_context_of_the_object assert_equal [self, @object, 1, 2, 3], @context end end state_machines-0.100.4/test/unit/callback/callback_with_unless_condition_test.rb000066400000000000000000000007371507333401300301550ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithUnlessConditionTest < StateMachinesTest def setup @object = Object.new end def test_should_call_if_false callback = StateMachines::Callback.new(:before, unless: -> { false }, do: -> {}) assert callback.call(@object) end def test_should_not_call_if_true callback = StateMachines::Callback.new(:before, unless: -> { true }, do: -> {}) refute callback.call(@object) end end state_machines-0.100.4/test/unit/callback/callback_without_arguments_test.rb000066400000000000000000000005711507333401300273270ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithoutArgumentsTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, do: ->(object) { @arg = object }) @object = Object.new @callback.call(@object, {}, 1, 2, 3) end def test_should_call_method_with_object_as_argument assert_equal @object, @arg end end state_machines-0.100.4/test/unit/callback/callback_without_terminator_test.rb000066400000000000000000000005041507333401300275020ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CallbackWithoutTerminatorTest < StateMachinesTest def setup @object = Object.new end def test_should_not_halt_if_result_is_false callback = StateMachines::Callback.new(:before, do: -> { false }, terminator: nil) callback.call(@object) end end state_machines-0.100.4/test/unit/error/000077500000000000000000000000001507333401300177315ustar00rootroot00000000000000state_machines-0.100.4/test/unit/error/error_by_default_test.rb000066400000000000000000000010471507333401300246460ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ErrorByDefaultTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(@machine) end def test_should_not_have_any_nodes assert_equal 0, @collection.length end def test_should_have_a_machine assert_equal @machine, @collection.machine end def test_should_index_by_name @collection << object = Struct.new(:name).new(:parked) assert_equal object, @collection[:parked] end end state_machines-0.100.4/test/unit/error/error_with_message_test.rb000066400000000000000000000016661507333401300252160ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ErrorWithMessageTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(@machine) end def test_should_raise_exception_if_invalid_option_specified exception = assert_raises(ArgumentError) { StateMachines::NodeCollection.new(@machine, invalid: true) } assert_equal 'Unknown key: :invalid. Valid keys are: :index', exception.message end def test_should_raise_exception_on_lookup_if_invalid_index_specified exception = assert_raises(ArgumentError) { @collection[:something, :invalid] } assert_equal 'Invalid index: :invalid', exception.message end def test_should_raise_exception_on_fetch_if_invalid_index_specified exception = assert_raises(ArgumentError) { @collection.fetch(:something, :invalid) } assert_equal 'Invalid index: :invalid', exception.message end end state_machines-0.100.4/test/unit/eval_helper/000077500000000000000000000000001507333401300210665ustar00rootroot00000000000000state_machines-0.100.4/test/unit/eval_helper/eval_helpers_base_test.rb000066400000000000000000000002461507333401300261170ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EvalHelpersBaseTest < StateMachinesTest include StateMachines::EvalHelpers def default_test; end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_proc_block_and_explicit_arguments_test.rb000066400000000000000000000007701507333401300341340ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcBlockAndExplicitArgumentsTest < EvalHelpersBaseTest def setup @object = Object.new @proc = ->(object, arg1, arg2, arg3, block) { [object, arg1, arg2, arg3, block] } end def test_should_call_method_on_object_with_all_arguments_and_block block = -> { true } assert_equal [@object, 1, 2, 3, block], evaluate_method(@object, @proc, 1, 2, 3, &block) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_proc_block_and_implicit_arguments_test.rb000066400000000000000000000007011507333401300341170ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcBlockAndImplicitArgumentsTest < EvalHelpersBaseTest def setup @object = Object.new @proc = ->(*args) { args } end def test_should_call_method_on_object_with_all_arguments_and_block block = -> { true } assert_equal [@object, 1, 2, 3, block], evaluate_method(@object, @proc, 1, 2, 3, &block) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_proc_test.rb000066400000000000000000000005411507333401300261460ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcTest < EvalHelpersBaseTest def setup @object = Object.new @proc = ->(obj) { obj } end def test_should_call_proc_with_object_as_argument assert_equal @object, evaluate_method(@object, @proc, 1, 2, 3) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_proc_with_arguments_test.rb000066400000000000000000000005711507333401300312710ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcWithArgumentsTest < EvalHelpersBaseTest def setup @object = Object.new @proc = ->(*args) { args } end def test_should_call_method_with_all_arguments assert_equal [@object, 1, 2, 3], evaluate_method(@object, @proc, 1, 2, 3) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_proc_with_block_test.rb000066400000000000000000000005621507333401300303560ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcWithBlockTest < EvalHelpersBaseTest def setup @object = Object.new @proc = ->(_obj, block) { block.call } end def test_should_call_method_on_object_with_block assert evaluate_method(@object, @proc, 1, 2, 3) { true } end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_proc_with_block_without_arguments_test.rb000066400000000000000000000006441507333401300342270ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcWithoutArgumentsTest < EvalHelpersBaseTest def setup @object = Object.new @proc = ->(*args) { args } class << @proc def arity 0 end end end def test_should_call_proc_with_no_arguments assert_empty evaluate_method(@object, @proc, 1, 2, 3) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_proc_with_block_without_object_test.rb000066400000000000000000000006261507333401300334700ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcWithBlockWithoutObjectTest < EvalHelpersBaseTest def setup @object = Object.new @proc = ->(block) { [block] } end def test_should_call_proc_with_block_only block = -> { true } assert_equal [block], evaluate_method(@object, @proc, 1, 2, 3, &block) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_proc_without_arguments_test.rb000066400000000000000000000007161507333401300320220ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcWithBlockWithoutArgumentsTest < EvalHelpersBaseTest def setup @object = Object.new @proc = ->(*args) { args } class << @proc def arity 0 end end end def test_should_call_proc_without_arguments block = -> { true } assert_empty evaluate_method(@object, @proc, 1, 2, 3, &block) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_string_test.rb000066400000000000000000000010711507333401300265100ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersStringTest < EvalHelpersBaseTest def setup @object = Object.new end def test_should_evaluate_string assert_equal 1, evaluate_method(@object, '1') end def test_should_evaluate_string_within_object_context @object.instance_variable_set(:@value, 1) assert_equal 1, evaluate_method(@object, '@value') end def test_should_ignore_additional_arguments assert_equal 1, evaluate_method(@object, '1', 2, 3, 4) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_string_with_block_test.rb000066400000000000000000000005101507333401300307120ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersStringWithBlockTest < EvalHelpersBaseTest def setup @object = Object.new end def test_should_call_method_on_object_with_block assert_equal 1, evaluate_method(@object, 'yield') { 1 } end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_symbol_method_missing_test.rb000066400000000000000000000010151507333401300315760ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersSymbolMethodMissingTest < EvalHelpersBaseTest def setup class << (@object = Object.new) def method_missing(symbol, *) send("method_missing_#{symbol}", *) end def method_missing_callback(*args) args end end end def test_should_call_dynamic_method_with_all_arguments assert_equal [1, 2, 3], evaluate_method(@object, :callback, 1, 2, 3) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_symbol_private_test.rb000066400000000000000000000006251507333401300302450ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersSymbolPrivateTest < EvalHelpersBaseTest def setup class << (@object = Object.new) private def callback true end end end def test_should_call_method_on_object_with_no_arguments assert evaluate_method(@object, :callback, 1, 2, 3) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_symbol_protected_test.rb000066400000000000000000000006311507333401300305610ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersSymbolProtectedTest < EvalHelpersBaseTest def setup class << (@object = Object.new) protected def callback true end end end def test_should_call_method_on_object_with_no_arguments assert evaluate_method(@object, :callback, 1, 2, 3) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_symbol_tainted_method_test.rb000066400000000000000000000007361507333401300315660ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.2') class EvalHelpersSymbolTaintedMethodTest < EvalHelpersBaseTest def setup class << (@object = Object.new) def callback true end taint end end def test_should_not_raise_security_error evaluate_method(@object, :callback, 1, 2, 3) end end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_symbol_test.rb000066400000000000000000000005771507333401300265210ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersSymbolTest < EvalHelpersBaseTest def setup class << (@object = Object.new) def callback true end end end def test_should_call_method_on_object_with_no_arguments assert evaluate_method(@object, :callback, 1, 2, 3) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_symbol_with_arguments_and_block_test.rb000066400000000000000000000007171507333401300336310ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersSymbolWithArgumentsAndBlockTest < EvalHelpersBaseTest def setup class << (@object = Object.new) def callback(*args) args << yield end end end def test_should_call_method_on_object_with_all_arguments_and_block assert_equal [1, 2, 3, true], evaluate_method(@object, :callback, 1, 2, 3) { true } end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_symbol_with_arguments_test.rb000066400000000000000000000006331507333401300316320ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersSymbolWithArgumentsTest < EvalHelpersBaseTest def setup class << (@object = Object.new) def callback(*args) args end end end def test_should_call_method_with_all_arguments assert_equal [1, 2, 3], evaluate_method(@object, :callback, 1, 2, 3) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_symbol_with_block_test.rb000066400000000000000000000006021507333401300307130ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersSymbolWithBlockTest < EvalHelpersBaseTest def setup class << (@object = Object.new) def callback yield end end end def test_should_call_method_on_object_with_block assert evaluate_method(@object, :callback) { true } end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_test.rb000066400000000000000000000006241507333401300251250ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersTest < EvalHelpersBaseTest def setup @object = Object.new end def test_should_raise_exception_if_method_is_not_symbol_string_or_proc exception = assert_raises(ArgumentError) { evaluate_method(@object, 1) } assert_match(/Methods must/, exception.message) end end state_machines-0.100.4/test/unit/eval_helper/eval_helpers_with_event_arguments_test.rb000066400000000000000000000076161507333401300314560ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../../test_helper' class EvalHelpersWithEventArgumentsTest < StateMachinesTest include StateMachines::EvalHelpers def setup @object = Object.new @object.instance_variable_set(:@value, 'test') def @object.value @value end def @object.valid? true end end def test_single_parameter_proc_only_receives_object proc = ->(obj) { obj.valid? } # Should call with only object, ignoring event args assert evaluate_method_with_event_args(@object, proc, %i[arg1 arg2]) end def test_splat_parameter_proc_receives_object_and_event_args result_args = nil proc = lambda { |obj, *args| result_args = args obj.valid? } # Should call with object + event args assert evaluate_method_with_event_args(@object, proc, %i[arg1 arg2]) assert_equal %i[arg1 arg2], result_args end def test_explicit_multiple_parameter_proc_receives_object_and_event_args result_obj = nil result_arg1 = nil result_arg2 = nil proc = lambda { |obj, arg1, arg2| result_obj = obj result_arg1 = arg1 result_arg2 = arg2 true } # Should call with object + first two event args assert evaluate_method_with_event_args(@object, proc, %i[first second third]) assert_equal @object, result_obj assert_equal :first, result_arg1 assert_equal :second, result_arg2 end def test_zero_parameter_proc_receives_no_arguments called = false proc = lambda { called = true true } # Should call with no arguments assert evaluate_method_with_event_args(@object, proc, %i[arg1 arg2]) assert called end def test_symbol_method_ignores_event_args # Symbol methods should work normally, ignoring event args assert evaluate_method_with_event_args(@object, :valid?, %i[any args]) end def test_string_method_ignores_event_args # String methods should work normally, ignoring event args assert evaluate_method_with_event_args(@object, '@value == "test"', %i[any args]) end def test_method_object_with_single_arity method = @object.method(:valid?) # Should call with only object for arity 0 methods assert evaluate_method_with_event_args(@object, method, %i[arg1 arg2]) end def test_method_object_with_multiple_arity # Create a method that accepts multiple arguments def @object.check_args(obj, arg1, arg2) obj == self && arg1 == :first && arg2 == :second end method = @object.method(:check_args) # Should call with event args for multi-arity methods assert evaluate_method_with_event_args(@object, method, %i[first second]) end def test_proc_arity_detection # Test various arity scenarios # Arity 0 proc_0 = -> { true } assert evaluate_method_with_event_args(@object, proc_0, [:ignored]) # Arity 1 proc_1 = ->(obj) { obj.valid? } assert evaluate_method_with_event_args(@object, proc_1, [:ignored]) # Arity 2 proc_2 = ->(obj, arg) { obj.valid? && arg == :test } assert evaluate_method_with_event_args(@object, proc_2, [:test]) # Arity -1 (splat) proc_splat = ->(obj, *args) { obj.valid? && args.include?(:test) } assert evaluate_method_with_event_args(@object, proc_splat, %i[test other]) end def test_backward_compatibility_with_no_event_args # Should work when no event args are provided proc = ->(obj, *args) { obj.valid? && args.empty? } assert evaluate_method_with_event_args(@object, proc) assert evaluate_method_with_event_args(@object, proc, []) end def test_fallback_to_standard_evaluate_method # Unknown method types should fallback to standard evaluation and raise appropriate error custom_object = Object.new # Should fallback to standard evaluate_method and raise the same error assert_raises(ArgumentError) do evaluate_method_with_event_args(@object, custom_object, [:any]) end end end state_machines-0.100.4/test/unit/event/000077500000000000000000000000001507333401300177215ustar00rootroot00000000000000state_machines-0.100.4/test/unit/event/event_after_being_copied_test.rb000066400000000000000000000010401507333401300262710ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventAfterBeingCopiedTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @copied_event = @event.dup end def test_should_not_have_the_same_collection_of_branches refute_same @event.branches, @copied_event.branches end def test_should_not_have_the_same_collection_of_known_states refute_same @event.known_states, @copied_event.known_states end end state_machines-0.100.4/test/unit/event/event_by_default_test.rb000066400000000000000000000024651507333401300246330ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventByDefaultTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @object = @klass.new end def test_should_have_a_machine assert_equal @machine, @event.machine end def test_should_have_a_name assert_equal :ignite, @event.name end def test_should_have_a_qualified_name assert_equal :ignite, @event.qualified_name end def test_should_have_a_human_name assert_equal 'ignite', @event.human_name end def test_should_not_have_any_branches assert_empty @event.branches end def test_should_have_no_known_states assert_empty @event.known_states end def test_should_not_be_able_to_fire refute @event.can_fire?(@object) end def test_should_not_have_a_transition assert_nil @event.transition_for(@object) end def test_should_define_a_predicate assert_respond_to @object, :can_ignite? end def test_should_define_a_transition_accessor assert_respond_to @object, :ignite_transition end def test_should_define_an_action assert_respond_to @object, :ignite end def test_should_define_a_bang_action assert_respond_to @object, :ignite! end end state_machines-0.100.4/test/unit/event/event_context_test.rb000066400000000000000000000006501507333401300241730ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventContextTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.events << @event = StateMachines::Event.new(@machine, :ignite, human_name: 'start') end def test_should_evaluate_within_the_event scope = nil @event.context { scope = self } assert_equal @event, scope end end state_machines-0.100.4/test/unit/event/event_initialize_test.rb000066400000000000000000000025061507333401300246520ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventInitializeTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) end def test_should_raise_exception_if_invalid_option_specified exception = assert_raises(ArgumentError) { StateMachines::Event.new(@machine, :ignite, invalid: true) } assert_equal 'Unknown key: :invalid. Valid keys are: :human_name', exception.message end def test_should_raise_exception_if_invalid_option_specified_with_kwargs exception = assert_raises(ArgumentError) { StateMachines::Event.new(@machine, :ignite, nil, invalid: true) } assert_equal 'Unknown key: :invalid. Valid keys are: :human_name', exception.message end def test_should_set_human_name_from_options event = StateMachines::Event.new(@machine, :ignite, human_name: 'Start') assert_equal 'Start', event.human_name end def test_should_set_human_name_from_kwargs event = StateMachines::Event.new(@machine, :ignite, nil, human_name: 'Start') assert_equal 'Start', event.human_name end def test_should_raise_exception_if_invalid_positional_argument exception = assert_raises(ArgumentError) { StateMachines::Event.new(@machine, :ignite, :invalid) } assert_equal 'Unexpected positional argument in Event initialize: :invalid', exception.message end end state_machines-0.100.4/test/unit/event/event_on_failure_test.rb000066400000000000000000000031541507333401300246340ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/integrations/event_on_failure_integration' class EventOnFailureTest < StateMachinesTest def setup StateMachines::Integrations.reset StateMachines::Integrations.register(EventOnFailureIntegration) @klass = Class.new do attr_accessor :errors end @machine = StateMachines::Machine.new(@klass, integration: :event_on_failure_integration) @machine.state :parked @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @object = @klass.new @object.state = 'parked' end def teardown StateMachines::Integrations.reset end def test_should_invalidate_the_state @event.fire(@object) assert_equal ['cannot transition via "ignite"'], @object.errors end def test_should_run_failure_callbacks callback_args = nil @machine.after_failure { |*args| callback_args = args } @event.fire(@object) object, transition = callback_args assert_equal @object, object refute_nil transition assert_equal @object, transition.object assert_equal @machine, transition.machine assert_equal :ignite, transition.event assert_equal :parked, transition.from_name assert_equal :parked, transition.to_name assert_empty transition.args end def test_should_pass_args_to_failure_callbacks callback_args = nil @machine.after_failure { |*args| callback_args = args } @event.fire(@object, foo: 'bar') object, transition = callback_args assert_equal @object, object refute_nil transition assert_equal [{ foo: 'bar' }], transition.args end end state_machines-0.100.4/test/unit/event/event_test.rb000066400000000000000000000020171507333401300224260ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @event.transition parked: :idling end def test_should_allow_changing_machine new_machine = StateMachines::Machine.new(Class.new) @event.machine = new_machine assert_equal new_machine, @event.machine end def test_should_allow_changing_human_name @event.human_name = 'Stop' assert_equal 'Stop', @event.human_name end def test_should_provide_matcher_helpers_during_initialization matchers = [] @event.instance_eval do matchers = [all, any, same] end assert_equal [StateMachines::AllMatcher.instance, StateMachines::AllMatcher.instance, StateMachines::LoopbackMatcher.instance], matchers end def test_should_use_pretty_inspect assert_match '# :idling]>', @event.inspect end end state_machines-0.100.4/test/unit/event/event_transitions_test.rb000066400000000000000000000042071507333401300250660ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventTransitionsTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @machine.events << @event = StateMachines::Event.new(@machine, :ignite) end def test_should_not_raise_exception_if_implicit_option_specified @event.transition(invalid: :valid) end def test_should_not_allow_on_option exception = assert_raises(ArgumentError) { @event.transition(on: :ignite) } assert_equal 'Unknown key: :on. Valid keys are: :from, :to, :except_from, :except_to, :if, :unless, :if_state, :unless_state, :if_all_states, :unless_all_states, :if_any_state, :unless_any_state', exception.message end def test_should_automatically_set_on_option branch = @event.transition(to: :idling) assert_instance_of StateMachines::WhitelistMatcher, branch.event_requirement assert_equal [:ignite], branch.event_requirement.values end def test_should_not_allow_except_on_option exception = assert_raises(ArgumentError) { @event.transition(except_on: :ignite) } assert_equal 'Unknown key: :except_on. Valid keys are: :from, :to, :except_from, :except_to, :if, :unless, :if_state, :unless_state, :if_all_states, :unless_all_states, :if_any_state, :unless_any_state', exception.message end def test_should_allow_transitioning_without_a_to_state @event.transition(from: :parked) end def test_should_allow_transitioning_without_a_from_state @event.transition(to: :idling) end def test_should_allow_except_from_option @event.transition(except_from: :idling) end def test_should_allow_except_to_option @event.transition(except_to: :idling) end def test_should_allow_transitioning_from_a_single_state assert @event.transition(parked: :idling) end def test_should_allow_transitioning_from_multiple_states assert @event.transition(%i[parked idling] => :idling) end def test_should_allow_transitions_to_multiple_states assert @event.transition(parked: %i[parked idling]) end def test_should_have_transitions branch = @event.transition(to: :idling) assert_equal [branch], @event.branches end end state_machines-0.100.4/test/unit/event/event_with_conflicting_helpers_after_definition_test.rb000066400000000000000000000027551507333401300331640ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'stringio' class EventWithConflictingHelpersAfterDefinitionTest < StateMachinesTest def setup @original_stderr = $stderr $stderr = StringIO.new @klass = Class.new do def can_ignite? 0 end def ignite_transition 0 end def ignite 0 end def ignite! 0 end end @machine = StateMachines::Machine.new(@klass) @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @object = @klass.new end def teardown $stderr = @original_stderr end def test_should_not_redefine_predicate assert_equal 0, @object.can_ignite? end def test_should_not_redefine_transition_accessor assert_equal 0, @object.ignite_transition end def test_should_not_redefine_action assert_equal 0, @object.ignite end def test_should_not_redefine_bang_action assert_equal 0, @object.ignite! end def test_should_allow_super_chaining @klass.class_eval do def can_ignite? super end def ignite_transition super end def ignite super end def ignite! super end end refute_predicate @object, :can_ignite? assert_nil @object.ignite_transition refute @object.ignite assert_raises(StateMachines::InvalidTransition) { @object.ignite! } end def test_should_not_output_warning assert_equal '', $stderr.string end end state_machines-0.100.4/test/unit/event/event_with_conflicting_helpers_before_definition_test.rb000066400000000000000000000025401507333401300333150ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'stringio' class EventWithConflictingHelpersBeforeDefinitionTest < StateMachinesTest def setup @original_stderr = $stderr $stderr = StringIO.new @superclass = Class.new do def can_ignite? 0 end def ignite_transition 0 end def ignite 0 end def ignite! 0 end end @klass = Class.new(@superclass) @machine = StateMachines::Machine.new(@klass) @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @object = @klass.new end def teardown $stderr = @original_stderr end def test_should_not_redefine_predicate assert_equal 0, @object.can_ignite? end def test_should_not_redefine_transition_accessor assert_equal 0, @object.ignite_transition end def test_should_not_redefine_action assert_equal 0, @object.ignite end def test_should_not_redefine_bang_action assert_equal 0, @object.ignite! end def test_should_output_warning expected = %w[can_ignite? ignite_transition ignite ignite!].map do |method| "Instance method \"#{method}\" is already defined in #{@superclass}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n" end.join assert_equal expected, $stderr.string end end state_machines-0.100.4/test/unit/event/event_with_conflicting_machine_test.rb000066400000000000000000000031441507333401300275260ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'stringio' class EventWithConflictingMachineTest < StateMachinesTest def setup @original_stderr = $stderr $stderr = StringIO.new @klass = Class.new @state_machine = StateMachines::Machine.new(@klass, :state) @state_machine.state :parked, :idling @state_machine.events << @state_event = StateMachines::Event.new(@state_machine, :ignite) end def teardown $stderr = @original_stderr end def test_should_not_overwrite_first_event @status_machine = StateMachines::Machine.new(@klass, :status) @status_machine.state :first_gear, :second_gear @status_machine.events << @status_event = StateMachines::Event.new(@status_machine, :ignite) @object = @klass.new @object.state = 'parked' @object.status = 'first_gear' @state_event.transition(parked: :idling) @status_event.transition(parked: :first_gear) @object.ignite assert_equal 'idling', @object.state assert_equal 'first_gear', @object.status end def test_should_output_warning @status_machine = StateMachines::Machine.new(@klass, :status) @status_machine.events << @status_event = StateMachines::Event.new(@status_machine, :ignite) assert_equal "Event :ignite for :status is already defined in :state\n", $stderr.string end def test_should_not_output_warning_if_using_different_namespace @status_machine = StateMachines::Machine.new(@klass, :status, namespace: 'alarm') @status_machine.events << @status_event = StateMachines::Event.new(@status_machine, :ignite) assert_equal '', $stderr.string end end state_machines-0.100.4/test/unit/event/event_with_dynamic_human_name_test.rb000066400000000000000000000013711507333401300273570ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithDynamicHumanNameTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.events << @event = StateMachines::Event.new(@machine, :ignite, human_name: ->(_event, object) { ['start', object] }) end def test_should_use_custom_human_name human_name, klass = @event.human_name assert_equal 'start', human_name assert_equal @klass, klass end def test_should_allow_custom_class_to_be_passed_through human_name, klass = @event.human_name(1) assert_equal 'start', human_name assert_equal 1, klass end def test_should_not_cache_value refute_same @event.human_name, @event.human_name end end state_machines-0.100.4/test/unit/event/event_with_guard_arguments_test.rb000066400000000000000000000150551507333401300267360ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../../test_helper' class EventWithGuardArgumentsTest < StateMachinesTest def setup @klass = Class.new do attr_accessor :trial_enabled, :force def initialize @trial_enabled = true @force = false super end def trial_enabled? @trial_enabled end def forced? @force end end @machine = StateMachines::Machine.new(@klass, initial: :uninitialized) @machine.other_states(:trial, :active, :future_active) @object = @klass.new end def test_backward_compatibility_with_single_parameter_guards # Single parameter guard (existing behavior) @machine.event :start do transition uninitialized: :trial, if: ->(obj) { obj.trial_enabled? } transition uninitialized: :active end @object.trial_enabled = true assert @object.start(:skip_trial) # Event args should be ignored for single-param guards assert_equal 'trial', @object.state end def test_backward_compatibility_with_symbol_guards # Symbol guards should continue working unchanged @machine.event :start do transition uninitialized: :trial, if: :trial_enabled? transition uninitialized: :active end @object.trial_enabled = true assert @object.start(:any_args, :should_be, :ignored) assert_equal 'trial', @object.state end def test_new_guard_with_event_arguments_splat_parameters # Multi-parameter guard with splat (new behavior) @machine.event :start do transition uninitialized: :trial, if: ->(obj, *args) { obj.trial_enabled? && !args.include?(:skip_trial) } transition uninitialized: :active end # First transition: should go to trial when no skip argument @object.trial_enabled = true @object.state = 'uninitialized' assert @object.start assert_equal 'trial', @object.state # Second transition: should skip trial when :skip_trial argument is passed @object.state = 'uninitialized' assert @object.start(:skip_trial) assert_equal 'active', @object.state end def test_new_guard_with_event_arguments_explicit_parameters # Multi-parameter guard with explicit parameters (new behavior) @machine.event :start do transition uninitialized: :future_active, if: ->(_obj, skip_trial, future_date) { skip_trial && future_date } transition uninitialized: :trial, if: ->(obj, skip_trial) { obj.trial_enabled? && !skip_trial } transition uninitialized: :active end # Should go to future_active when both arguments are truthy @object.state = 'uninitialized' assert @object.start(true, true) assert_equal 'future_active', @object.state # Should go to trial when skip_trial is false (need 2 args for the first guard) @object.state = 'uninitialized' assert @object.start(false, false) assert_equal 'trial', @object.state # Should go to active when skipping trial but no future date @object.state = 'uninitialized' assert @object.start(true, false) assert_equal 'active', @object.state end def test_unless_guards_with_event_arguments # Test unless guards with event arguments @machine.event :start do transition uninitialized: :trial, unless: ->(_obj, *args) { args.include?(:skip_trial) } transition uninitialized: :active end # Should go to trial when no skip argument @object.state = 'uninitialized' assert @object.start assert_equal 'trial', @object.state # Should go to active when skip argument is present @object.state = 'uninitialized' assert @object.start(:skip_trial) assert_equal 'active', @object.state end def test_mixed_guard_types_with_event_arguments # Test mixing single-param and multi-param guards @machine.event :start do transition uninitialized: :future_active, if: ->(_obj, *args) { args.include?(:future) } transition uninitialized: :trial, if: ->(obj) { obj.trial_enabled? } transition uninitialized: :active end # Should go to future_active when :future arg is passed @object.state = 'uninitialized' assert @object.start(:future) assert_equal 'future_active', @object.state # Should go to trial when no special args and trial enabled @object.state = 'uninitialized' @object.trial_enabled = true assert @object.start assert_equal 'trial', @object.state # Should go to active when trial disabled and no special args @object.state = 'uninitialized' @object.trial_enabled = false assert @object.start assert_equal 'active', @object.state end def test_zero_arity_guards_still_work # Test edge case of zero-arity guards called = false @machine.event :start do transition uninitialized: :active, if: lambda { called = true true } end @object.state = 'uninitialized' assert @object.start(:any, :args) assert_equal 'active', @object.state assert called, 'Zero-arity guard should have been called' end def test_complex_use_case_from_github_issue # Test the exact use case from GitHub issue #39 @machine.event :start do transition uninitialized: :trial, if: lambda { |subscription, *args| subscription.trial_enabled? && (args.empty? || args[0] != true) } transition %i[uninitialized trial] => :active end # Should start trial normally @object.trial_enabled = true @object.state = 'uninitialized' assert @object.start assert_equal 'trial', @object.state # Should skip trial when true argument is passed @object.state = 'uninitialized' assert @object.start(true) assert_equal 'active', @object.state end def test_method_guards_with_arguments_unsupported # Method objects currently don't support event arguments for security test_method = @object.method(:trial_enabled?) @machine.event :start do transition uninitialized: :trial, if: test_method transition uninitialized: :active end @object.trial_enabled = true @object.state = 'uninitialized' assert @object.start(:any_args) assert_equal 'trial', @object.state # Should work normally, ignoring args end def test_string_guards_with_arguments_unsupported # String guards don't support event arguments for security @machine.event :start do transition uninitialized: :trial, if: 'trial_enabled?' transition uninitialized: :active end @object.trial_enabled = true @object.state = 'uninitialized' assert @object.start(:any_args) assert_equal 'trial', @object.state # Should work normally, ignoring args end end state_machines-0.100.4/test/unit/event/event_with_human_name_test.rb000066400000000000000000000006021507333401300256470ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithHumanNameTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.events << @event = StateMachines::Event.new(@machine, :ignite, human_name: 'start') end def test_should_use_custom_human_name assert_equal 'start', @event.human_name end end state_machines-0.100.4/test/unit/event/event_with_invalid_current_state_test.rb000066400000000000000000000020341507333401300301300ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithInvalidCurrentStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @event.transition(parked: :idling) @object = @klass.new @object.state = 'invalid' end def test_should_raise_exception_when_checking_availability exception = assert_raises(ArgumentError) { @event.can_fire?(@object) } assert_equal '"invalid" is not a known state value', exception.message end def test_should_raise_exception_when_finding_transition exception = assert_raises(ArgumentError) { @event.transition_for(@object) } assert_equal '"invalid" is not a known state value', exception.message end def test_should_raise_exception_when_firing exception = assert_raises(ArgumentError) { @event.fire(@object) } assert_equal '"invalid" is not a known state value', exception.message end end state_machines-0.100.4/test/unit/event/event_with_machine_action_test.rb000066400000000000000000000013241507333401300265020ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithMachineActionTest < StateMachinesTest def setup @klass = Class.new do attr_reader :saved def save @saved = true end end @machine = StateMachines::Machine.new(@klass, action: :save) @machine.state :parked, :idling @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @event.transition(parked: :idling) @object = @klass.new @object.state = 'parked' end def test_should_run_action_on_fire @event.fire(@object) assert @object.saved end def test_should_not_run_action_if_configured_to_skip @event.fire(@object, false) refute @object.saved end end state_machines-0.100.4/test/unit/event/event_with_marshalling_test.rb000066400000000000000000000020771507333401300260500ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithMarshallingTest < StateMachinesTest def setup @klass = Class.new do def save true end end self.class.const_set('Example', @klass) @machine = StateMachines::Machine.new(@klass, action: :save) @machine.state :parked, :idling @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @event.transition(parked: :idling) @object = @klass.new @object.state = 'parked' end def teardown self.class.send(:remove_const, 'Example') end def test_should_marshal_during_before_callbacks @machine.before_transition { |object, _transition| Marshal.dump(object) } @event.fire(@object) end def test_should_marshal_during_action @klass.class_eval do remove_method :save def save Marshal.dump(self) end end @event.fire(@object) end def test_should_marshal_during_after_callbacks @machine.after_transition { |object, _transition| Marshal.dump(object) } @event.fire(@object) end end state_machines-0.100.4/test/unit/event/event_with_matching_disabled_transitions_test.rb000066400000000000000000000061111507333401300316160ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithMatchingDisabledTransitionsTest < StateMachinesTest module Custom include StateMachines::Integrations::Base def invalidate(object, _attribute, message, values = []) (object.errors ||= []) << generate_message(message, values) end def reset(object) object.errors = [] end end def setup StateMachines::Integrations.register(EventWithMatchingDisabledTransitionsTest::Custom) @klass = Class.new do attr_accessor :errors end @machine = StateMachines::Machine.new(@klass, integration: :custom) @machine.state :parked, :idling @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @event.transition(parked: :idling, if: -> { false }) @object = @klass.new @object.state = 'parked' end def teardown StateMachines::Integrations.reset end def test_should_not_be_able_to_fire refute @event.can_fire?(@object) end def test_should_be_able_to_fire_with_disabled_guards assert @event.can_fire?(@object, guard: false) end def test_should_not_have_a_transition assert_nil @event.transition_for(@object) end def test_should_have_a_transition_with_disabled_guards refute_nil @event.transition_for(@object, guard: false) end def test_should_not_fire refute @event.fire(@object) end def test_should_not_change_the_current_state @event.fire(@object) assert_equal 'parked', @object.state end def test_should_invalidate_the_state @event.fire(@object) assert_equal ['cannot transition via "ignite"'], @object.errors end def test_should_invalidate_with_human_event_name @event.human_name = 'start' @event.fire(@object) assert_equal ['cannot transition via "start"'], @object.errors end def test_should_invalid_with_human_state_name_if_specified klass = Class.new do attr_accessor :errors end machine = StateMachines::Machine.new(klass, integration: :custom, messages: { invalid_transition: 'cannot transition via "%s" from "%s"' }) parked, _idling = machine.state :parked, :idling parked.human_name = 'stopped' machine.events << event = StateMachines::Event.new(machine, :ignite) event.transition(parked: :idling, if: -> { false }) object = @klass.new object.state = 'parked' event.fire(object) assert_equal ['cannot transition via "ignite" from "stopped"'], object.errors end def test_should_reset_existing_error @object.errors = ['invalid'] @event.fire(@object) assert_equal ['cannot transition via "ignite"'], @object.errors end def test_should_run_failure_callbacks callback_args = nil @machine.after_failure { |*args| callback_args = args } @event.fire(@object) object, transition = callback_args assert_equal @object, object refute_nil transition assert_equal @object, transition.object assert_equal @machine, transition.machine assert_equal :ignite, transition.event assert_equal :parked, transition.from_name assert_equal :parked, transition.to_name end end state_machines-0.100.4/test/unit/event/event_with_matching_enabled_transitions_test.rb000066400000000000000000000033311507333401300314420ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithMatchingEnabledTransitionsTest < StateMachinesTest module Custom include StateMachines::Integrations::Base def invalidate(object, _attribute, message, values = []) (object.errors ||= []) << generate_message(message, values) end def reset(object) object.errors = [] end end def setup StateMachines::Integrations.register(EventWithMatchingEnabledTransitionsTest::Custom) @klass = Class.new do attr_accessor :errors end @machine = StateMachines::Machine.new(@klass, integration: :custom) @machine.state :parked, :idling @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @event.transition(parked: :idling) @object = @klass.new @object.state = 'parked' end def teardown StateMachines::Integrations.reset end def test_should_be_able_to_fire assert @event.can_fire?(@object) end def test_should_have_a_transition transition = @event.transition_for(@object) refute_nil transition assert_equal 'parked', transition.from assert_equal 'idling', transition.to assert_equal :ignite, transition.event end def test_should_fire assert @event.fire(@object) end def test_should_change_the_current_state @event.fire(@object) assert_equal 'idling', @object.state end def test_should_reset_existing_error @object.errors = ['invalid'] @event.fire(@object) assert_empty @object.errors end def test_should_not_invalidate_the_state @event.fire(@object) assert_empty @object.errors end def test_should_not_be_able_to_fire_on_reset @event.reset refute @event.can_fire?(@object) end end state_machines-0.100.4/test/unit/event/event_with_multiple_transitions_test.rb000066400000000000000000000034601507333401300300340ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithMultipleTransitionsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @event.transition(idling: :idling) @event.transition(parked: :idling) @event.transition(parked: :parked) @object = @klass.new @object.state = 'parked' end def test_should_be_able_to_fire assert @event.can_fire?(@object) end def test_should_have_a_transition transition = @event.transition_for(@object) refute_nil transition assert_equal 'parked', transition.from assert_equal 'idling', transition.to assert_equal :ignite, transition.event end def test_should_allow_specific_transition_selection_using_from transition = @event.transition_for(@object, from: :idling) refute_nil transition assert_equal 'idling', transition.from assert_equal 'idling', transition.to assert_equal :ignite, transition.event end def test_should_allow_specific_transition_selection_using_to transition = @event.transition_for(@object, from: :parked, to: :parked) refute_nil transition assert_equal 'parked', transition.from assert_equal 'parked', transition.to assert_equal :ignite, transition.event end def test_should_not_allow_specific_transition_selection_using_on exception = assert_raises(ArgumentError) { @event.transition_for(@object, on: :park) } assert_equal 'Unknown key: :on. Valid keys are: :from, :to, :guard', exception.message end def test_should_fire assert @event.fire(@object) end def test_should_change_the_current_state @event.fire(@object) assert_equal 'idling', @object.state end end state_machines-0.100.4/test/unit/event/event_with_namespace_test.rb000066400000000000000000000015611507333401300255000ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithNamespaceTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, namespace: 'alarm') @machine.events << @event = StateMachines::Event.new(@machine, :enable) @object = @klass.new end def test_should_have_a_name assert_equal :enable, @event.name end def test_should_have_a_qualified_name assert_equal :enable_alarm, @event.qualified_name end def test_should_namespace_predicate assert_respond_to @object, :can_enable_alarm? end def test_should_namespace_transition_accessor assert_respond_to @object, :enable_alarm_transition end def test_should_namespace_action assert_respond_to @object, :enable_alarm end def test_should_namespace_bang_action assert_respond_to @object, :enable_alarm! end end state_machines-0.100.4/test/unit/event/event_with_transition_with_blacklisted_to_state_test.rb000066400000000000000000000036161507333401300332370ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithTransitionWithBlacklistedToStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @machine.state :parked, :idling, :first_gear, :second_gear @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @event.transition(from: :parked, to: StateMachines::BlacklistMatcher.new(%i[parked idling])) @object = @klass.new @object.state = 'parked' end def test_should_be_able_to_fire assert @event.can_fire?(@object) end def test_should_have_a_transition transition = @event.transition_for(@object) refute_nil transition assert_equal 'parked', transition.from assert_equal 'first_gear', transition.to assert_equal :ignite, transition.event end def test_should_allow_loopback_first_when_possible @event.transition(from: :second_gear, to: StateMachines::BlacklistMatcher.new(%i[parked idling])) @object.state = 'second_gear' transition = @event.transition_for(@object) refute_nil transition assert_equal 'second_gear', transition.from assert_equal 'second_gear', transition.to assert_equal :ignite, transition.event end def test_should_allow_specific_transition_selection_using_to transition = @event.transition_for(@object, from: :parked, to: :second_gear) refute_nil transition assert_equal 'parked', transition.from assert_equal 'second_gear', transition.to assert_equal :ignite, transition.event end def test_should_not_allow_transition_selection_if_not_matching transition = @event.transition_for(@object, from: :parked, to: :parked) assert_nil transition end def test_should_fire assert @event.fire(@object) end def test_should_change_the_current_state @event.fire(@object) assert_equal 'first_gear', @object.state end end state_machines-0.100.4/test/unit/event/event_with_transition_with_loopback_state_test.rb000066400000000000000000000016771507333401300320530ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithTransitionWithLoopbackStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked @machine.events << @event = StateMachines::Event.new(@machine, :park) @event.transition(from: :parked, to: StateMachines::LoopbackMatcher.instance) @object = @klass.new @object.state = 'parked' end def test_should_be_able_to_fire assert @event.can_fire?(@object) end def test_should_have_a_transition transition = @event.transition_for(@object) refute_nil transition assert_equal 'parked', transition.from assert_equal 'parked', transition.to assert_equal :park, transition.event end def test_should_fire assert @event.fire(@object) end def test_should_not_change_the_current_state @event.fire(@object) assert_equal 'parked', @object.state end end state_machines-0.100.4/test/unit/event/event_with_transition_with_nil_to_state_test.rb000066400000000000000000000015721507333401300315370ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithTransitionWithNilToStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state nil, :idling @machine.events << @event = StateMachines::Event.new(@machine, :park) @event.transition(idling: nil) @object = @klass.new @object.state = 'idling' end def test_should_be_able_to_fire assert @event.can_fire?(@object) end def test_should_have_a_transition transition = @event.transition_for(@object) refute_nil transition assert_equal 'idling', transition.from assert_nil transition.to assert_equal :park, transition.event end def test_should_fire assert @event.fire(@object) end def test_should_not_change_the_current_state @event.fire(@object) assert_nil @object.state end end state_machines-0.100.4/test/unit/event/event_with_transition_with_whitelisted_to_state_test.rb000066400000000000000000000027771507333401300333120ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithTransitionWithWhitelistedToStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @machine.state :parked, :idling, :first_gear, :second_gear @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @event.transition(from: :parked, to: StateMachines::WhitelistMatcher.new(%i[first_gear second_gear])) @object = @klass.new @object.state = 'parked' end def test_should_be_able_to_fire assert @event.can_fire?(@object) end def test_should_have_a_transition transition = @event.transition_for(@object) refute_nil transition assert_equal 'parked', transition.from assert_equal 'first_gear', transition.to assert_equal :ignite, transition.event end def test_should_allow_specific_transition_selection_using_to transition = @event.transition_for(@object, from: :parked, to: :second_gear) refute_nil transition assert_equal 'parked', transition.from assert_equal 'second_gear', transition.to assert_equal :ignite, transition.event end def test_should_not_allow_transition_selection_if_not_matching transition = @event.transition_for(@object, from: :parked, to: :parked) assert_nil transition end def test_should_fire assert @event.fire(@object) end def test_should_change_the_current_state @event.fire(@object) assert_equal 'first_gear', @object.state end end state_machines-0.100.4/test/unit/event/event_with_transition_without_to_state_test.rb000066400000000000000000000016171507333401300314250ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithTransitionWithoutToStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked @machine.events << @event = StateMachines::Event.new(@machine, :park) @event.transition(from: :parked) @object = @klass.new @object.state = 'parked' end def test_should_be_able_to_fire assert @event.can_fire?(@object) end def test_should_have_a_transition transition = @event.transition_for(@object) refute_nil transition assert_equal 'parked', transition.from assert_equal 'parked', transition.to assert_equal :park, transition.event end def test_should_fire assert @event.fire(@object) end def test_should_not_change_the_current_state @event.fire(@object) assert_equal 'parked', @object.state end end state_machines-0.100.4/test/unit/event/event_with_transitions_test.rb000066400000000000000000000017471507333401300261270ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithTransitionsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @event.transition(parked: :idling) @event.transition(first_gear: :idling) end def test_should_include_all_transition_states_in_known_states assert_equal %i[parked idling first_gear], @event.known_states end def test_should_include_new_transition_states_after_calling_known_states @event.known_states @event.transition(stalled: :idling) assert_equal %i[parked idling first_gear stalled], @event.known_states end def test_should_clear_known_states_on_reset @event.reset assert_empty @event.known_states end def test_should_use_pretty_inspect assert_match '# :idling, :first_gear => :idling]>', @event.inspect end end state_machines-0.100.4/test/unit/event/event_without_matching_transitions_test.rb000066400000000000000000000017771507333401300305340ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithoutMatchingTransitionsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @event.transition(parked: :idling) @object = @klass.new @object.state = 'idling' end def test_should_not_be_able_to_fire refute @event.can_fire?(@object) end def test_should_be_able_to_fire_with_custom_from_state assert @event.can_fire?(@object, from: :parked) end def test_should_not_have_a_transition assert_nil @event.transition_for(@object) end def test_should_have_a_transition_with_custom_from_state refute_nil @event.transition_for(@object, from: :parked) end def test_should_not_fire refute @event.fire(@object) end def test_should_not_change_the_current_state @event.fire(@object) assert_equal 'idling', @object.state end end state_machines-0.100.4/test/unit/event/event_without_transitions_test.rb000066400000000000000000000012161507333401300266460ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventWithoutTransitionsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.events << @event = StateMachines::Event.new(@machine, :ignite) @object = @klass.new end def test_should_not_be_able_to_fire refute @event.can_fire?(@object) end def test_should_not_have_a_transition assert_nil @event.transition_for(@object) end def test_should_not_fire refute @event.fire(@object) end def test_should_not_change_the_current_state @event.fire(@object) assert_nil @object.state end end state_machines-0.100.4/test/unit/event/invalid_event_test.rb000066400000000000000000000010111507333401300241250ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class InvalidEventTest < StateMachinesTest def setup @object = Object.new @invalid_event = StateMachines::InvalidEvent.new(@object, :invalid) end def test_should_have_an_object assert_equal @object, @invalid_event.object end def test_should_have_an_event assert_equal :invalid, @invalid_event.event end def test_should_generate_a_message assert_equal ':invalid is an unknown state machine event', @invalid_event.message end end state_machines-0.100.4/test/unit/event_collection/000077500000000000000000000000001507333401300221345ustar00rootroot00000000000000event_collection_attribute_with_machine_action_test.rb000066400000000000000000000037151507333401300351420ustar00rootroot00000000000000state_machines-0.100.4/test/unit/event_collection# frozen_string_literal: true require 'test_helper' class EventCollectionAttributeWithMachineActionTest < StateMachinesTest def setup @klass = Class.new do def save; end end @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @events = StateMachines::EventCollection.new(@machine) @machine.state :parked, :idling @events << @ignite = StateMachines::Event.new(@machine, :ignite) @machine.events.concat(@events) @object = @klass.new end def test_should_not_have_transition_if_nil @object.state_event = nil assert_nil @events.attribute_transition_for(@object) end def test_should_not_have_transition_if_empty @object.state_event = '' assert_nil @events.attribute_transition_for(@object) end def test_should_have_invalid_transition_if_invalid_event_specified @object.state_event = 'invalid' refute @events.attribute_transition_for(@object) end def test_should_have_invalid_transition_if_event_cannot_be_fired @object.state_event = 'ignite' refute @events.attribute_transition_for(@object) end def test_should_have_valid_transition_if_event_can_be_fired @ignite.transition parked: :idling @object.state_event = 'ignite' assert_instance_of StateMachines::Transition, @events.attribute_transition_for(@object) end def test_should_have_valid_transition_if_already_defined_in_transition_cache @ignite.transition parked: :idling @object.state_event = nil @object.send(:state_event_transition=, transition = @ignite.transition_for(@object)) assert_equal transition, @events.attribute_transition_for(@object) end def test_should_use_transition_cache_if_both_event_and_transition_are_present @ignite.transition parked: :idling @object.state_event = 'ignite' @object.send(:state_event_transition=, transition = @ignite.transition_for(@object)) assert_equal transition, @events.attribute_transition_for(@object) end end event_collection_attribute_with_namespaced_machine_test.rb000066400000000000000000000020631507333401300357600ustar00rootroot00000000000000state_machines-0.100.4/test/unit/event_collection# frozen_string_literal: true require 'test_helper' class EventCollectionAttributeWithNamespacedMachineTest < StateMachinesTest def setup @klass = Class.new do def save; end end @machine = StateMachines::Machine.new(@klass, namespace: 'alarm', initial: :active, action: :save) @events = StateMachines::EventCollection.new(@machine) @machine.state :active, :off @events << @disable = StateMachines::Event.new(@machine, :disable) @machine.events.concat(@events) @object = @klass.new end def test_should_not_have_transition_if_nil @object.state_event = nil assert_nil @events.attribute_transition_for(@object) end def test_should_have_invalid_transition_if_event_cannot_be_fired @object.state_event = 'disable' refute @events.attribute_transition_for(@object) end def test_should_have_valid_transition_if_event_can_be_fired @disable.transition active: :off @object.state_event = 'disable' assert_instance_of StateMachines::Transition, @events.attribute_transition_for(@object) end end state_machines-0.100.4/test/unit/event_collection/event_collection_by_default_test.rb000066400000000000000000000012411507333401300312500ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventCollectionByDefaultTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @events = StateMachines::EventCollection.new(@machine) @object = @klass.new end def test_should_not_have_any_nodes assert_equal 0, @events.length end def test_should_have_a_machine assert_equal @machine, @events.machine end def test_should_not_have_any_valid_events_for_an_object assert_empty @events.valid_for(@object) end def test_should_not_have_any_transitions_for_an_object assert_empty @events.transitions_for(@object) end end state_machines-0.100.4/test/unit/event_collection/event_collection_test.rb000066400000000000000000000015261507333401300270600ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventCollectionTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new, namespace: 'alarm') @events = StateMachines::EventCollection.new(machine) @events << @open = StateMachines::Event.new(machine, :enable) machine.events.concat(@events) end def test_should_index_by_name assert_equal @open, @events[:enable, :name] end def test_should_index_by_name_by_default assert_equal @open, @events[:enable] end def test_should_index_by_string_name assert_equal @open, @events['enable'] end def test_should_index_by_qualified_name assert_equal @open, @events[:enable_alarm, :qualified_name] end def test_should_index_by_string_qualified_name assert_equal @open, @events['enable_alarm', :qualified_name] end end event_collection_with_custom_machine_attribute_test.rb000066400000000000000000000016271507333401300351770ustar00rootroot00000000000000state_machines-0.100.4/test/unit/event_collection# frozen_string_literal: true require 'test_helper' class EventCollectionWithCustomMachineAttributeTest < StateMachinesTest def setup @klass = Class.new do def save; end end @machine = StateMachines::Machine.new(@klass, :state, attribute: :state_id, initial: :parked, action: :save) @events = StateMachines::EventCollection.new(@machine) @machine.state :parked, :idling @events << @ignite = StateMachines::Event.new(@machine, :ignite) @machine.events.concat(@events) @object = @klass.new end def test_should_not_have_transition_if_nil @object.state_event = nil assert_nil @events.attribute_transition_for(@object) end def test_should_have_valid_transition_if_event_can_be_fired @ignite.transition parked: :idling @object.state_event = 'ignite' assert_instance_of StateMachines::Transition, @events.attribute_transition_for(@object) end end event_collection_with_events_with_transitions_test.rb000066400000000000000000000056131507333401300351110ustar00rootroot00000000000000state_machines-0.100.4/test/unit/event_collection# frozen_string_literal: true require 'test_helper' class EventCollectionWithEventsWithTransitionsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @events = StateMachines::EventCollection.new(@machine) @machine.state :idling, :first_gear @events << @ignite = StateMachines::Event.new(@machine, :ignite) @ignite.transition parked: :idling @events << @park = StateMachines::Event.new(@machine, :park) @park.transition idling: :parked @events << @shift_up = StateMachines::Event.new(@machine, :shift_up) @shift_up.transition parked: :first_gear @shift_up.transition idling: :first_gear, if: -> { false } @machine.events.concat(@events) @object = @klass.new end def test_should_find_valid_events_based_on_current_state assert_equal [@ignite, @shift_up], @events.valid_for(@object) end def test_should_filter_valid_events_by_from_state assert_equal [@park], @events.valid_for(@object, from: :idling) end def test_should_filter_valid_events_by_to_state assert_equal [@shift_up], @events.valid_for(@object, to: :first_gear) end def test_should_filter_valid_events_by_event assert_equal [@ignite], @events.valid_for(@object, on: :ignite) end def test_should_filter_valid_events_by_multiple_requirements assert_empty @events.valid_for(@object, from: :idling, to: :first_gear) end def test_should_allow_finding_valid_events_without_guards assert_equal [@shift_up], @events.valid_for(@object, from: :idling, to: :first_gear, guard: false) end def test_should_find_valid_transitions_based_on_current_state assert_equal [ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling), StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :first_gear) ], @events.transitions_for(@object) end def test_should_filter_valid_transitions_by_from_state assert_equal [StateMachines::Transition.new(@object, @machine, :park, :idling, :parked)], @events.transitions_for(@object, from: :idling) end def test_should_filter_valid_transitions_by_to_state assert_equal [StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :first_gear)], @events.transitions_for(@object, to: :first_gear) end def test_should_filter_valid_transitions_by_event assert_equal [StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)], @events.transitions_for(@object, on: :ignite) end def test_should_filter_valid_transitions_by_multiple_requirements assert_empty @events.transitions_for(@object, from: :idling, to: :first_gear) end def test_should_allow_finding_valid_transitions_without_guards assert_equal [StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear)], @events.transitions_for(@object, from: :idling, to: :first_gear, guard: false) end end state_machines-0.100.4/test/unit/event_collection/event_collection_with_multiple_events_test.rb000066400000000000000000000013531507333401300334100ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventCollectionWithMultipleEventsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @events = StateMachines::EventCollection.new(@machine) @machine.state :first_gear @park, @shift_down = @machine.event :park, :shift_down @events << @park @park.transition first_gear: :parked @events << @shift_down @shift_down.transition first_gear: :parked @machine.events.concat(@events) end def test_should_only_include_all_valid_events_for_an_object object = @klass.new object.state = 'first_gear' assert_equal [@park, @shift_down], @events.valid_for(object) end end state_machines-0.100.4/test/unit/event_collection/event_collection_with_validations_test.rb000066400000000000000000000037031507333401300325070ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventCollectionWithValidationsTest < StateMachinesTest module Custom include StateMachines::Integrations::Base def invalidate(object, _attribute, message, values = []) (object.errors ||= []) << generate_message(message, values) end def reset(object) object.errors = [] end end def setup StateMachines::Integrations.register(EventCollectionWithValidationsTest::Custom) @klass = Class.new do attr_accessor :errors def initialize @errors = [] super end end @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save, integration: :custom) @events = StateMachines::EventCollection.new(@machine) @parked, @idling = @machine.state :parked, :idling @events << @ignite = StateMachines::Event.new(@machine, :ignite) @machine.events.concat(@events) @object = @klass.new end def teardown StateMachines::Integrations.reset end def test_should_invalidate_if_invalid_event_specified @object.state_event = 'invalid' @events.attribute_transition_for(@object, true) assert_equal ['is invalid'], @object.errors end def test_should_invalidate_if_event_cannot_be_fired @object.state = 'idling' @object.state_event = 'ignite' @events.attribute_transition_for(@object, true) assert_equal ['cannot transition when idling'], @object.errors end def test_should_invalidate_with_human_name_if_invalid_event_specified @idling.human_name = 'waiting' @object.state = 'idling' @object.state_event = 'ignite' @events.attribute_transition_for(@object, true) assert_equal ['cannot transition when waiting'], @object.errors end def test_should_not_invalidate_event_can_be_fired @ignite.transition parked: :idling @object.state_event = 'ignite' @events.attribute_transition_for(@object, true) assert_empty @object.errors end end state_machines-0.100.4/test/unit/event_collection/event_collection_without_machine_action_test.rb000066400000000000000000000010201507333401300336510ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventCollectionWithoutMachineActionTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @events = StateMachines::EventCollection.new(@machine) @events << StateMachines::Event.new(@machine, :ignite) @machine.events.concat(@events) @object = @klass.new end def test_should_not_have_an_attribute_transition assert_nil @events.attribute_transition_for(@object) end end state_machines-0.100.4/test/unit/event_collection/event_string_collection_test.rb000066400000000000000000000015361507333401300304470ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EventStringCollectionTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new, namespace: 'alarm') @events = StateMachines::EventCollection.new(machine) @events << @open = StateMachines::Event.new(machine, 'enable') machine.events.concat(@events) end def test_should_index_by_name assert_equal @open, @events['enable', :name] end def test_should_index_by_name_by_default assert_equal @open, @events['enable'] end def test_should_index_by_symbol_name assert_equal @open, @events[:enable] end def test_should_index_by_qualified_name assert_equal @open, @events['enable_alarm', :qualified_name] end def test_should_index_by_symbol_qualified_name assert_equal @open, @events[:enable_alarm, :qualified_name] end end state_machines-0.100.4/test/unit/helper_module_test.rb000066400000000000000000000007401507333401300230110ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class HelperModuleTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @helper_module = StateMachines::HelperModule.new(@machine, :instance) end def test_should_not_have_a_name assert_equal '', @helper_module.name.to_s end def test_should_provide_human_readable_to_s assert_equal "#{@klass} :state instance helpers", @helper_module.to_s end end state_machines-0.100.4/test/unit/integration/000077500000000000000000000000001507333401300211235ustar00rootroot00000000000000state_machines-0.100.4/test/unit/integration/guards_async_integration_test.rb000066400000000000000000000203361507333401300276000ustar00rootroot00000000000000# frozen_string_literal: true require File.expand_path('../../test_helper', __dir__) # Integration tests showing state guards and async functionality working together class GuardsAsyncIntegrationTest < Minitest::Test def setup # Skip async tests on unsupported Ruby engines where gems aren't available skip "Guards + Async integration tests not supported on #{RUBY_ENGINE} - async gems not available on this platform" if RUBY_ENGINE == 'jruby' || RUBY_ENGINE == 'truffleruby' @space_station_class = Class.new do # Life support system (sync for safety) state_machine :life_support, initial: :active do event :emergency_shutdown do transition active: :offline end event :restore do transition offline: :active end end # Docking bay doors (async for responsiveness) state_machine :docking_bay, async: true, initial: :closed do event :open_bay do # Only open if life support is active transition closed: :open, if_state: { life_support: :active } end event :close_bay do transition open: :closed end end # Cargo operations (async with complex guards) state_machine :cargo_system, async: true, initial: :idle do event :start_loading do # Can only load if bay is open AND life support is active transition idle: :loading, if_all_states: { docking_bay: :open, life_support: :active } end event :complete_loading do transition loading: :loaded end event :start_unloading do # Can unload if loaded AND bay is open transition loaded: :unloading, if_all_states: { docking_bay: :open, life_support: :active } end event :complete_unloading do transition unloading: :idle end event :emergency_stop do # Emergency stop unless life support is offline transition %i[loading unloading] => :idle, unless_state: { life_support: :offline } end end # Alert system (async) that monitors other systems state_machine :alert_status, async: true, initial: :green do event :raise_alert do # Alert if ANY critical condition exists transition green: :yellow, if_any_state: { life_support: :offline, docking_bay: :open } end event :critical_alert do # Critical alert if life support is down transition %i[green yellow] => :red, if_state: { life_support: :offline } end event :all_clear do # All clear only if everything is safe transition %i[yellow red] => :green, if_all_states: { life_support: :active, docking_bay: :closed, cargo_system: :idle } end end end @station = @space_station_class.new end def test_coordinated_async_operations # Normal operation sequence Async do # Open docking bay (should work - life support active) result = @station.open_bay_async.wait assert result, 'Should be able to open bay when life support is active' # Start loading (should work - bay open, life support active) result = @station.start_loading_async.wait assert result, 'Should be able to start loading when conditions are met' # Raise alert (should work - bay is open) result = @station.raise_alert_async.wait assert result, 'Should raise alert when bay is open' end end def test_emergency_scenarios_with_guards Async do # Open bay and start loading @station.open_bay_async.wait @station.start_loading_async.wait assert_equal 'loading', @station.cargo_system # Emergency shutdown of life support @station.emergency_shutdown! assert_equal 'offline', @station.life_support # Try to start new loading operation (should fail - life support offline) @station.complete_loading_async.wait # Complete current loading first result = @station.start_loading_async.wait refute result, 'Should not be able to start loading when life support is offline' # Emergency stop should NOT work (life support is offline, so guard prevents it) # The guard says "unless life_support is offline", so when it IS offline, emergency_stop is blocked result = @station.emergency_stop_async.wait refute result, 'Emergency stop should not work when life support is offline (guard protection)' # Critical alert should trigger result = @station.critical_alert_async.wait assert result, 'Critical alert should trigger when life support is offline' end end def test_complex_state_coordination Async do # Set up complex scenario @station.open_bay_async.wait @station.start_loading_async.wait @station.complete_loading_async.wait assert_equal 'loaded', @station.cargo_system # Close bay - this should allow all_clear to work later @station.close_bay_async.wait # Start unloading (should fail - bay is closed) result = @station.start_unloading_async.wait refute result, 'Should not be able to unload when bay is closed' # Open bay again @station.open_bay_async.wait # Now unloading should work result = @station.start_unloading_async.wait assert result, 'Should be able to unload when bay is open and life support active' # Complete unloading and close bay @station.complete_unloading_async.wait @station.close_bay_async.wait # The bay was open during operations, so we should be in yellow alert # If not, let's manually trigger an alert state first if @station.alert_status == 'green' # Temporarily open bay to trigger alert, then close it @station.open_bay_async.wait @station.raise_alert_async.wait # This should put us in yellow @station.close_bay_async.wait end # Now all clear should work (transitioning from yellow/red to green) result = @station.all_clear_async.wait # If it's already green, the transition won't work, so let's check the current state if @station.alert_status == 'green' assert true, 'Alert status is already green, which is the desired state' else assert result, "All clear should work when all systems are safe (alert_status: #{@station.alert_status})" end end end def test_mixed_sync_async_with_guards # Test that sync and async machines can coordinate via guards # Emergency shutdown (sync operation) @station.emergency_shutdown! Async do # Async operation should respect sync machine state result = @station.open_bay_async.wait refute result, 'Async machine should respect sync machine guard' # Restore life support (sync) @station.restore! # Now async operation should work result = @station.open_bay_async.wait assert result, 'Async machine should work when sync machine state allows it' end end def test_error_handling_in_async_context Async do # Create a branch with invalid state machine reference branch = StateMachines::Branch.new(if_state: { nonexistent_machine: :some_state }) error = assert_raises(ArgumentError) do branch.matches?(@station) end assert_match(/State machine 'nonexistent_machine' is not defined/, error.message) end end def test_performance_with_multiple_guard_evaluations # Test that caching works correctly with multiple evaluations Async do branch = StateMachines::Branch.new( if_all_states: { life_support: :active, docking_bay: :closed, cargo_system: :idle, alert_status: :green } ) # Multiple evaluations should use cached state machines 100.times do result = branch.matches?(@station) assert result, 'All states should match initially' end # Change one state @station.open_bay_async.wait # Should now fail result = branch.matches?(@station) refute result, 'Should fail when docking_bay is not closed' end end end state_machines-0.100.4/test/unit/integration/integration_options_test.rb000066400000000000000000000037001507333401300266050ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class IntegrationOptionsTest < StateMachinesTest def test_should_have_empty_integration_options_by_default integration = Class.new do include StateMachines::Integrations::Base end assert_empty integration.integration_options end def test_should_allow_integration_to_define_custom_options integration = Class.new do include StateMachines::Integrations::Base def self.integration_options %i[custom_option another_option] end end assert_equal %i[custom_option another_option], integration.integration_options end def test_should_accept_integration_specific_options_in_state_machine # Create a test integration integration = Module.new do include StateMachines::Integrations::Base def self.integration_options [:test_option] end def self.matching_ancestors [TestModel] end end # Register the integration StateMachines::Integrations.register(integration) # Create a test model model = Class.new do def self.name 'TestModel' end end Object.const_set('TestModel', model) # Should not raise error with integration-specific option machine = model.state_machine test_option: true do state :active end # Verify the machine was created successfully assert_instance_of StateMachines::Machine, machine assert_equal :state, machine.name assert_includes machine.states.map(&:name), :active ensure # Clean up StateMachines::Integrations.send(:integrations).delete(integration) Object.send(:remove_const, 'TestModel') if defined?(TestModel) end def test_should_reject_unknown_options model = Class.new error = assert_raises(ArgumentError) do model.state_machine unknown_option: true do state :active end end assert_match(/Unknown key: :unknown_option/, error.message) end end state_machines-0.100.4/test/unit/integrations/000077500000000000000000000000001507333401300213065ustar00rootroot00000000000000state_machines-0.100.4/test/unit/integrations/integration_finder_test.rb000066400000000000000000000010301507333401300265360ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class IntegrationFinderTest < StateMachinesTest def setup StateMachines::Integrations.reset end def test_should_raise_an_exception_if_invalid exception = assert_raises(StateMachines::IntegrationNotFound) { StateMachines::Integrations.find_by_name(:invalid) } assert_equal ':invalid is an invalid integration. No integrations registered', exception.message end def test_should_have_no_integrations assert_empty(StateMachines::Integrations.list) end end state_machines-0.100.4/test/unit/integrations/integration_matcher_test.rb000066400000000000000000000017111507333401300267200ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' require 'files/integrations/vehicle' class IntegrationMatcherTest < StateMachinesTest def setup StateMachines::Integrations.reset end def test_should_return_nil_if_no_match_found assert_nil StateMachines::Integrations.match(Vehicle) end def test_should_return_integration_class_if_match_found StateMachines::Integrations.register(VehicleIntegration) assert_equal VehicleIntegration, StateMachines::Integrations.match(Vehicle) end def test_should_return_nil_if_no_match_found_with_ancestors fake = Class.new assert_nil StateMachines::Integrations.match_ancestors([fake]) end def test_should_return_integration_class_if_match_found_with_ancestors fake = Class.new StateMachines::Integrations.register(VehicleIntegration) assert_equal VehicleIntegration, StateMachines::Integrations.match_ancestors([fake, Vehicle]) end end state_machines-0.100.4/test/unit/invalid_transition/000077500000000000000000000000001507333401300225005ustar00rootroot00000000000000state_machines-0.100.4/test/unit/invalid_transition/invalid_parallel_transition_test.rb000066400000000000000000000007251507333401300316440ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class InvalidParallelTransitionTest < StateMachinesTest def setup @object = Object.new @events = %i[ignite disable_alarm] @invalid_transition = StateMachines::InvalidParallelTransition.new(@object, @events) end def test_should_have_an_object assert_equal @object, @invalid_transition.object end def test_should_have_events assert_equal @events, @invalid_transition.events end end state_machines-0.100.4/test/unit/invalid_transition/invalid_transition_test.rb000066400000000000000000000023401507333401300277630ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class InvalidTransitionTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @state = @machine.state :parked @machine.event :ignite @object = @klass.new @object.state = 'parked' @invalid_transition = StateMachines::InvalidTransition.new(@object, @machine, :ignite) end def test_should_have_an_object assert_equal @object, @invalid_transition.object end def test_should_have_a_machine assert_equal @machine, @invalid_transition.machine end def test_should_have_an_event assert_equal :ignite, @invalid_transition.event end def test_should_have_a_qualified_event assert_equal :ignite, @invalid_transition.qualified_event end def test_should_have_a_from_value assert_equal 'parked', @invalid_transition.from end def test_should_have_a_from_name assert_equal :parked, @invalid_transition.from_name end def test_should_have_a_qualified_from_name assert_equal :parked, @invalid_transition.qualified_from_name end def test_should_generate_a_message assert_equal 'Cannot transition state via :ignite from :parked', @invalid_transition.message end end state_machines-0.100.4/test/unit/invalid_transition/invalid_transition_with_integration_test.rb000066400000000000000000000024621507333401300334260ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class InvalidTransitionWithIntegrationTest < StateMachinesTest module Custom include StateMachines::Integrations::Base def errors_for(object) object.errors end end def setup StateMachines::Integrations.register(InvalidTransitionWithIntegrationTest::Custom) @klass = Class.new do attr_accessor :errors end @machine = StateMachines::Machine.new(@klass, integration: :custom) @machine.state :parked @machine.event :ignite @object = @klass.new @object.state = 'parked' end def fix_test skip end def teardown StateMachines::Integrations.reset end def test_should_generate_a_message_without_reasons_if_empty @object.errors = '' invalid_transition = StateMachines::InvalidTransition.new(@object, @machine, :ignite) assert_equal 'Cannot transition state via :ignite from :parked', invalid_transition.message end def test_should_generate_a_message_with_error_reasons_if_errors_found @object.errors = 'Id is invalid, Name is invalid' invalid_transition = StateMachines::InvalidTransition.new(@object, @machine, :ignite) assert_equal 'Cannot transition state via :ignite from :parked (Reason(s): Id is invalid, Name is invalid)', invalid_transition.message end end state_machines-0.100.4/test/unit/invalid_transition/invalid_transition_with_namespace_test.rb000066400000000000000000000015511507333401300330350ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class InvalidTransitionWithNamespaceTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, namespace: 'alarm') @state = @machine.state :active @machine.event :disable @object = @klass.new @object.state = 'active' @invalid_transition = StateMachines::InvalidTransition.new(@object, @machine, :disable) end def test_should_have_an_event assert_equal :disable, @invalid_transition.event end def test_should_have_a_qualified_event assert_equal :disable_alarm, @invalid_transition.qualified_event end def test_should_have_a_from_name assert_equal :active, @invalid_transition.from_name end def test_should_have_a_qualified_from_name assert_equal :alarm_active, @invalid_transition.qualified_from_name end end state_machines-0.100.4/test/unit/machine/000077500000000000000000000000001507333401300202045ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine/machine_after_being_copied_test.rb000066400000000000000000000036161507333401300270520ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineAfterBeingCopiedTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new, :state, initial: :parked) @machine.event(:ignite) {} @machine.before_transition(-> {}) @machine.after_transition(-> {}) @machine.around_transition(-> {}) @machine.after_failure(-> {}) @copied_machine = @machine.clone end def test_should_not_have_the_same_collection_of_states refute_same @copied_machine.states, @machine.states end def test_should_copy_each_state refute_same @copied_machine.states[:parked], @machine.states[:parked] end def test_should_update_machine_for_each_state assert_equal @copied_machine, @copied_machine.states[:parked].machine end def test_should_not_update_machine_for_original_state assert_equal @machine, @machine.states[:parked].machine end def test_should_not_have_the_same_collection_of_events refute_same @copied_machine.events, @machine.events end def test_should_copy_each_event refute_same @copied_machine.events[:ignite], @machine.events[:ignite] end def test_should_update_machine_for_each_event assert_equal @copied_machine, @copied_machine.events[:ignite].machine end def test_should_not_update_machine_for_original_event assert_equal @machine, @machine.events[:ignite].machine end def test_should_not_have_the_same_callbacks refute_same @copied_machine.callbacks, @machine.callbacks end def test_should_not_have_the_same_before_callbacks refute_same @copied_machine.callbacks[:before], @machine.callbacks[:before] end def test_should_not_have_the_same_after_callbacks refute_same @copied_machine.callbacks[:after], @machine.callbacks[:after] end def test_should_not_have_the_same_failure_callbacks refute_same @copied_machine.callbacks[:failure], @machine.callbacks[:failure] end end state_machines-0.100.4/test/unit/machine/machine_after_changing_initial_state.rb000066400000000000000000000013211507333401300300620ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineAfterChangingInitialState < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @machine.initial_state = :idling @object = @klass.new end def test_should_change_the_initial_state assert_equal :idling, @machine.initial_state(@object).name end def test_should_include_in_known_states assert_equal(%i[parked idling], @machine.states.map { |state| state.name }) end def test_should_reset_original_initial_state refute @machine.state(:parked).initial end def test_should_set_new_state_to_initial assert @machine.state(:idling).initial end end state_machines-0.100.4/test/unit/machine/machine_after_changing_owner_class_test.rb000066400000000000000000000015511507333401300306140ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineAfterChangingOwnerClassTest < StateMachinesTest def setup @original_class = Class.new @machine = StateMachines::Machine.new(@original_class) @new_class = Class.new(@original_class) @new_machine = @machine.clone @new_machine.owner_class = @new_class @object = @new_class.new end def test_should_update_owner_class assert_equal @new_class, @new_machine.owner_class end def test_should_not_change_original_owner_class assert_equal @original_class, @machine.owner_class end def test_should_change_the_associated_machine_in_the_new_class assert_equal @new_machine, @new_class.state_machines[:state] end def test_should_not_change_the_associated_machine_in_the_original_class assert_equal @machine, @original_class.state_machines[:state] end end state_machines-0.100.4/test/unit/machine/machine_by_default_test.rb000066400000000000000000000104211507333401300253700ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineByDefaultTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @object = @klass.new end def test_should_have_an_owner_class assert_equal @klass, @machine.owner_class end def test_should_have_a_name assert_equal :state, @machine.name end def test_should_have_an_attribute assert_equal :state, @machine.attribute end def test_should_prefix_custom_attributes_with_attribute assert_equal :state_event, @machine.attribute(:event) end def test_should_have_an_initial_state refute_nil @machine.initial_state(@object) end def test_should_have_a_nil_initial_state assert_nil @machine.initial_state(@object).value end def test_should_not_have_any_events refute_predicate @machine.events, :any? end def test_should_not_have_any_before_callbacks assert_empty @machine.callbacks[:before] end def test_should_not_have_any_after_callbacks assert_empty @machine.callbacks[:after] end def test_should_not_have_any_failure_callbacks assert_empty @machine.callbacks[:failure] end def test_should_not_have_an_action assert_nil @machine.action end def test_should_use_tranactions assert @machine.use_transactions end def test_should_not_have_a_namespace assert_nil @machine.namespace end def test_should_have_a_nil_state assert_equal [nil], @machine.states.keys end def test_should_set_initial_on_nil_state assert @machine.state(nil).initial end def test_should_generate_default_messages assert_equal 'is invalid', @machine.generate_message(:invalid) assert_equal 'cannot transition when parked', @machine.generate_message(:invalid_event, [%i[state parked]]) assert_equal 'cannot transition via "park"', @machine.generate_message(:invalid_transition, [%i[event park]]) end def test_should_define_a_reader_attribute_for_the_attribute assert_respond_to @object, :state end def test_should_define_a_writer_attribute_for_the_attribute assert_respond_to @object, :state= end def test_should_define_a_predicate_for_the_attribute assert_respond_to @object, :state? end def test_should_define_a_name_reader_for_the_attribute assert_respond_to @object, :state_name end def test_should_define_an_event_reader_for_the_attribute assert_respond_to @object, :state_events end def test_should_define_a_transition_reader_for_the_attribute assert_respond_to @object, :state_transitions end def test_should_define_a_path_reader_for_the_attribute assert_respond_to @object, :state_paths end def test_should_define_an_event_runner_for_the_attribute assert_respond_to @object, :fire_state_event end def test_should_not_define_an_event_attribute_reader refute_respond_to @object, :state_event end def test_should_not_define_an_event_attribute_writer refute_respond_to @object, :state_event= end def test_should_not_define_an_event_transition_attribute_reader refute_respond_to @object, :state_event_transition end def test_should_not_define_an_event_transition_attribute_writer refute_respond_to @object, :state_event_transition= end def test_should_define_a_human_attribute_name_reader_for_the_attribute assert_respond_to @klass, :human_state_name end def test_should_define_a_human_event_name_reader_for_the_attribute assert_respond_to @klass, :human_state_event_name end def test_should_not_define_singular_with_scope refute_respond_to @klass, :with_state end def test_should_not_define_singular_without_scope refute_respond_to @klass, :without_state end def test_should_not_define_plural_with_scope refute_respond_to @klass, :with_states end def test_should_not_define_plural_without_scope refute_respond_to @klass, :without_states end def test_should_extend_owner_class_with_class_methods assert_includes(class << @klass; ancestors; end, StateMachines::ClassMethods) end def test_should_include_instance_methods_in_owner_class assert_includes @klass.included_modules, StateMachines::InstanceMethods end def test_should_define_state_machines_reader expected = { state: @machine } assert_equal expected, @klass.state_machines end end state_machines-0.100.4/test/unit/machine/machine_finder_custom_options_test.rb000066400000000000000000000007311507333401300276710ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineFinderCustomOptionsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.find_or_create(@klass, :status, initial: :parked) @object = @klass.new end def test_should_use_custom_attribute assert_equal :status, @machine.attribute end def test_should_set_custom_initial_state assert_equal :parked, @machine.initial_state(@object).name end end state_machines-0.100.4/test/unit/machine/machine_finder_with_existing_machine_on_superclass_test.rb000066400000000000000000000045251507333401300341220ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineFinderWithExistingMachineOnSuperclassTest < StateMachinesTest module Custom include StateMachines::Integrations::Base def self.matches?(_klass) false end end def setup StateMachines::Integrations.register(MachineFinderWithExistingMachineOnSuperclassTest::Custom) @base_class = Class.new @base_machine = StateMachines::Machine.new(@base_class, :status, action: :save, integration: :custom) @base_machine.event(:ignite) {} @base_machine.before_transition(-> {}) @base_machine.after_transition(-> {}) @base_machine.around_transition(-> {}) @klass = Class.new(@base_class) @machine = StateMachines::Machine.find_or_create(@klass, :status) {} end def teardown StateMachines::Integrations.reset end def test_should_accept_a_block called = false StateMachines::Machine.find_or_create(Class.new(@base_class)) do called = respond_to?(:event) end assert called end def test_should_not_create_a_new_machine_if_no_block_or_options machine = StateMachines::Machine.find_or_create(Class.new(@base_class), :status) assert_same machine, @base_machine end def test_should_create_a_new_machine_if_given_options machine = StateMachines::Machine.find_or_create(@klass, :status, initial: :parked) refute_nil machine refute_same machine, @base_machine end def test_should_create_a_new_machine_if_given_block refute_nil @machine refute_same @machine, @base_machine end def test_should_copy_the_base_attribute assert_equal :status, @machine.attribute end def test_should_copy_the_base_configuration assert_equal :save, @machine.action end def test_should_copy_events # Can't assert equal arrays since their machines change assert_equal 1, @machine.events.length end def test_should_copy_before_callbacks assert_equal @base_machine.callbacks[:before], @machine.callbacks[:before] end def test_should_copy_after_transitions assert_equal @base_machine.callbacks[:after], @machine.callbacks[:after] end def test_should_use_the_same_integration class_ancestors = class << @machine ancestors end assert_includes(class_ancestors, MachineFinderWithExistingMachineOnSuperclassTest::Custom) end end state_machines-0.100.4/test/unit/machine/machine_finder_with_existing_on_same_class_test.rb000066400000000000000000000010521507333401300323540ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineFinderWithExistingOnSameClassTest < StateMachinesTest def setup @klass = Class.new @existing_machine = StateMachines::Machine.new(@klass) @machine = StateMachines::Machine.find_or_create(@klass) end def test_should_accept_a_block called = false StateMachines::Machine.find_or_create(@klass) do called = respond_to?(:event) end assert called end def test_should_not_create_a_new_machine assert_same @machine, @existing_machine end end state_machines-0.100.4/test/unit/machine/machine_finder_without_existing_machine_test.rb000066400000000000000000000010601507333401300317010ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineFinderWithoutExistingMachineTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.find_or_create(@klass) end def test_should_accept_a_block called = false StateMachines::Machine.find_or_create(Class.new) do called = respond_to?(:event) end assert called end def test_should_create_a_new_machine refute_nil @machine end def test_should_use_default_state assert_equal :state, @machine.attribute end end state_machines-0.100.4/test/unit/machine/machine_persistence_test.rb000066400000000000000000000026601507333401300256040ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachinePersistenceTest < StateMachinesTest def setup @klass = Class.new do attr_accessor :state_event end @machine = StateMachines::Machine.new(@klass, initial: :parked) @object = @klass.new end def test_should_allow_reading_state assert_equal 'parked', @machine.read(@object, :state) end def test_should_allow_reading_custom_attributes assert_nil @machine.read(@object, :event) @object.state_event = 'ignite' assert_equal 'ignite', @machine.read(@object, :event) end def test_should_allow_reading_custom_instance_variables @klass.class_eval do attr_writer :state_value end @object.state_value = 1 assert_raises(NoMethodError) { @machine.read(@object, :value) } assert_equal 1, @machine.read(@object, :value, true) end def test_should_allow_writing_state @machine.write(@object, :state, 'idling') assert_equal 'idling', @object.state end def test_should_allow_writing_custom_attributes @machine.write(@object, :event, 'ignite') assert_equal 'ignite', @object.state_event end def test_should_allow_writing_custom_instance_variables @klass.class_eval do attr_reader :state_value end assert_raises(NoMethodError) { @machine.write(@object, :value, 1) } assert_equal 1, @machine.write(@object, :value, 1, true) assert_equal 1, @object.state_value end end state_machines-0.100.4/test/unit/machine/machine_state_initialization_test.rb000066400000000000000000000025621507333401300275100ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineStateInitializationTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, initialize: false) @object = @klass.new @object.state = nil end def test_should_set_states_if_nil @machine.initialize_state(@object) assert_equal 'parked', @object.state end def test_should_set_states_if_empty @object.state = '' @machine.initialize_state(@object) assert_equal 'parked', @object.state end def test_should_not_set_states_if_not_empty @object.state = 'idling' @machine.initialize_state(@object) assert_equal 'idling', @object.state end def test_should_set_states_if_not_empty_and_forced @object.state = 'idling' @machine.initialize_state(@object, force: true) assert_equal 'parked', @object.state end def test_should_not_set_state_if_nil_and_nil_is_valid_state @machine.state :initial, value: nil @machine.initialize_state(@object) assert_nil @object.state end def test_should_write_to_hash_if_specified @machine.initialize_state(@object, to: hash = {}) assert_equal({ 'state' => 'parked' }, hash) end def test_should_not_write_to_object_if_writing_to_hash @machine.initialize_state(@object, to: {}) assert_nil @object.state end end state_machines-0.100.4/test/unit/machine/machine_test.rb000066400000000000000000000016321507333401300231760ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineTest < StateMachinesTest def test_should_raise_exception_if_invalid_option_specified assert_raises(ArgumentError) { StateMachines::Machine.new(Class.new, invalid: true) } end def test_should_not_raise_exception_if_custom_messages_specified StateMachines::Machine.new(Class.new, messages: { invalid_transition: 'custom' }) end def test_should_evaluate_a_block_during_initialization called = true StateMachines::Machine.new(Class.new) do called = respond_to?(:event) end assert called end def test_should_provide_matcher_helpers_during_initialization matchers = [] StateMachines::Machine.new(Class.new) do matchers = [all, any, same] end assert_equal [StateMachines::AllMatcher.instance, StateMachines::AllMatcher.instance, StateMachines::LoopbackMatcher.instance], matchers end end state_machines-0.100.4/test/unit/machine/machine_validation_test.rb000066400000000000000000000060341507333401300254110ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineValidationTest < StateMachinesTest include StateMachines::Machine::Validation def test_validate_eval_string_with_safe_code validate_eval_string('1 + 1') validate_eval_string('object.method') validate_eval_string('if condition; end') validate_eval_string('lambda { |x| x * 2 }') validate_eval_string('value.present?') end def test_validate_eval_string_with_backtick_execution assert_raises(SecurityError) { validate_eval_string('`ls -la`') } assert_raises(SecurityError) { validate_eval_string('`rm -rf /`') } end def test_validate_eval_string_with_system_calls assert_raises(SecurityError) { validate_eval_string('system("rm -rf /")') } assert_raises(SecurityError) { validate_eval_string('system("malicious")') } end def test_validate_eval_string_with_exec_calls assert_raises(SecurityError) { validate_eval_string('exec("malicious")') } assert_raises(SecurityError) { validate_eval_string('exec ("dangerous")') } end def test_validate_eval_string_with_nested_eval assert_raises(SecurityError) { validate_eval_string('eval("dangerous")') } assert_raises(SecurityError) { validate_eval_string('eval ("code")') } end def test_validate_eval_string_with_require_statements assert_raises(SecurityError) { validate_eval_string('require "malicious"') } assert_raises(SecurityError) { validate_eval_string("require 'dangerous'") } end def test_validate_eval_string_with_load_statements assert_raises(SecurityError) { validate_eval_string('load "malicious"') } assert_raises(SecurityError) { validate_eval_string("load 'dangerous'") } end def test_validate_eval_string_with_file_operations assert_raises(SecurityError) { validate_eval_string('File.delete("important")') } assert_raises(SecurityError) { validate_eval_string('File.read("/etc/passwd")') } end def test_validate_eval_string_with_io_operations assert_raises(SecurityError) { validate_eval_string('IO.read("/etc/passwd")') } assert_raises(SecurityError) { validate_eval_string('IO.popen("ls")') } end def test_validate_eval_string_with_dir_operations assert_raises(SecurityError) { validate_eval_string('Dir.glob("*")') } assert_raises(SecurityError) { validate_eval_string('Dir.chdir("/tmp")') } end def test_validate_eval_string_with_kernel_operations assert_raises(SecurityError) { validate_eval_string('Kernel.system("ls")') } assert_raises(SecurityError) { validate_eval_string('Kernel.exec("rm")') } end def test_validate_eval_string_with_invalid_syntax assert_raises(ArgumentError) { validate_eval_string('if without end') } assert_raises(ArgumentError) { validate_eval_string('def method; end end') } assert_raises(ArgumentError) { validate_eval_string('class Foo; def; end') } end def test_validate_eval_string_with_empty_string validate_eval_string('') end def test_validate_eval_string_with_whitespace_only validate_eval_string(' ') validate_eval_string("\t\n") end end state_machines-0.100.4/test/unit/machine/machine_with_action_already_overridden_test.rb000066400000000000000000000012441507333401300315070ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithActionAlreadyOverriddenTest < StateMachinesTest def setup @superclass = Class.new do def save; end end @klass = Class.new(@superclass) StateMachines::Machine.new(@klass, action: :save) @machine = StateMachines::Machine.new(@klass, :status, action: :save) @object = @klass.new end def test_should_not_redefine_action assert_equal 1, @klass.ancestors.select { |ancestor| ![@klass, @superclass].include?(ancestor) && ancestor.method_defined?(:save) }.length end def test_should_mark_action_hook_as_defined assert_predicate @machine, :action_hook? end end state_machines-0.100.4/test/unit/machine/machine_with_action_defined_in_class_test.rb000066400000000000000000000017471507333401300311260ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithActionDefinedInClassTest < StateMachinesTest def setup @klass = Class.new do def save; end end @machine = StateMachines::Machine.new(@klass, action: :save) @object = @klass.new end def test_should_define_an_event_attribute_reader assert_respond_to @object, :state_event end def test_should_define_an_event_attribute_writer assert_respond_to @object, :state_event= end def test_should_define_an_event_transition_attribute_reader assert @object.respond_to?(:state_event_transition, true) end def test_should_define_an_event_transition_attribute_writer assert @object.respond_to?(:state_event_transition=, true) end def test_should_not_define_action refute(@klass.ancestors.any? { |ancestor| ancestor != @klass && ancestor.method_defined?(:save) }) end def test_should_not_mark_action_hook_as_defined refute_predicate @machine, :action_hook? end end state_machines-0.100.4/test/unit/machine/machine_with_action_defined_in_included_module_test.rb000066400000000000000000000024711507333401300331500ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithActionDefinedInIncludedModuleTest < StateMachinesTest def setup @mod = mod = Module.new do def save; end end @klass = Class.new do include mod def bar; end end @machine = StateMachines::Machine.new(@klass, action: :save) @object = @klass.new end def test_should_define_an_event_attribute_reader assert_respond_to(@object, :state_event) end def test_should_define_an_event_attribute_writer assert_respond_to(@object, :state_event=) end def test_should_define_an_event_transition_attribute_reader assert @object.respond_to?(:state_event_transition, true) end def test_should_define_an_event_transition_attribute_writer assert @object.respond_to?(:state_event_transition=, true) end def test_should_define_action assert(@klass.ancestors.any? { |ancestor| ![@klass, @mod].include?(ancestor) && ancestor.method_defined?(:save) }) end def test_should_keep_action_public assert @klass.public_method_defined?(:save) end def test_should_mark_action_hook_as_defined assert_predicate @machine, :action_hook? end def test_should_owner_class_ancestor_has_method_return_nil assert_nil @machine.send(:owner_class_ancestor_has_method?, :instance, :bar) end end state_machines-0.100.4/test/unit/machine/machine_with_action_defined_in_prepended_module_test.rb000066400000000000000000000024771507333401300333350ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithActionDefinedInPrependedModuleTest < StateMachinesTest def setup @mod = mod = Module.new do def save; end end @klass = Class.new do prepend mod def bar; end end @machine = StateMachines::Machine.new(@klass, action: :save) @object = @klass.new end def test_should_define_an_event_attribute_reader assert_respond_to(@object, :state_event) end def test_should_define_an_event_attribute_writer assert_respond_to(@object, :state_event=) end def test_should_define_an_event_transition_attribute_reader assert @object.respond_to?(:state_event_transition, true) end def test_should_define_an_event_transition_attribute_writer assert @object.respond_to?(:state_event_transition=, true) end def test_should_not_define_action assert(@klass.ancestors.none? { |ancestor| ![@klass, @mod].include?(ancestor) && ancestor.method_defined?(:save) }) end def test_should_keep_action_public assert @klass.public_method_defined?(:save) end def test_should_mark_action_hook_as_defined refute_predicate @machine, :action_hook? end def test_should_owner_class_ancestor_has_method_return_nil assert_nil @machine.send(:owner_class_ancestor_has_method?, :instance, :bar) end end state_machines-0.100.4/test/unit/machine/machine_with_action_defined_in_superclass_test.rb000066400000000000000000000022001507333401300321660ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithActionDefinedInSuperclassTest < StateMachinesTest def setup @superclass = Class.new do def save; end end @klass = Class.new(@superclass) @machine = StateMachines::Machine.new(@klass, action: :save) @object = @klass.new end def test_should_define_an_event_attribute_reader assert_respond_to @object, :state_event end def test_should_define_an_event_attribute_writer assert_respond_to @object, :state_event= end def test_should_define_an_event_transition_attribute_reader assert @object.respond_to?(:state_event_transition, true) end def test_should_define_an_event_transition_attribute_writer assert @object.respond_to?(:state_event_transition=, true) end def test_should_define_action assert(@klass.ancestors.any? { |ancestor| ![@klass, @superclass].include?(ancestor) && ancestor.method_defined?(:save) }) end def test_should_keep_action_public assert @klass.public_method_defined?(:save) end def test_should_mark_action_hook_as_defined assert_predicate @machine, :action_hook? end end state_machines-0.100.4/test/unit/machine/machine_with_action_undefined_test.rb000066400000000000000000000016001507333401300276020ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithActionUndefinedTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, action: :save) @object = @klass.new end def test_should_define_an_event_attribute_reader assert_respond_to @object, :state_event end def test_should_define_an_event_attribute_writer assert_respond_to @object, :state_event= end def test_should_define_an_event_transition_attribute_reader assert @object.respond_to?(:state_event_transition, true) end def test_should_define_an_event_transition_attribute_writer assert @object.respond_to?(:state_event_transition=, true) end def test_should_not_define_action refute_respond_to @object, :save end def test_should_not_mark_action_hook_as_defined refute_predicate @machine, :action_hook? end end state_machines-0.100.4/test/unit/machine/machine_with_cached_state_test.rb000066400000000000000000000010051507333401300267120ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithCachedStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @state = @machine.state :parked, value: -> { Object.new }, cache: true @object = @klass.new end def test_should_use_evaluated_value assert_instance_of Object, @object.state end def test_use_same_value_across_multiple_objects assert_equal @object.state, @klass.new.state end end state_machines-0.100.4/test/unit/machine/machine_with_class_helpers_test.rb000066400000000000000000000114471507333401300271450ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'stringio' class MachineWithClassHelpersTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) end def test_should_not_redefine_existing_public_methods class << @klass def states [] end end @machine.define_helper(:class, :states) {} assert_empty @klass.states end def test_should_not_redefine_existing_protected_methods class << @klass protected def states [] end end @machine.define_helper(:class, :states) {} assert_empty @klass.send(:states) end def test_should_not_redefine_existing_private_methods class << @klass private def states [] end end @machine.define_helper(:class, :states) {} assert_empty @klass.send(:states) end def test_should_warn_if_defined_in_superclass @original_stderr = $stderr $stderr = StringIO.new superclass = Class.new do def self.park; end end klass = Class.new(superclass) machine = StateMachines::Machine.new(klass) machine.define_helper(:class, :park) {} assert_equal "Class method \"park\" is already defined in #{superclass}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string ensure $stderr = @original_stderr end def test_should_warn_if_defined_in_multiple_superclasses @original_stderr = $stderr $stderr = StringIO.new superclass1 = Class.new do def self.park; end end superclass2 = Class.new(superclass1) do def self.park; end end klass = Class.new(superclass2) machine = StateMachines::Machine.new(klass) machine.define_helper(:class, :park) {} assert_equal "Class method \"park\" is already defined in #{superclass1}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string ensure $stderr = @original_stderr end def test_should_warn_if_defined_in_module_prior_to_helper_module @original_stderr = $stderr $stderr = StringIO.new mod = Module.new do def park; end end klass = Class.new do extend mod end machine = StateMachines::Machine.new(klass) machine.define_helper(:class, :park) {} assert_equal "Class method \"park\" is already defined in #{mod}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string ensure $stderr = @original_stderr end def test_should_not_warn_if_defined_in_module_after_helper_module @original_stderr = $stderr $stderr = StringIO.new klass = Class.new machine = StateMachines::Machine.new(klass) mod = Module.new do def park; end end klass.class_eval do extend mod end machine.define_helper(:class, :park) {} assert_equal '', $stderr.string ensure $stderr = @original_stderr end def test_should_define_if_ignoring_method_conflicts_and_defined_in_superclass @original_stderr = $stderr $stderr = StringIO.new StateMachines::Machine.ignore_method_conflicts = true superclass = Class.new do def self.park; end end klass = Class.new(superclass) machine = StateMachines::Machine.new(klass) machine.define_helper(:class, :park) { true } assert_equal '', $stderr.string assert klass.park ensure StateMachines::Machine.ignore_method_conflicts = false $stderr = @original_stderr end def test_should_define_nonexistent_methods @machine.define_helper(:class, :states) { [] } assert_empty @klass.states end def test_should_warn_if_defined_multiple_times @original_stderr = $stderr $stderr = StringIO.new @machine.define_helper(:class, :states) {} @machine.define_helper(:class, :states) {} assert_equal "Class method \"states\" is already defined in #{@klass} :state class helpers, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string ensure $stderr = @original_stderr end def test_should_pass_context_as_arguments helper_args = nil @machine.define_helper(:class, :states) { |*args| helper_args = args } @klass.states assert_equal 2, helper_args.length assert_equal [@machine, @klass], helper_args end def test_should_pass_method_arguments_through helper_args = nil @machine.define_helper(:class, :states) { |*args| helper_args = args } @klass.states(1, 2, 3) assert_equal 5, helper_args.length assert_equal [@machine, @klass, 1, 2, 3], helper_args end def test_should_allow_string_evaluation @machine.define_helper :class, <<-END_EVAL, __FILE__, __LINE__ + 1 def states [] end END_EVAL assert_empty @klass.states end end state_machines-0.100.4/test/unit/machine/machine_with_conflicting_helpers_after_definition_test.rb000066400000000000000000000120201507333401300337140ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithConflictingHelpersAfterDefinitionTest < StateMachinesTest module Custom include StateMachines::Integrations::Base def create_with_scope(_name) ->(_klass, _values) { [] } end def create_without_scope(_name) ->(_klass, _values) { [] } end end def setup @original_stderr = $stderr $stderr = StringIO.new StateMachines::Integrations.register(MachineWithConflictingHelpersAfterDefinitionTest::Custom) @klass = Class.new do def self.with_state :with_state end def self.with_states :with_states end def self.without_state :without_state end def self.without_states :without_states end def self.human_state_name :human_state_name end def self.human_state_event_name :human_state_event_name end attr_accessor :status def state 'parked' end def state=(value) self.status = value end def state? true end def state_name :parked end def human_state_name 'parked' end def state_events [:ignite] end def state_transitions [{ parked: :idling }] end def state_paths [[{ parked: :idling }]] end def fire_state_event true end end @machine = StateMachines::Machine.new(@klass, integration: :custom) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new end def teardown $stderr = @original_stderr StateMachines::Integrations.reset end def test_should_not_redefine_singular_with_scope assert_equal :with_state, @klass.with_state end def test_should_not_redefine_plural_with_scope assert_equal :with_states, @klass.with_states end def test_should_not_redefine_singular_without_scope assert_equal :without_state, @klass.without_state end def test_should_not_redefine_plural_without_scope assert_equal :without_states, @klass.without_states end def test_should_not_redefine_human_attribute_name_reader assert_equal :human_state_name, @klass.human_state_name end def test_should_not_redefine_human_event_name_reader assert_equal :human_state_event_name, @klass.human_state_event_name end def test_should_not_redefine_attribute_reader assert_equal 'parked', @object.state end def test_should_not_redefine_attribute_writer @object.state = 'parked' assert_equal 'parked', @object.status end def test_should_not_define_attribute_predicate assert_predicate @object, :state? end def test_should_not_redefine_attribute_name_reader assert_equal :parked, @object.state_name end def test_should_not_redefine_attribute_human_name_reader assert_equal 'parked', @object.human_state_name end def test_should_not_redefine_attribute_events_reader assert_equal [:ignite], @object.state_events end def test_should_not_redefine_attribute_transitions_reader assert_equal [{ parked: :idling }], @object.state_transitions end def test_should_not_redefine_attribute_paths_reader assert_equal [[{ parked: :idling }]], @object.state_paths end def test_should_not_redefine_event_runner assert @object.fire_state_event end def test_should_allow_super_chaining @klass.class_eval do def self.with_state(*states) super end def self.with_states(*states) super end def self.without_state(*states) super end def self.without_states(*states) super end def self.human_state_name(state) super end def self.human_state_event_name(event) super end attr_accessor :status def state super end def state=(value) super end def state?(state) super end def state_name super end def human_state_name super end def state_events super end def state_transitions super end def state_paths super end def fire_state_event(event) super end end assert_empty @klass.with_state assert_empty @klass.with_states assert_empty @klass.without_state assert_empty @klass.without_states assert_equal 'parked', @klass.human_state_name(:parked) assert_equal 'ignite', @klass.human_state_event_name(:ignite) assert_nil @object.state @object.state = 'idling' assert_equal 'idling', @object.state assert_nil @object.status refute @object.state?(:parked) assert_equal :idling, @object.state_name assert_equal 'idling', @object.human_state_name assert_empty @object.state_events assert_empty @object.state_transitions assert_empty @object.state_paths refute @object.fire_state_event(:ignite) end def test_should_not_output_warning assert_equal '', $stderr.string end end state_machines-0.100.4/test/unit/machine/machine_with_conflicting_helpers_before_definition_test.rb000066400000000000000000000101261507333401300340620ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithConflictingHelpersBeforeDefinitionTest < StateMachinesTest module Custom include StateMachines::Integrations::Base def create_with_scope(_name) ->(_klass, _values) { [] } end def create_without_scope(_name) ->(_klass, _values) { [] } end end def setup @original_stderr = $stderr $stderr = StringIO.new StateMachines::Integrations.register(MachineWithConflictingHelpersBeforeDefinitionTest::Custom) @superclass = Class.new do def self.with_state :with_state end def self.with_states :with_states end def self.without_state :without_state end def self.without_states :without_states end def self.human_state_name :human_state_name end def self.human_state_event_name :human_state_event_name end attr_accessor :status def state 'parked' end def state=(value) self.status = value end def state? true end def state_name :parked end def human_state_name 'parked' end def state_events [:ignite] end def state_transitions [{ parked: :idling }] end def state_paths [[{ parked: :idling }]] end def fire_state_event true end end @klass = Class.new(@superclass) @machine = StateMachines::Machine.new(@klass, integration: :custom) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new end def teardown $stderr = @original_stderr end def test_should_not_redefine_singular_with_scope assert_equal :with_state, @klass.with_state end def test_should_not_redefine_plural_with_scope assert_equal :with_states, @klass.with_states end def test_should_not_redefine_singular_without_scope assert_equal :without_state, @klass.without_state end def test_should_not_redefine_plural_without_scope assert_equal :without_states, @klass.without_states end def test_should_not_redefine_human_attribute_name_reader assert_equal :human_state_name, @klass.human_state_name end def test_should_not_redefine_human_event_name_reader assert_equal :human_state_event_name, @klass.human_state_event_name end def test_should_not_redefine_attribute_reader assert_equal 'parked', @object.state end def test_should_not_redefine_attribute_writer @object.state = 'parked' assert_equal 'parked', @object.status end def test_should_not_define_attribute_predicate assert_predicate @object, :state? end def test_should_not_redefine_attribute_name_reader assert_equal :parked, @object.state_name end def test_should_not_redefine_attribute_human_name_reader assert_equal 'parked', @object.human_state_name end def test_should_not_redefine_attribute_events_reader assert_equal [:ignite], @object.state_events end def test_should_not_redefine_attribute_transitions_reader assert_equal [{ parked: :idling }], @object.state_transitions end def test_should_not_redefine_attribute_paths_reader assert_equal [[{ parked: :idling }]], @object.state_paths end def test_should_not_redefine_event_runner assert @object.fire_state_event end def test_should_output_warning expected = [ 'Instance method "state_events"', 'Instance method "state_transitions"', 'Instance method "fire_state_event"', 'Instance method "state_paths"', 'Class method "human_state_name"', 'Class method "human_state_event_name"', 'Instance method "state_name"', 'Instance method "human_state_name"', 'Class method "with_state"', 'Class method "with_states"', 'Class method "without_state"', 'Class method "without_states"' ].map { |method| "#{method} is already defined in #{@superclass}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n" }.join assert_equal expected, $stderr.string end end state_machines-0.100.4/test/unit/machine/machine_with_custom_action_test.rb000066400000000000000000000004351507333401300271600ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithCustomActionTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new, action: :save) end def test_should_use_the_custom_action assert_equal :save, @machine.action end end state_machines-0.100.4/test/unit/machine/machine_with_custom_attribute_test.rb000066400000000000000000000050261507333401300277070ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' module MachineWithCustomAttributeIntegration include StateMachines::Integrations::Base def self.integration_name :custom_attribute end @defaults = { action: :save, use_transactions: false } def create_with_scope(_name) -> {} end def create_without_scope(_name) -> {} end end class MachineWithCustomAttributeTest < StateMachinesTest def setup StateMachines::Integrations.register(MachineWithCustomAttributeIntegration) @klass = Class.new @machine = StateMachines::Machine.new(@klass, :state, attribute: :state_id, initial: :active, integration: :custom_attribute) do event :ignite do transition parked: :idling end end @object = @klass.new end def teardown StateMachines::Integrations.reset end def test_should_define_a_reader_attribute_for_the_attribute assert_respond_to @object, :state_id end def test_should_define_a_writer_attribute_for_the_attribute assert_respond_to @object, :state_id= end def test_should_define_a_predicate_for_the_attribute assert_respond_to @object, :state? end def test_should_define_a_name_reader_for_the_attribute assert_respond_to @object, :state_name end def test_should_define_a_human_name_reader_for_the_attribute assert_respond_to @object, :state_name end def test_should_define_an_event_reader_for_the_attribute assert_respond_to @object, :state_events end def test_should_define_a_transition_reader_for_the_attribute assert_respond_to @object, :state_transitions end def test_should_define_a_path_reader_for_the_attribute assert_respond_to @object, :state_paths end def test_should_define_an_event_runner_for_the_attribute assert_respond_to @object, :fire_state_event end def test_should_define_a_human_attribute_name_reader assert_respond_to @klass, :human_state_name end def test_should_define_a_human_event_name_reader assert_respond_to @klass, :human_state_event_name end def test_should_define_singular_with_scope assert_respond_to @klass, :with_state end def test_should_define_singular_without_scope assert_respond_to @klass, :without_state end def test_should_define_plural_with_scope assert_respond_to @klass, :with_states end def test_should_define_plural_without_scope assert_respond_to @klass, :without_states end def test_should_define_state_machines_reader expected = { state: @machine } assert_equal expected, @klass.state_machines end end state_machines-0.100.4/test/unit/machine/machine_with_custom_initialize_test.rb000066400000000000000000000011721507333401300300430ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithCustomInitializeTest < StateMachinesTest def setup @klass = Class.new do def initialize(state = nil, options = {}) @state = state initialize_state_machines(options) end end @machine = StateMachines::Machine.new(@klass, initial: :parked) @object = @klass.new end def test_should_initialize_state assert_equal 'parked', @object.state end def test_should_allow_custom_options @machine.state :idling @object = @klass.new('idling', static: :force) assert_equal 'parked', @object.state end end state_machines-0.100.4/test/unit/machine/machine_with_custom_integration_test.rb000066400000000000000000000045701507333401300302320ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/models/vehicle' class MachineWithCustomIntegrationTest < StateMachinesTest module Custom include StateMachines::Integrations::Base def self.matching_ancestors [Vehicle] end end def setup StateMachines::Integrations.register(MachineWithCustomIntegrationTest::Custom) @klass = Vehicle end def teardown StateMachines::Integrations.reset MachineWithCustomIntegrationTest::Custom.class_eval do class << self; remove_method :matching_ancestors; end def self.matching_ancestors [Vehicle] end end end def test_should_be_extended_by_the_integration_if_explicit machine = StateMachines::Machine.new(@klass, integration: :custom) assert_includes(class << machine; ancestors; end, MachineWithCustomIntegrationTest::Custom) end def test_should_not_be_extended_by_the_integration_if_implicit_but_not_available MachineWithCustomIntegrationTest::Custom.class_eval do class << self; remove_method :matching_ancestors; end def self.matching_ancestors [] end end machine = StateMachines::Machine.new(@klass) refute_includes(class << machine; ancestors; end, MachineWithCustomIntegrationTest::Custom) end def test_should_not_be_extended_by_the_integration_if_implicit_but_not_matched MachineWithCustomIntegrationTest::Custom.class_eval do class << self; remove_method :matching_ancestors; end def self.matching_ancestors [] end end machine = StateMachines::Machine.new(@klass) refute_includes(class << machine; ancestors; end, MachineWithCustomIntegrationTest::Custom) end def test_should_be_extended_by_the_integration_if_implicit_and_available_and_matches machine = StateMachines::Machine.new(@klass) assert_includes(class << machine; ancestors; end, MachineWithCustomIntegrationTest::Custom) end def test_should_not_be_extended_by_the_integration_if_nil machine = StateMachines::Machine.new(@klass, integration: nil) refute_includes(class << machine; ancestors; end, MachineWithCustomIntegrationTest::Custom) end def test_should_not_be_extended_by_the_integration_if_false machine = StateMachines::Machine.new(@klass, integration: false) refute_includes(class << machine; ancestors; end, MachineWithCustomIntegrationTest::Custom) end end state_machines-0.100.4/test/unit/machine/machine_with_custom_invalidation_test.rb000066400000000000000000000020231507333401300303570ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithCustomInvalidationTest < StateMachinesTest module Custom include StateMachines::Integrations::Base def invalidate(object, _attribute, message, values = []) object.error = generate_message(message, values) end end def setup StateMachines::Integrations.register(MachineWithCustomInvalidationTest::Custom) @klass = Class.new do attr_accessor :error end @machine = StateMachines::Machine.new(@klass, integration: :custom, messages: { invalid_transition: 'cannot %s' }) @machine.state :parked @object = @klass.new @object.state = 'parked' end def teardown StateMachines::Integrations.reset end def test_generate_custom_message assert_equal 'cannot park', @machine.generate_message(:invalid_transition, [%i[event park]]) end def test_use_custom_message @machine.invalidate(@object, :state, :invalid_transition, [[:event, 'park']]) assert_equal 'cannot park', @object.error end end state_machines-0.100.4/test/unit/machine/machine_with_custom_name_test.rb000066400000000000000000000030521507333401300266210ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithCustomNameTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, :status) @object = @klass.new end def test_should_use_custom_name assert_equal :status, @machine.name end def test_should_use_custom_name_for_attribute assert_equal :status, @machine.attribute end def test_should_prefix_custom_attributes_with_custom_name assert_equal :status_event, @machine.attribute(:event) end def test_should_define_a_reader_attribute_for_the_attribute assert_respond_to @object, :status end def test_should_define_a_writer_attribute_for_the_attribute assert_respond_to @object, :status= end def test_should_define_a_predicate_for_the_attribute assert_respond_to @object, :status? end def test_should_define_a_name_reader_for_the_attribute assert_respond_to @object, :status_name end def test_should_define_an_event_reader_for_the_attribute assert_respond_to @object, :status_events end def test_should_define_a_transition_reader_for_the_attribute assert_respond_to @object, :status_transitions end def test_should_define_an_event_runner_for_the_attribute assert_respond_to @object, :fire_status_event end def test_should_define_a_human_attribute_name_reader_for_the_attribute assert_respond_to @klass, :human_status_name end def test_should_define_a_human_event_name_reader_for_the_attribute assert_respond_to @klass, :human_status_event_name end end state_machines-0.100.4/test/unit/machine/machine_with_custom_plural_test.rb000066400000000000000000000033331507333401300272020ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithCustomPluralTest < StateMachinesTest def setup @integration = Module.new do include StateMachines::Integrations::Base class << self; attr_accessor :with_scopes, :without_scopes; end @with_scopes = [] @without_scopes = [] def create_with_scope(name) MachineWithCustomPluralTest::Custom.with_scopes << name -> {} end def create_without_scope(name) MachineWithCustomPluralTest::Custom.without_scopes << name -> {} end end MachineWithCustomPluralTest.const_set('Custom', @integration) StateMachines::Integrations.register(MachineWithCustomPluralTest::Custom) end def teardown StateMachines::Integrations.reset MachineWithCustomPluralTest.send(:remove_const, 'Custom') end def test_should_define_a_singular_and_plural_with_scope StateMachines::Machine.new(Class.new, integration: :custom, plural: 'staties') assert_equal %w[with_state with_staties], @integration.with_scopes end def test_should_define_a_singular_and_plural_without_scope StateMachines::Machine.new(Class.new, integration: :custom, plural: 'staties') assert_equal %w[without_state without_staties], @integration.without_scopes end def test_should_define_single_with_scope_if_singular_same_as_plural StateMachines::Machine.new(Class.new, integration: :custom, plural: 'state') assert_equal %w[with_state], @integration.with_scopes end def test_should_define_single_without_scope_if_singular_same_as_plural StateMachines::Machine.new(Class.new, integration: :custom, plural: 'state') assert_equal %w[without_state], @integration.without_scopes end end state_machines-0.100.4/test/unit/machine/machine_with_dynamic_initial_state_test.rb000066400000000000000000000035021507333401300306440ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithDynamicInitialStateTest < StateMachinesTest def setup @klass = Class.new do attr_accessor :initial_state end @machine = StateMachines::Machine.new(@klass, initial: ->(object) { object.initial_state || :default }) @machine.state :parked, :idling, :default @object = @klass.new end def test_should_have_dynamic_initial_state assert_predicate @machine, :dynamic_initial_state? end def test_should_use_the_record_for_determining_the_initial_state @object.initial_state = :parked assert_equal :parked, @machine.initial_state(@object).name @object.initial_state = :idling assert_equal :idling, @machine.initial_state(@object).name end def test_should_write_to_attribute_when_initializing_state object = @klass.allocate object.initial_state = :parked @machine.initialize_state(object) assert_equal 'parked', object.state end def test_should_set_initial_state_on_created_object assert_equal 'default', @object.state end def test_should_not_set_initial_state_even_if_not_empty @klass.class_eval do def initialize(_attributes = {}) self.state = 'parked' super() end end object = @klass.new assert_equal 'parked', object.state end def test_should_set_initial_state_after_initialization base = Class.new do attr_accessor :state_on_init def initialize self.state_on_init = state end end klass = Class.new(base) machine = StateMachines::Machine.new(klass, initial: ->(_object) { :parked }) machine.state :parked assert_nil klass.new.state_on_init end def test_should_not_be_included_in_known_states assert_equal(%i[parked idling default], @machine.states.map { |state| state.name }) end end state_machines-0.100.4/test/unit/machine/machine_with_event_matchers_test.rb000066400000000000000000000027201507333401300273170ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithEventMatchersTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) end def test_should_empty_array_for_all_matcher assert_empty @machine.event(StateMachines::AllMatcher.instance) end def test_should_return_referenced_events_for_blacklist_matcher assert_instance_of StateMachines::Event, @machine.event(StateMachines::BlacklistMatcher.new([:park])) end def test_should_not_allow_configurations expected_hash = { human_name: 'Parked' } expected_message = "Cannot configure states when using matchers (using #{expected_hash.inspect})" exception = assert_raises(ArgumentError) do @machine.state(StateMachines::BlacklistMatcher.new([:parked]), human_name: 'Parked') end assert_equal expected_message, exception.message end def test_should_track_referenced_events @machine.event(StateMachines::BlacklistMatcher.new([:park])) assert_equal([:park], @machine.events.map { |event| event.name }) end def test_should_eval_context_for_matching_events contexts_run = [] @machine.event(StateMachines::BlacklistMatcher.new([:park])) { contexts_run << name } @machine.event :park assert_empty contexts_run @machine.event :ignite assert_equal [:ignite], contexts_run @machine.event :shift_up, :shift_down assert_equal %i[ignite shift_up shift_down], contexts_run end end state_machines-0.100.4/test/unit/machine/machine_with_events_test.rb000066400000000000000000000030221507333401300256100ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithEventsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) end def test_should_return_the_created_event assert_instance_of StateMachines::Event, @machine.event(:ignite) end def test_should_create_event_with_given_name event = @machine.event(:ignite) {} assert_equal :ignite, event.name end def test_should_evaluate_block_within_event_context responded = false @machine.event :ignite do responded = respond_to?(:transition) end assert responded end def test_should_be_aliased_as_on event = @machine.on(:ignite) {} assert_equal :ignite, event.name end def test_should_have_events event = @machine.event(:ignite) assert_equal [event], @machine.events.to_a end def test_should_allow_human_state_name_lookup @machine.event(:ignite) assert_equal 'ignite', @klass.human_state_event_name(:ignite) end def test_should_raise_exception_on_invalid_human_state_event_name_lookup exception = assert_raises(IndexError) { @klass.human_state_event_name(:invalid) } assert_equal ':invalid is an invalid name', exception.message end def test_should_raise_exception_if_conflicting_type_used_for_name @machine.event :park exception = assert_raises(ArgumentError) { @machine.event 'ignite' } assert_equal '"ignite" event defined as String, :park defined as Symbol; all events must be consistent', exception.message end end state_machines-0.100.4/test/unit/machine/machine_with_events_with_custom_human_names_test.rb000066400000000000000000000007461507333401300326220ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithEventsWithCustomHumanNamesTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @event = @machine.event(:ignite, human_name: 'start') end def test_should_use_custom_human_name assert_equal 'start', @event.human_name end def test_should_allow_human_state_name_lookup assert_equal 'start', @klass.human_state_event_name(:ignite) end end state_machines-0.100.4/test/unit/machine/machine_with_events_with_transitions_test.rb000066400000000000000000000020411507333401300313000ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithEventsWithTransitionsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @event = @machine.event(:ignite) do transition parked: :idling transition stalled: :idling end end def test_should_have_events assert_equal [@event], @machine.events.to_a end def test_should_track_states_defined_in_event_transitions assert_equal(%i[parked idling stalled], @machine.states.map { |state| state.name }) end def test_should_not_duplicate_states_defined_in_multiple_event_transitions @machine.event :park do transition idling: :parked end assert_equal(%i[parked idling stalled], @machine.states.map { |state| state.name }) end def test_should_track_state_from_new_events @machine.event :shift_up do transition idling: :first_gear end assert_equal(%i[parked idling stalled first_gear], @machine.states.map { |state| state.name }) end end state_machines-0.100.4/test/unit/machine/machine_with_existing_event_test.rb000066400000000000000000000007071507333401300273460ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithExistingEventTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @event = @machine.event(:ignite) @same_event = @machine.event(:ignite) end def test_should_not_create_new_event assert_same @event, @same_event end def test_should_allow_accessing_event_without_block assert_equal @event, @machine.event(:ignite) end end state_machines-0.100.4/test/unit/machine/machine_with_existing_machines_on_owner_class_test.rb000066400000000000000000000012111507333401300330760ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithExistingMachinesOnOwnerClassTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @second_machine = StateMachines::Machine.new(@klass, :status, initial: :idling) @object = @klass.new end def test_should_track_each_state_machine expected = { state: @machine, status: @second_machine } assert_equal expected, @klass.state_machines end def test_should_initialize_state_for_both_machines assert_equal 'parked', @object.state assert_equal 'idling', @object.status end end machine_with_existing_machines_with_same_attributes_on_owner_class_test.rb000066400000000000000000000037331507333401300373400ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine# frozen_string_literal: true require 'test_helper' class MachineWithExistingMachinesWithSameAttributesOnOwnerClassTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @second_machine = StateMachines::Machine.new(@klass, :public_state, initial: :idling, attribute: :state) @object = @klass.new end def test_should_track_each_state_machine expected = { state: @machine, public_state: @second_machine } assert_equal expected, @klass.state_machines end def test_should_write_to_state_only_once @klass.class_eval do attr_reader :write_count def state=(_value) @write_count ||= 0 @write_count += 1 end end object = @klass.new assert_equal 1, object.write_count end def test_should_initialize_based_on_first_machine assert_equal 'parked', @object.state end def test_should_not_allow_second_machine_to_initialize_state @object.state = nil @second_machine.initialize_state(@object) assert_nil @object.state end def test_should_allow_transitions_on_both_machines @machine.event :ignite do transition parked: :idling end @second_machine.event :park do transition idling: :parked end @object.ignite assert_equal 'idling', @object.state @object.park assert_equal 'parked', @object.state end def test_should_copy_new_states_to_sibling_machines @first_gear = @machine.state :first_gear assert_equal @first_gear, @second_machine.state(:first_gear) @second_gear = @second_machine.state :second_gear assert_equal @second_gear, @machine.state(:second_gear) end def test_should_copy_all_existing_states_to_new_machines third_machine = StateMachines::Machine.new(@klass, :protected_state, attribute: :state) assert_equal @machine.state(:parked), third_machine.state(:parked) assert_equal @machine.state(:idling), third_machine.state(:idling) end end machine_with_existing_machines_with_same_attributes_on_owner_subclass_test.rb000066400000000000000000000023151507333401300400450ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine# frozen_string_literal: true require 'test_helper' class MachineWithExistingMachinesWithSameAttributesOnOwnerSubclassTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @second_machine = StateMachines::Machine.new(@klass, :public_state, initial: :idling, attribute: :state) @subclass = Class.new(@klass) @object = @subclass.new end def test_should_not_copy_sibling_machines_to_subclass_after_initialization @subclass.state_machine(:state) {} assert_equal @klass.state_machine(:public_state), @subclass.state_machine(:public_state) end def test_should_copy_sibling_machines_to_subclass_after_new_state subclass_machine = @subclass.state_machine(:state) {} subclass_machine.state :first_gear refute_equal @klass.state_machine(:public_state), @subclass.state_machine(:public_state) end def test_should_copy_new_states_to_sibling_machines subclass_machine = @subclass.state_machine(:state) {} @first_gear = subclass_machine.state :first_gear second_subclass_machine = @subclass.state_machine(:public_state) assert_equal @first_gear, second_subclass_machine.state(:first_gear) end end state_machines-0.100.4/test/unit/machine/machine_with_existing_state_test.rb000066400000000000000000000012651507333401300273450ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithExistingStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @state = @machine.state :parked @same_state = @machine.state :parked, value: 1 end def test_should_not_create_a_new_state assert_same @state, @same_state end def test_should_update_attributes assert_equal 1, @state.value end def test_should_no_longer_be_able_to_look_up_state_by_original_value assert_nil @machine.states['parked', :value] end def test_should_be_able_to_look_up_state_by_new_value assert_equal @state, @machine.states[1, :value] end end state_machines-0.100.4/test/unit/machine/machine_with_failure_callbacks_test.rb000066400000000000000000000033621507333401300277410ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithFailureCallbacksTest < StateMachinesTest def setup @klass = Class.new do attr_accessor :callbacks end @machine = StateMachines::Machine.new(@klass, initial: :parked) @event = @machine.event :ignite @object = @klass.new @object.callbacks = [] end def test_should_raise_exception_if_implicit_option_specified exception = assert_raises(ArgumentError) { @machine.after_failure invalid: :valid, do: -> {} } assert_equal 'Unknown key: :invalid. Valid keys are: :on, :do, :if, :unless', exception.message end def test_should_raise_exception_if_method_not_specified exception = assert_raises(ArgumentError) { @machine.after_failure on: :ignite } assert_equal 'Method(s) for callback must be specified', exception.message end def test_should_invoke_callbacks_during_failed_transition @machine.after_failure ->(object) { object.callbacks << 'failure' } @event.fire(@object) assert_equal %w[failure], @object.callbacks end def test_should_allow_multiple_callbacks @machine.after_failure ->(object) { object.callbacks << 'failure1' }, ->(object) { object.callbacks << 'failure2' } @event.fire(@object) assert_equal %w[failure1 failure2], @object.callbacks end def test_should_allow_multiple_callbacks_with_requirements @machine.after_failure ->(object) { object.callbacks << 'failure_ignite1' }, ->(object) { object.callbacks << 'failure_ignite2' }, on: :ignite @machine.after_failure ->(object) { object.callbacks << 'failure_park1' }, ->(object) { object.callbacks << 'failure_park2' }, on: :park @event.fire(@object) assert_equal %w[failure_ignite1 failure_ignite2], @object.callbacks end end state_machines-0.100.4/test/unit/machine/machine_with_helpers_test.rb000066400000000000000000000005461507333401300257560ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithHelpersTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @object = @klass.new end def test_should_throw_exception_with_invalid_scope assert_raises(KeyError) { @machine.define_helper(:invalid, :park) {} } end end state_machines-0.100.4/test/unit/machine/machine_with_initial_state_with_value_and_owner_default.rb000066400000000000000000000011641507333401300340720ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithInitialStateWithValueAndOwnerDefault < StateMachinesTest def setup @original_stderr = $stderr $stderr = StringIO.new state_machine_with_defaults = Class.new(StateMachines::Machine) do def owner_class_attribute_default 0 end end @klass = Class.new @machine = state_machine_with_defaults.new(@klass, initial: :parked) do state :parked, value: 0 end end def test_should_not_warn_about_wrong_default assert_equal '', $stderr.string end def teardown $stderr = @original_stderr end end state_machines-0.100.4/test/unit/machine/machine_with_initialize_and_super_test.rb000066400000000000000000000006071507333401300305130ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithInitializeAndSuperTest < StateMachinesTest def setup @klass = Class.new do def initialize super end end @machine = StateMachines::Machine.new(@klass, initial: :parked) @object = @klass.new end def test_should_initialize_state assert_equal 'parked', @object.state end end state_machines-0.100.4/test/unit/machine/machine_with_initialize_arguments_and_block_test.rb000066400000000000000000000013131507333401300325270ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithInitializeArgumentsAndBlockTest < StateMachinesTest def setup @superclass = Class.new do attr_reader :args attr_reader :block_given def initialize(*args) @args = args @block_given = block_given? end end @klass = Class.new(@superclass) @machine = StateMachines::Machine.new(@klass, initial: :parked) @object = @klass.new(1, 2, 3) {} end def test_should_initialize_state assert_equal 'parked', @object.state end def test_should_preserve_arguments assert_equal [1, 2, 3], @object.args end def test_should_preserve_block assert @object.block_given end end state_machines-0.100.4/test/unit/machine/machine_with_initialize_without_super_test.rb000066400000000000000000000005601507333401300314520ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithInitializeWithoutSuperTest < StateMachinesTest def setup @klass = Class.new do def initialize; end end @machine = StateMachines::Machine.new(@klass, initial: :parked) @object = @klass.new end def test_should_not_initialize_state assert_nil @object.state end end state_machines-0.100.4/test/unit/machine/machine_with_instance_helpers_test.rb000066400000000000000000000114671507333401300276460ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithInstanceHelpersTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @object = @klass.new end def test_should_not_redefine_existing_public_methods @klass.class_eval do def park true end end @machine.define_helper(:instance, :park) {} assert @object.park end def test_should_not_redefine_existing_protected_methods @klass.class_eval do protected def park true end end @machine.define_helper(:instance, :park) {} assert @object.send(:park) end def test_should_not_redefine_existing_private_methods @klass.class_eval do private def park true end end @machine.define_helper(:instance, :park) {} assert @object.send(:park) end def test_should_warn_if_defined_in_superclass @original_stderr = $stderr $stderr = StringIO.new superclass = Class.new do def park; end end klass = Class.new(superclass) machine = StateMachines::Machine.new(klass) machine.define_helper(:instance, :park) {} assert_equal "Instance method \"park\" is already defined in #{superclass}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string ensure $stderr = @original_stderr end def test_should_warn_if_defined_in_multiple_superclasses @original_stderr = $stderr $stderr = StringIO.new superclass1 = Class.new do def park; end end superclass2 = Class.new(superclass1) do def park; end end klass = Class.new(superclass2) machine = StateMachines::Machine.new(klass) machine.define_helper(:instance, :park) {} assert_equal "Instance method \"park\" is already defined in #{superclass1}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string ensure $stderr = @original_stderr end def test_should_warn_if_defined_in_module_prior_to_helper_module @original_stderr = $stderr $stderr = StringIO.new mod = Module.new do def park; end end klass = Class.new do include mod end machine = StateMachines::Machine.new(klass) machine.define_helper(:instance, :park) {} assert_equal "Instance method \"park\" is already defined in #{mod}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string ensure $stderr = @original_stderr end def test_should_not_warn_if_defined_in_module_after_helper_module @original_stderr = $stderr $stderr = StringIO.new klass = Class.new machine = StateMachines::Machine.new(klass) mod = Module.new do def park; end end klass.class_eval do include mod end machine.define_helper(:instance, :park) {} assert_equal '', $stderr.string ensure $stderr = @original_stderr end def test_should_define_if_ignoring_method_conflicts_and_defined_in_superclass @original_stderr = $stderr $stderr = StringIO.new StateMachines::Machine.ignore_method_conflicts = true superclass = Class.new do def park; end end klass = Class.new(superclass) machine = StateMachines::Machine.new(klass) machine.define_helper(:instance, :park) { true } assert_equal '', $stderr.string assert klass.new.park ensure StateMachines::Machine.ignore_method_conflicts = false $stderr = @original_stderr end def test_should_define_nonexistent_methods @machine.define_helper(:instance, :park) { false } refute @object.park end def test_should_warn_if_defined_multiple_times @original_stderr = $stderr $stderr = StringIO.new @machine.define_helper(:instance, :park) {} @machine.define_helper(:instance, :park) {} assert_equal "Instance method \"park\" is already defined in #{@klass} :state instance helpers, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string ensure $stderr = @original_stderr end def test_should_pass_context_as_arguments helper_args = nil @machine.define_helper(:instance, :park) { |*args| helper_args = args } @object.park assert_equal 2, helper_args.length assert_equal [@machine, @object], helper_args end def test_should_pass_method_arguments_through helper_args = nil @machine.define_helper(:instance, :park) { |*args| helper_args = args } @object.park(1, 2, 3) assert_equal 5, helper_args.length assert_equal [@machine, @object, 1, 2, 3], helper_args end def test_should_allow_string_evaluation @machine.define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1 def park false end END_EVAL refute @object.park end end state_machines-0.100.4/test/unit/machine/machine_with_integration_test.rb000066400000000000000000000034151507333401300266350ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithIntegrationTest < StateMachinesTest module Custom include StateMachines::Integrations::Base @defaults = { action: :save, use_transactions: false } attr_reader :initialized, :with_scopes, :without_scopes, :ran_transaction def after_initialize @initialized = true end def create_with_scope(name) (@with_scopes ||= []) << name -> {} end def create_without_scope(name) (@without_scopes ||= []) << name -> {} end def transaction(_) @ran_transaction = true yield end end def setup StateMachines::Integrations.register(MachineWithIntegrationTest::Custom) @machine = StateMachines::Machine.new(Class.new, integration: :custom) end def teardown StateMachines::Integrations.reset end def test_should_call_after_initialize_hook assert @machine.initialized end def test_should_use_the_default_action assert_equal :save, @machine.action end def test_should_use_the_custom_action_if_specified machine = StateMachines::Machine.new(Class.new, integration: :custom, action: :save!) assert_equal :save!, machine.action end def test_should_use_the_default_use_transactions refute @machine.use_transactions end def test_should_use_the_custom_use_transactions_if_specified machine = StateMachines::Machine.new(Class.new, integration: :custom, use_transactions: true) assert machine.use_transactions end def test_should_define_a_singular_and_plural_with_scope assert_equal %w[with_state with_states], @machine.with_scopes end def test_should_define_a_singular_and_plural_without_scope assert_equal %w[without_state without_states], @machine.without_scopes end end state_machines-0.100.4/test/unit/machine/machine_with_multiple_events_test.rb000066400000000000000000000015321507333401300275270ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithMultipleEventsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @park, @shift_down = @machine.event(:park, :shift_down) do transition first_gear: :parked end end def test_should_have_events assert_equal [@park, @shift_down], @machine.events.to_a end def test_should_define_transitions_for_each_event [@park, @shift_down].each { |event| assert_equal 1, event.branches.size } end def test_should_transition_the_same_for_each_event object = @klass.new object.state = 'first_gear' object.park assert_equal 'parked', object.state object = @klass.new object.state = 'first_gear' object.shift_down assert_equal 'parked', object.state end end state_machines-0.100.4/test/unit/machine/machine_with_namespace_test.rb000066400000000000000000000022171507333401300262450ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithNamespaceTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, namespace: 'alarm', initial: :active) do event :enable do transition off: :active end event :disable do transition active: :off end end @object = @klass.new end def test_should_namespace_state_predicates %i[alarm_active? alarm_off?].each do |name| assert_respond_to @object, name end end def test_should_namespace_event_checks %i[can_enable_alarm? can_disable_alarm?].each do |name| assert_respond_to @object, name end end def test_should_namespace_event_transition_readers %i[enable_alarm_transition disable_alarm_transition].each do |name| assert_respond_to @object, name end end def test_should_namespace_events %i[enable_alarm disable_alarm].each do |name| assert_respond_to @object, name end end def test_should_namespace_bang_events %i[enable_alarm! disable_alarm!].each do |name| assert_respond_to @object, name end end end state_machines-0.100.4/test/unit/machine/machine_with_nil_action_test.rb000066400000000000000000000012561507333401300264320ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithNilActionTest < StateMachinesTest module Custom include StateMachines::Integrations::Base @defaults = { action: :save } end def setup StateMachines::Integrations.register(MachineWithNilActionTest::Custom) end def teardown StateMachines::Integrations.reset end def test_should_have_a_nil_action machine = StateMachines::Machine.new(Class.new, action: nil, integration: :custom) assert_nil machine.action end def test_should_have_default_action machine = StateMachines::Machine.new(Class.new, integration: :custom) assert_equal :save, machine.action end end state_machines-0.100.4/test/unit/machine/machine_with_other_states.rb000066400000000000000000000010561507333401300257560ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithOtherStates < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @parked, @idling = @machine.other_states(:parked, :idling) end def test_should_include_other_states_in_known_states assert_equal [@parked, @idling], @machine.states.to_a end def test_should_use_default_value assert_equal 'idling', @idling.value end def test_should_not_create_matcher assert_nil @idling.matcher end end state_machines-0.100.4/test/unit/machine/machine_with_owner_subclass_test.rb000066400000000000000000000010011507333401300273300ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithOwnerSubclassTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @subclass = Class.new(@klass) end def test_should_have_a_different_collection_of_state_machines refute_same @klass.state_machines, @subclass.state_machines end def test_should_have_the_same_attribute_associated_state_machines assert_equal @klass.state_machines, @subclass.state_machines end end state_machines-0.100.4/test/unit/machine/machine_with_paths_test.rb000066400000000000000000000014131507333401300254250ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithPathsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.event :ignite do transition parked: :idling end @machine.event :shift_up do transition first_gear: :second_gear end @object = @klass.new @object.state = 'parked' end def test_should_have_paths assert_equal [[StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)]], @machine.paths_for(@object) end def test_should_allow_requirement_configuration assert_equal [[StateMachines::Transition.new(@object, @machine, :shift_up, :first_gear, :second_gear)]], @machine.paths_for(@object, from: :first_gear) end end state_machines-0.100.4/test/unit/machine/machine_with_private_action_test.rb000066400000000000000000000022151507333401300273160ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithPrivateActionTest < StateMachinesTest def setup @superclass = Class.new do private def save; end end @klass = Class.new(@superclass) @machine = StateMachines::Machine.new(@klass, action: :save) @object = @klass.new end def test_should_define_an_event_attribute_reader assert_respond_to @object, :state_event end def test_should_define_an_event_attribute_writer assert_respond_to @object, :state_event= end def test_should_define_an_event_transition_attribute_reader assert @object.respond_to?(:state_event_transition, true) end def test_should_define_an_event_transition_attribute_writer assert @object.respond_to?(:state_event_transition=, true) end def test_should_define_action assert(@klass.ancestors.any? { |ancestor| ![@klass, @superclass].include?(ancestor) && ancestor.private_method_defined?(:save) }) end def test_should_keep_action_private assert @klass.private_method_defined?(:save) end def test_should_mark_action_hook_as_defined assert_predicate @machine, :action_hook? end end state_machines-0.100.4/test/unit/machine/machine_with_state_matchers_test.rb000066400000000000000000000027451507333401300273250ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithStateMatchersTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) end def test_should_empty_array_for_all_matcher assert_empty @machine.state(StateMachines::AllMatcher.instance) end def test_should_return_referenced_states_for_blacklist_matcher assert_instance_of StateMachines::State, @machine.state(StateMachines::BlacklistMatcher.new([:parked])) end def test_should_not_allow_configurations expected_hash = { human_name: 'Parked' } expected_message = "Cannot configure states when using matchers (using #{expected_hash.inspect})" exception = assert_raises(ArgumentError) do @machine.state(StateMachines::BlacklistMatcher.new([:parked]), human_name: 'Parked') end assert_equal expected_message, exception.message end def test_should_track_referenced_states @machine.state(StateMachines::BlacklistMatcher.new([:parked])) assert_equal([nil, :parked], @machine.states.map { |state| state.name }) end def test_should_eval_context_for_matching_states contexts_run = [] @machine.event(StateMachines::BlacklistMatcher.new([:parked])) { contexts_run << name } @machine.event :parked assert_empty contexts_run @machine.event :idling assert_equal [:idling], contexts_run @machine.event :first_gear, :second_gear assert_equal %i[idling first_gear second_gear], contexts_run end end state_machines-0.100.4/test/unit/machine/machine_with_state_with_matchers_test.rb000066400000000000000000000007131507333401300303510ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithStateWithMatchersTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @state = @machine.state :parked, if: ->(value) { !value.nil? } @object = @klass.new @object.state = 1 end def test_should_use_custom_matcher refute_nil @state.matcher assert @state.matches?(1) refute @state.matches?(nil) end end state_machines-0.100.4/test/unit/machine/machine_with_states_test.rb000066400000000000000000000033761507333401300256230ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithStatesTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @parked, @idling = @machine.state :parked, :idling @object = @klass.new end def test_should_have_states assert_equal([nil, :parked, :idling], @machine.states.map { |state| state.name }) end def test_should_allow_state_lookup_by_name assert_equal @parked, @machine.states[:parked] end def test_should_allow_state_lookup_by_value assert_equal @parked, @machine.states['parked', :value] end def test_should_allow_human_state_name_lookup assert_equal 'parked', @klass.human_state_name(:parked) end def test_should_raise_exception_on_invalid_human_state_name_lookup exception = assert_raises(IndexError) { @klass.human_state_name(:invalid) } assert_equal ':invalid is an invalid name', exception.message end def test_should_use_stringified_name_for_value assert_equal 'parked', @parked.value end def test_should_not_use_custom_matcher assert_nil @parked.matcher end def test_should_raise_exception_if_invalid_option_specified exception = assert_raises(ArgumentError) { @machine.state(:first_gear, invalid: true) } assert_equal 'Unknown key: :invalid. Valid keys are: :value, :cache, :if, :human_name', exception.message end def test_should_raise_exception_if_conflicting_type_used_for_name exception = assert_raises(ArgumentError) { @machine.state 'first_gear' } assert_equal '"first_gear" state defined as String, :parked defined as Symbol; all states must be consistent', exception.message end def test_should_not_raise_exception_if_conflicting_type_is_nil_for_name @machine.state nil end end state_machines-0.100.4/test/unit/machine/machine_with_states_with_behaviors_test.rb000066400000000000000000000011451507333401300307100ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithStatesWithBehaviorsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @parked, @idling = @machine.state :parked, :idling do def speed 0 end end end def test_should_define_behaviors_for_each_state refute_nil @parked.context_methods[:speed] refute_nil @idling.context_methods[:speed] end def test_should_define_different_behaviors_for_each_state refute_equal @parked.context_methods[:speed], @idling.context_methods[:speed] end end state_machines-0.100.4/test/unit/machine/machine_with_states_with_custom_human_names_test.rb000066400000000000000000000007451507333401300326200ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithStatesWithCustomHumanNamesTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @state = @machine.state :parked, human_name: 'stopped' end def test_should_use_custom_human_name assert_equal 'stopped', @state.human_name end def test_should_allow_human_state_name_lookup assert_equal 'stopped', @klass.human_state_name(:parked) end end state_machines-0.100.4/test/unit/machine/machine_with_states_with_custom_values_test.rb000066400000000000000000000007501507333401300316200ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithStatesWithCustomValuesTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @state = @machine.state :parked, value: 1 @object = @klass.new @object.state = 1 end def test_should_use_custom_value assert_equal 1, @state.value end def test_should_allow_lookup_by_custom_value assert_equal @state, @machine.states[1, :value] end end state_machines-0.100.4/test/unit/machine/machine_with_states_with_runtime_dependencies_test.rb000066400000000000000000000007751507333401300331270ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithStatesWithRuntimeDependenciesTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked end def test_should_not_evaluate_value_during_definition @machine.state :parked, value: -> { raise ArgumentError } end def test_should_not_evaluate_if_not_initial_state @machine.state :parked, value: -> { raise ArgumentError } @klass.new end end state_machines-0.100.4/test/unit/machine/machine_with_static_initial_state_test.rb000066400000000000000000000025561507333401300305170ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithStaticInitialStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) end def test_should_not_have_dynamic_initial_state refute_predicate @machine, :dynamic_initial_state? end def test_should_have_an_initial_state object = @klass.new assert_equal 'parked', @machine.initial_state(object).value end def test_should_write_to_attribute_when_initializing_state object = @klass.allocate @machine.initialize_state(object) assert_sm_state(object, :parked) end def test_should_set_initial_on_state_object assert @machine.state(:parked).initial end def test_should_set_initial_state_on_created_object object = @klass.new assert_sm_state(object, :parked) end def test_should_have_correct_initial_state assert_sm_initial_state(@machine, :parked) end def test_should_not_initial_state_prior_to_initialization base = Class.new do attr_accessor :state_on_init def initialize self.state_on_init = state end end klass = Class.new(base) StateMachines::Machine.new(klass, initial: :parked) assert_nil klass.new.state_on_init end def test_should_be_included_in_known_states assert_sm_states_list(@machine, [:parked]) end end machine_with_superclass_conflicting_helpers_after_definition_test.rb000066400000000000000000000014321507333401300361060ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine# frozen_string_literal: true require 'test_helper' require 'stringio' class MachineWithSuperclassConflictingHelpersAfterDefinitionTest < StateMachinesTest def setup @original_stderr = $stderr $stderr = StringIO.new @superclass = Class.new @klass = Class.new(@superclass) @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @superclass.class_eval do def state? true end end @object = @klass.new end def teardown $stderr = @original_stderr end def test_should_call_superclass_attribute_predicate_without_arguments assert_predicate @object, :state? end def test_should_define_attribute_predicate_with_arguments refute @object.state?(:parked) end end state_machines-0.100.4/test/unit/machine/machine_with_transition_callbacks_test.rb000066400000000000000000000153771507333401300305150ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithTransitionCallbacksTest < StateMachinesTest def setup @klass = Class.new do attr_accessor :callbacks end @machine = StateMachines::Machine.new(@klass, initial: :parked) @event = @machine.event :ignite do transition parked: :idling end @object = @klass.new @object.callbacks = [] end def test_should_not_raise_exception_if_implicit_option_specified @machine.before_transition invalid: :valid, do: -> {} end def test_should_raise_exception_if_method_not_specified exception = assert_raises(ArgumentError) { @machine.before_transition to: :idling } assert_equal 'Method(s) for callback must be specified', exception.message end def test_should_invoke_callbacks_during_transition @machine.before_transition ->(object) { object.callbacks << 'before' } @machine.after_transition ->(object) { object.callbacks << 'after' } @machine.around_transition lambda { |object, _transition, block| object.callbacks << 'before_around' block.call object.callbacks << 'after_around' } @event.fire(@object) assert_equal %w[before before_around after_around after], @object.callbacks end def test_should_allow_multiple_callbacks @machine.before_transition ->(object) { object.callbacks << 'before1' }, ->(object) { object.callbacks << 'before2' } @machine.after_transition ->(object) { object.callbacks << 'after1' }, ->(object) { object.callbacks << 'after2' } @machine.around_transition( lambda { |object, _transition, block| object.callbacks << 'before_around1' block.call object.callbacks << 'after_around1' }, lambda { |object, _transition, block| object.callbacks << 'before_around2' block.call object.callbacks << 'after_around2' } ) @event.fire(@object) assert_equal %w[before1 before2 before_around1 before_around2 after_around2 after_around1 after1 after2], @object.callbacks end def test_should_allow_multiple_callbacks_with_requirements @machine.before_transition ->(object) { object.callbacks << 'before_parked1' }, ->(object) { object.callbacks << 'before_parked2' }, from: :parked @machine.before_transition ->(object) { object.callbacks << 'before_idling1' }, ->(object) { object.callbacks << 'before_idling2' }, from: :idling @machine.after_transition ->(object) { object.callbacks << 'after_parked1' }, ->(object) { object.callbacks << 'after_parked2' }, from: :parked @machine.after_transition ->(object) { object.callbacks << 'after_idling1' }, ->(object) { object.callbacks << 'after_idling2' }, from: :idling @machine.around_transition( lambda { |object, _transition, block| object.callbacks << 'before_around_parked1' block.call object.callbacks << 'after_around_parked1' }, lambda { |object, _transition, block| object.callbacks << 'before_around_parked2' block.call object.callbacks << 'after_around_parked2' }, from: :parked ) @machine.around_transition( lambda { |object, _transition, block| object.callbacks << 'before_around_idling1' block.call object.callbacks << 'after_around_idling1' }, lambda { |object, _transition, block| object.callbacks << 'before_around_idling2' block.call object.callbacks << 'after_around_idling2' }, from: :idling ) @event.fire(@object) assert_equal %w[before_parked1 before_parked2 before_around_parked1 before_around_parked2 after_around_parked2 after_around_parked1 after_parked1 after_parked2], @object.callbacks end def test_should_support_from_requirement @machine.before_transition from: :parked, do: ->(object) { object.callbacks << :parked } @machine.before_transition from: :idling, do: ->(object) { object.callbacks << :idling } @event.fire(@object) assert_equal [:parked], @object.callbacks end def test_should_support_except_from_requirement @machine.before_transition except_from: :parked, do: ->(object) { object.callbacks << :parked } @machine.before_transition except_from: :idling, do: ->(object) { object.callbacks << :idling } @event.fire(@object) assert_equal [:idling], @object.callbacks end def test_should_support_to_requirement @machine.before_transition to: :parked, do: ->(object) { object.callbacks << :parked } @machine.before_transition to: :idling, do: ->(object) { object.callbacks << :idling } @event.fire(@object) assert_equal [:idling], @object.callbacks end def test_should_support_except_to_requirement @machine.before_transition except_to: :parked, do: ->(object) { object.callbacks << :parked } @machine.before_transition except_to: :idling, do: ->(object) { object.callbacks << :idling } @event.fire(@object) assert_equal [:parked], @object.callbacks end def test_should_support_on_requirement @machine.before_transition on: :park, do: ->(object) { object.callbacks << :park } @machine.before_transition on: :ignite, do: ->(object) { object.callbacks << :ignite } @event.fire(@object) assert_equal [:ignite], @object.callbacks end def test_should_support_except_on_requirement @machine.before_transition except_on: :park, do: ->(object) { object.callbacks << :park } @machine.before_transition except_on: :ignite, do: ->(object) { object.callbacks << :ignite } @event.fire(@object) assert_equal [:park], @object.callbacks end def test_should_support_implicit_requirement @machine.before_transition parked: :idling, do: ->(object) { object.callbacks << :parked } @machine.before_transition idling: :parked, do: ->(object) { object.callbacks << :idling } @event.fire(@object) assert_equal [:parked], @object.callbacks end def test_should_track_states_defined_in_transition_callbacks @machine.before_transition parked: :idling, do: -> {} @machine.after_transition first_gear: :second_gear, do: -> {} @machine.around_transition third_gear: :fourth_gear, do: -> {} assert_equal(%i[parked idling first_gear second_gear third_gear fourth_gear], @machine.states.map { |state| state.name }) end def test_should_not_duplicate_states_defined_in_multiple_event_transitions @machine.before_transition parked: :idling, do: -> {} @machine.after_transition first_gear: :second_gear, do: -> {} @machine.after_transition parked: :idling, do: -> {} @machine.around_transition parked: :idling, do: -> {} assert_equal(%i[parked idling first_gear second_gear], @machine.states.map { |state| state.name }) end def test_should_define_predicates_for_each_state %i[parked? idling?].each { |predicate| assert_respond_to @object, predicate } end end state_machines-0.100.4/test/unit/machine/machine_with_transitions_test.rb000066400000000000000000000062301507333401300266650ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithTransitionsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) end def test_should_require_on_event exception = assert_raises(ArgumentError) { @machine.transition(parked: :idling) } assert_equal 'Must specify :on event', exception.message end def test_should_not_allow_except_on_option exception = assert_raises(ArgumentError) { @machine.transition(except_on: :ignite, on: :ignite) } assert_equal 'Unknown key: :except_on. Valid keys are: :from, :to, :except_from, :except_to, :if, :unless, :if_state, :unless_state, :if_all_states, :unless_all_states, :if_any_state, :unless_any_state', exception.message end def test_should_allow_transitioning_without_a_to_state @machine.transition(from: :parked, on: :ignite) end def test_should_allow_transitioning_without_a_from_state @machine.transition(to: :idling, on: :ignite) end def test_should_allow_except_from_option @machine.transition(except_from: :idling, on: :ignite) end def test_should_allow_except_to_option @machine.transition(except_to: :parked, on: :ignite) end def test_should_allow_implicit_options branch = @machine.transition(first_gear: :second_gear, on: :shift_up) assert_instance_of StateMachines::Branch, branch state_requirements = branch.state_requirements assert_equal 1, state_requirements.length assert_instance_of StateMachines::WhitelistMatcher, state_requirements[0][:from] assert_equal [:first_gear], state_requirements[0][:from].values assert_instance_of StateMachines::WhitelistMatcher, state_requirements[0][:to] assert_equal [:second_gear], state_requirements[0][:to].values assert_instance_of StateMachines::WhitelistMatcher, branch.event_requirement assert_equal [:shift_up], branch.event_requirement.values end def test_should_allow_multiple_implicit_options branch = @machine.transition(first_gear: :second_gear, second_gear: :third_gear, on: :shift_up) state_requirements = branch.state_requirements assert_equal 2, state_requirements.length end def test_should_allow_verbose_options branch = @machine.transition(from: :parked, to: :idling, on: :ignite) assert_instance_of StateMachines::Branch, branch end def test_should_include_all_transition_states_in_machine_states @machine.transition(parked: :idling, on: :ignite) assert_equal(%i[parked idling], @machine.states.map { |state| state.name }) end def test_should_include_all_transition_events_in_machine_events @machine.transition(parked: :idling, on: :ignite) assert_equal([:ignite], @machine.events.map { |event| event.name }) end def test_should_allow_multiple_events branches = @machine.transition(parked: :ignite, on: %i[ignite shift_up]) assert_equal 2, branches.length assert_equal(%i[ignite shift_up], @machine.events.map { |event| event.name }) end def test_should_not_modify_options options = { parked: :idling, on: :ignite } @machine.transition(options) assert_equal({ parked: :idling, on: :ignite }, options) end end state_machines-0.100.4/test/unit/machine/machine_without_initialization_test.rb000066400000000000000000000013741507333401300300730ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithoutInitializationTest < StateMachinesTest def setup @klass = Class.new do def initialize(attributes = {}) attributes.each { |attr, value| send("#{attr}=", value) } super() end end @machine = StateMachines::Machine.new(@klass, initial: :parked, initialize: false) end def test_should_not_have_an_initial_state object = @klass.new assert_nil object.state end def test_should_still_allow_manual_initialization @klass.send(:include, Module.new do def initialize(_attributes = {}) super() initialize_state_machines end end) object = @klass.new assert_equal 'parked', object.state end end state_machines-0.100.4/test/unit/machine/machine_without_initialize_test.rb000066400000000000000000000004741507333401300272050ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithoutInitializeTest < StateMachinesTest def setup klass = Class.new StateMachines::Machine.new(klass, initial: :parked) @object = klass.new end def test_should_initialize_state assert_equal 'parked', @object.state end end state_machines-0.100.4/test/unit/machine/machine_without_integration_test.rb000066400000000000000000000013031507333401300273570ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineWithoutIntegrationTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @object = @klass.new end def test_transaction_should_yield @yielded = false @machine.within_transaction(@object) do @yielded = true end assert @yielded end def test_invalidation_should_do_nothing assert_nil @machine.invalidate(@object, :state, :invalid_transition, [[:event, 'park']]) end def test_reset_should_do_nothing assert_nil @machine.reset(@object) end def test_errors_for_should_be_empty assert_equal '', @machine.errors_for(@object) end end state_machines-0.100.4/test/unit/machine_collection/000077500000000000000000000000001507333401300224175ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine_collection/machine_collection_by_default_test.rb000066400000000000000000000004041507333401300320160ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineCollectionByDefaultTest < StateMachinesTest def setup @machines = StateMachines::MachineCollection.new end def test_should_not_have_any_machines assert_empty @machines end end state_machines-0.100.4/test/unit/machine_collection/machine_collection_fire_test.rb000066400000000000000000000050211507333401300306250ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineCollectionFireTest < StateMachinesTest def setup @machines = StateMachines::MachineCollection.new @klass = Class.new do attr_reader :saved def save @saved = true end end # First machine @machines[:state] = @state = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save) @state.event :ignite do transition parked: :idling end @state.event :park do transition idling: :parked end # Second machine @machines[:alarm_state] = @alarm_state = StateMachines::Machine.new(@klass, :alarm_state, initial: :active, action: :save, namespace: 'alarm') @alarm_state.event :enable do transition off: :active end @alarm_state.event :disable do transition active: :off end @object = @klass.new end def test_should_raise_exception_if_invalid_event_specified exception = assert_raises(StateMachines::InvalidEvent) { @machines.fire_events(@object, :invalid) } assert_equal :invalid, exception.event exception = assert_raises(StateMachines::InvalidEvent) { @machines.fire_events(@object, :ignite, :invalid) } assert_equal :invalid, exception.event end def test_should_fail_if_any_event_cannot_transition refute @machines.fire_events(@object, :park, :disable_alarm) assert_equal 'parked', @object.state assert_equal 'active', @object.alarm_state refute @object.saved refute @machines.fire_events(@object, :ignite, :enable_alarm) assert_equal 'parked', @object.state assert_equal 'active', @object.alarm_state refute @object.saved end def test_should_run_failure_callbacks_if_any_event_cannot_transition @state_failure_run = @alarm_state_failure_run = false @machines[:state].after_failure { @state_failure_run = true } @machines[:alarm_state].after_failure { @alarm_state_failure_run = true } refute @machines.fire_events(@object, :park, :disable_alarm) assert @state_failure_run refute @alarm_state_failure_run end def test_should_be_successful_if_all_events_transition assert @machines.fire_events(@object, :ignite, :disable_alarm) assert_equal 'idling', @object.state assert_equal 'off', @object.alarm_state assert @object.saved end def test_should_not_save_if_skipping_action assert @machines.fire_events(@object, :ignite, :disable_alarm, false) assert_equal 'idling', @object.state assert_equal 'off', @object.alarm_state refute @object.saved end end machine_collection_fire_with_transactions_test.rb000066400000000000000000000024431507333401300343760ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine_collection# frozen_string_literal: true require 'test_helper' class MachineCollectionFireAttributesWithValidationsTest < StateMachinesTest def setup @klass = Class.new do attr_accessor :errors def initialize @errors = [] super end end @machines = StateMachines::MachineCollection.new @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save) @machine.event :ignite do transition parked: :idling end class << @machine def invalidate(object, _attribute, message, values = []) (object.errors ||= []) << generate_message(message, values) end def reset(object) object.errors = [] end end @object = @klass.new end def test_should_invalidate_if_event_is_invalid @object.state_event = 'invalid' @machines.transitions(@object, :save) refute_empty @object.errors end def test_should_invalidate_if_no_transition_exists @object.state = 'idling' @object.state_event = 'ignite' @machines.transitions(@object, :save) refute_empty @object.errors end def test_should_not_invalidate_if_transition_exists @object.state_event = 'ignite' @machines.transitions(@object, :save) assert_empty @object.errors end end state_machines-0.100.4/test/unit/machine_collection/machine_collection_fire_with_validations_test.rb000066400000000000000000000042771507333401300342710ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' module MachineCollectionFireWithValidationsIntegration include StateMachines::Integrations::Base def self.integration_name :custom_validation end def invalidate(object, _attribute, message, values = []) (object.errors ||= []) << generate_message(message, values) end def reset(object) object.errors = [] end end class MachineCollectionFireWithValidationsTest < StateMachinesTest def setup StateMachines::Integrations.reset StateMachines::Integrations.register(MachineCollectionFireWithValidationsIntegration) @klass = Class.new do attr_accessor :errors def initialize @errors = [] super end end @machines = StateMachines::MachineCollection.new @machines[:state] = @state = StateMachines::Machine.new(@klass, :state, initial: :parked, integration: :custom_validation) @state.event :ignite do transition parked: :idling end @machines[:alarm_state] = @alarm_state = StateMachines::Machine.new(@klass, :alarm_state, initial: :active, namespace: 'alarm', integration: :custom_validation) @alarm_state.event :disable do transition active: :off end @object = @klass.new end def teardown StateMachines::Integrations.reset end def test_should_not_invalidate_if_transitions_exist assert @machines.fire_events(@object, :ignite, :disable_alarm) assert_empty @object.errors end def test_should_invalidate_if_no_transitions_exist @object.state = 'idling' @object.alarm_state = 'off' refute @machines.fire_events(@object, :ignite, :disable_alarm) assert_equal ['cannot transition via "ignite"', 'cannot transition via "disable"'], @object.errors end def test_should_run_failure_callbacks_if_no_transitions_exist @object.state = 'idling' @object.alarm_state = 'off' @state_failure_run = @alarm_state_failure_run = false @machines[:state].after_failure { @state_failure_run = true } @machines[:alarm_state].after_failure { @alarm_state_failure_run = true } refute @machines.fire_events(@object, :ignite, :disable_alarm) assert @state_failure_run assert @alarm_state_failure_run end end state_machines-0.100.4/test/unit/machine_collection/machine_collection_state_initialization_test.rb000066400000000000000000000066251507333401300341420ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MachineCollectionStateInitializationTest < StateMachinesTest def setup @machines = StateMachines::MachineCollection.new @klass = Class.new @machines[:state] = StateMachines::Machine.new(@klass, :state, initial: :parked) @machines[:alarm_state] = StateMachines::Machine.new(@klass, :alarm_state, initial: ->(_object) { :active }) @machines[:alarm_state].state :active, value: -> { 'active' } # Prevent the auto-initialization hook from firing @klass.class_eval do def initialize; end end @object = @klass.new @object.state = nil @object.alarm_state = nil end def test_should_raise_exception_if_invalid_option_specified assert_raises(ArgumentError) { @machines.initialize_states(@object, invalid: true) } end def test_should_initialize_static_states_after_block @machines.initialize_states(@object) do @state_in_block = @object.state @alarm_state_in_block = @object.alarm_state end assert_nil @state_in_block assert_nil @alarm_state_in_block end def test_should_initialize_dynamic_states_after_block @machines.initialize_states(@object) do @alarm_state_in_block = @object.alarm_state end assert_nil @alarm_state_in_block assert_equal 'active', @object.alarm_state end def test_should_initialize_all_states_without_block @machines.initialize_states(@object) assert_equal 'parked', @object.state assert_equal 'active', @object.alarm_state end def test_should_skip_static_states_if_disabled @machines.initialize_states(@object, static: false) assert_nil @object.state assert_equal 'active', @object.alarm_state end def test_should_initialize_existing_static_states_by_default @object.state = 'idling' @machines.initialize_states(@object) assert_equal 'parked', @object.state end def test_should_initialize_existing_static_states_if_forced @object.state = 'idling' @machines.initialize_states(@object, static: :force) assert_equal 'parked', @object.state end def test_should_initialize_existing_static_states_if_not_forced @object.state = 'idling' @machines.initialize_states(@object, static: true) assert_equal 'parked', @object.state end def test_should_skip_dynamic_states_if_disabled @machines.initialize_states(@object, dynamic: false) assert_equal 'parked', @object.state assert_nil @object.alarm_state end def test_should_not_initialize_existing_dynamic_states_by_default @object.alarm_state = 'inactive' @machines.initialize_states(@object) assert_equal 'inactive', @object.alarm_state end def test_should_initialize_existing_dynamic_states_if_forced @object.alarm_state = 'inactive' @machines.initialize_states(@object, dynamic: :force) assert_equal 'active', @object.alarm_state end def test_should_not_initialize_existing_dynamic_states_if_not_forced @object.alarm_state = 'inactive' @machines.initialize_states(@object, dynamic: true) assert_equal 'inactive', @object.alarm_state end def test_shouldnt_force_state_given_either_as_string_or_symbol @object.state = 'notparked' @machines.initialize_states(@object, {}, { state: 'parked' }) assert_equal 'notparked', @object.state @machines.initialize_states(@object, {}, { 'state' => 'parked' }) assert_equal 'notparked', @object.state end end machine_collection_transitions_with_blank_events_test.rb000066400000000000000000000012121507333401300357620ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine_collection# frozen_string_literal: true require 'test_helper' class MachineCollectionTransitionsWithBlankEventsTest < StateMachinesTest def setup @klass = Class.new @machines = StateMachines::MachineCollection.new @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save) @machine.event :ignite do transition parked: :idling end @object = @klass.new @object.state_event = '' @transitions = @machines.transitions(@object, :save) end def test_should_be_empty assert_empty @transitions end def test_should_perform assert @transitions.perform end end machine_collection_transitions_with_custom_options_test.rb000066400000000000000000000011131507333401300363740ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine_collection# frozen_string_literal: true require 'test_helper' class MachineCollectionTransitionsWithCustomOptionsTest < StateMachinesTest def setup @klass = Class.new @machines = StateMachines::MachineCollection.new @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save) @machine.event :ignite do transition parked: :idling end @object = @klass.new @transitions = @machines.transitions(@object, :save, after: false) end def test_should_use_custom_options assert @transitions.skip_after end end machine_collection_transitions_with_different_actions_test.rb000066400000000000000000000015341507333401300370040ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine_collection# frozen_string_literal: true require 'test_helper' class MachineCollectionTransitionsWithDifferentActionsTest < StateMachinesTest def setup @klass = Class.new @machines = StateMachines::MachineCollection.new @machines[:state] = @state = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save) @state.event :ignite do transition parked: :idling end @machines[:status] = @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :persist) @status.event :shift_up do transition first_gear: :second_gear end @object = @klass.new @object.state_event = 'ignite' @object.status_event = 'shift_up' @transitions = @machines.transitions(@object, :save) end def test_should_only_select_matching_actions assert_equal 1, @transitions.length end end machine_collection_transitions_with_exisiting_transitions_test.rb000066400000000000000000000013741507333401300377600ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine_collection# frozen_string_literal: true require 'test_helper' class MachineCollectionTransitionsWithExisitingTransitionsTest < StateMachinesTest def setup @klass = Class.new @machines = StateMachines::MachineCollection.new @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save) @machine.event :ignite do transition parked: :idling end @object = @klass.new @object.send(:state_event_transition=, StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)) @transitions = @machines.transitions(@object, :save) end def test_should_not_be_empty assert_equal 1, @transitions.length end def test_should_perform assert @transitions.perform end end machine_collection_transitions_with_invalid_events_test.rb000066400000000000000000000012271507333401300363270ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine_collection# frozen_string_literal: true require 'test_helper' class MachineCollectionTransitionsWithInvalidEventsTest < StateMachinesTest def setup @klass = Class.new @machines = StateMachines::MachineCollection.new @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save) @machine.event :ignite do transition parked: :idling end @object = @klass.new @object.state_event = 'invalid' @transitions = @machines.transitions(@object, :save) end def test_should_be_empty assert_empty @transitions end def test_should_not_perform refute @transitions.perform end end machine_collection_transitions_with_same_actions_test.rb000066400000000000000000000016131507333401300357610ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine_collection# frozen_string_literal: true require 'test_helper' class MachineCollectionTransitionsWithSameActionsTest < StateMachinesTest def setup @klass = Class.new @machines = StateMachines::MachineCollection.new @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save) @machine.event :ignite do transition parked: :idling end @machines[:status] = @machine = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save) @machine.event :shift_up do transition first_gear: :second_gear end @object = @klass.new @object.state_event = 'ignite' @object.status_event = 'shift_up' @transitions = @machines.transitions(@object, :save) end def test_should_not_be_empty assert_equal 2, @transitions.length end def test_should_perform assert @transitions.perform end end machine_collection_transitions_with_transition_test.rb000066400000000000000000000012351507333401300355060ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine_collection# frozen_string_literal: true require 'test_helper' class MachineCollectionTransitionsWithTransitionTest < StateMachinesTest def setup @klass = Class.new @machines = StateMachines::MachineCollection.new @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save) @machine.event :ignite do transition parked: :idling end @object = @klass.new @object.state_event = 'ignite' @transitions = @machines.transitions(@object, :save) end def test_should_not_be_empty assert_equal 1, @transitions.length end def test_should_perform assert @transitions.perform end end machine_collection_transitions_without_events_test.rb000066400000000000000000000012111507333401300353420ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine_collection# frozen_string_literal: true require 'test_helper' class MachineCollectionTransitionsWithoutEventsTest < StateMachinesTest def setup @klass = Class.new @machines = StateMachines::MachineCollection.new @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save) @machine.event :ignite do transition parked: :idling end @object = @klass.new @object.state_event = nil @transitions = @machines.transitions(@object, :save) end def test_should_be_empty assert_empty @transitions end def test_should_perform assert @transitions.perform end end machine_collection_transitions_without_transition_test.rb000066400000000000000000000012631507333401300362370ustar00rootroot00000000000000state_machines-0.100.4/test/unit/machine_collection# frozen_string_literal: true require 'test_helper' class MachineCollectionTransitionsWithoutTransitionTest < StateMachinesTest def setup @klass = Class.new @machines = StateMachines::MachineCollection.new @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save) @machine.event :ignite do transition parked: :idling end @object = @klass.new @object.state = 'idling' @object.state_event = 'ignite' @transitions = @machines.transitions(@object, :save) end def test_should_be_empty assert_empty @transitions end def test_should_not_perform refute @transitions.perform end end state_machines-0.100.4/test/unit/matcher/000077500000000000000000000000001507333401300202235ustar00rootroot00000000000000state_machines-0.100.4/test/unit/matcher/all_matcher_test.rb000066400000000000000000000014271507333401300240660ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class AllMatcherTest < StateMachinesTest def setup @matcher = StateMachines::AllMatcher.instance end def test_should_have_no_values assert_empty @matcher.values end def test_should_always_match [nil, :parked, :idling].each { |value| assert @matcher.matches?(value) } end def test_should_not_filter_any_values assert_equal %i[parked idling], @matcher.filter(%i[parked idling]) end def test_should_generate_blacklist_matcher_after_subtraction matcher = @matcher - %i[parked idling] assert_instance_of StateMachines::BlacklistMatcher, matcher assert_equal %i[parked idling], matcher.values end def test_should_have_a_description assert_equal 'all', @matcher.description end end state_machines-0.100.4/test/unit/matcher/blacklist_matcher_test.rb000066400000000000000000000014461507333401300252670ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BlacklistMatcherTest < StateMachinesTest def setup @matcher = StateMachines::BlacklistMatcher.new(%i[parked idling]) end def test_should_have_values assert_equal %i[parked idling], @matcher.values end def test_should_filter_known_values assert_equal [:first_gear], @matcher.filter(%i[parked idling first_gear]) end def test_should_match_unknown_values assert @matcher.matches?(:first_gear) end def test_should_not_match_known_values refute @matcher.matches?(:parked) end def test_should_have_a_description assert_equal 'all - [:parked, :idling]', @matcher.description matcher = StateMachines::BlacklistMatcher.new([:parked]) assert_equal 'all - :parked', matcher.description end end state_machines-0.100.4/test/unit/matcher/loopback_matcher_test.rb000066400000000000000000000012241507333401300251030ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class LoopbackMatcherTest < StateMachinesTest def setup @matcher = StateMachines::LoopbackMatcher.instance end def test_should_have_no_values assert_empty @matcher.values end def test_should_filter_all_values assert_empty @matcher.filter(%i[parked idling]) end def test_should_match_if_from_context_is_same assert @matcher.matches?(:parked, from: :parked) end def test_should_not_match_if_from_context_is_different refute @matcher.matches?(:parked, from: :idling) end def test_should_have_a_description assert_equal 'same', @matcher.description end end state_machines-0.100.4/test/unit/matcher/matcher_by_default_test.rb000066400000000000000000000005151507333401300254310ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MatcherByDefaultTest < StateMachinesTest def setup @matcher = StateMachines::Matcher.new end def test_should_have_no_values assert_empty @matcher.values end def test_should_filter_all_values assert_empty @matcher.filter(%i[parked idling]) end end state_machines-0.100.4/test/unit/matcher/matcher_with_multiple_values_test.rb000066400000000000000000000006141507333401300275600ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MatcherWithMultipleValuesTest < StateMachinesTest def setup @matcher = StateMachines::Matcher.new(%i[parked idling]) end def test_should_have_values assert_equal %i[parked idling], @matcher.values end def test_should_filter_unknown_values assert_equal [:parked], @matcher.filter(%i[parked first_gear]) end end state_machines-0.100.4/test/unit/matcher/matcher_with_value_test.rb000066400000000000000000000005361507333401300254650ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MatcherWithValueTest < StateMachinesTest def setup @matcher = StateMachines::Matcher.new(nil) end def test_should_have_values assert_equal [nil], @matcher.values end def test_should_filter_unknown_values assert_equal [nil], @matcher.filter([nil, :parked]) end end state_machines-0.100.4/test/unit/matcher/whitelist_matcher_test.rb000066400000000000000000000014401507333401300253250ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class WhitelistMatcherTest < StateMachinesTest def setup @matcher = StateMachines::WhitelistMatcher.new(%i[parked idling]) end def test_should_have_values assert_equal %i[parked idling], @matcher.values end def test_should_filter_unknown_values assert_equal %i[parked idling], @matcher.filter(%i[parked idling first_gear]) end def test_should_match_known_values assert @matcher.matches?(:parked) end def test_should_not_match_unknown_values refute @matcher.matches?(:first_gear) end def test_should_have_a_description assert_equal '[:parked, :idling]', @matcher.description matcher = StateMachines::WhitelistMatcher.new([:parked]) assert_equal ':parked', matcher.description end end state_machines-0.100.4/test/unit/matcher_helpers/000077500000000000000000000000001507333401300217455ustar00rootroot00000000000000state_machines-0.100.4/test/unit/matcher_helpers/matcher_helpers_all_test.rb000066400000000000000000000004441507333401300273300ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MatcherHelpersAllTest < StateMachinesTest include StateMachines::MatcherHelpers def setup @matcher = all end def test_should_build_an_all_matcher assert_equal StateMachines::AllMatcher.instance, @matcher end end state_machines-0.100.4/test/unit/matcher_helpers/matcher_helpers_any_test.rb000066400000000000000000000004441507333401300273470ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MatcherHelpersAnyTest < StateMachinesTest include StateMachines::MatcherHelpers def setup @matcher = any end def test_should_build_an_all_matcher assert_equal StateMachines::AllMatcher.instance, @matcher end end state_machines-0.100.4/test/unit/matcher_helpers/matcher_helpers_same_test.rb000066400000000000000000000004571507333401300275110ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class MatcherHelpersSameTest < StateMachinesTest include StateMachines::MatcherHelpers def setup @matcher = same end def test_should_build_a_loopback_matcher assert_equal StateMachines::LoopbackMatcher.instance, @matcher end end state_machines-0.100.4/test/unit/node_collection/000077500000000000000000000000001507333401300217405ustar00rootroot00000000000000state_machines-0.100.4/test/unit/node_collection/node_collection_after_being_copied_test.rb000066400000000000000000000024161507333401300323370ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/node' class NodeCollectionAfterBeingCopiedTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine) @collection << @parked = Node.new(:parked) @contexts_run = contexts_run = [] @collection.context([:parked]) { contexts_run << :parked } @contexts_run.clear @copied_collection = @collection.dup @copied_collection << @idling = Node.new(:idling) @copied_collection.context([:first_gear]) { contexts_run << :first_gear } end def test_should_not_modify_the_original_list assert_equal 1, @collection.length assert_equal 2, @copied_collection.length end def test_should_not_modify_the_indices assert_nil @collection[:idling] assert_equal @idling, @copied_collection[:idling] end def test_should_copy_each_node refute_same @parked, @copied_collection[:parked] end def test_should_not_run_contexts assert_empty @contexts_run end def test_should_not_modify_contexts @collection << Node.new(:first_gear) assert_empty @contexts_run end def test_should_copy_contexts @copied_collection << Node.new(:parked) refute_empty @contexts_run end end state_machines-0.100.4/test/unit/node_collection/node_collection_after_update_test.rb000066400000000000000000000016521507333401300312130ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/node' class NodeCollectionAfterUpdateTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine, index: %i[name value]) @parked = Node.new(:parked, 1) @idling = Node.new(:idling, 2) @collection << @parked << @idling @parked.name = :parking @parked.value = 0 @collection.update(@parked) end def test_should_not_change_the_index assert_equal @parked, @collection.at(0) end def test_should_not_duplicate_in_the_collection assert_equal 2, @collection.length end def test_should_add_each_indexed_key assert_equal @parked, @collection[:parking] assert_equal @parked, @collection[0, :value] end def test_should_remove_each_old_indexed_key assert_nil @collection[:parked] assert_nil @collection[1, :value] end end state_machines-0.100.4/test/unit/node_collection/node_collection_by_default_test.rb000066400000000000000000000010701507333401300306600ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/node' class NodeCollectionByDefaultTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(@machine) end def test_should_not_have_any_nodes assert_equal 0, @collection.length end def test_should_have_a_machine assert_equal @machine, @collection.machine end def test_should_index_by_name @collection << object = Node.new(:parked) assert_equal object, @collection[:parked] end end state_machines-0.100.4/test/unit/node_collection/node_collection_test.rb000066400000000000000000000016641507333401300264730ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class NodeCollectionTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(@machine) end def test_should_raise_exception_if_invalid_option_specified exception = assert_raises(ArgumentError) { StateMachines::NodeCollection.new(@machine, invalid: true) } assert_equal 'Unknown key: :invalid. Valid keys are: :index', exception.message end def test_should_raise_exception_on_lookup_if_invalid_index_specified exception = assert_raises(ArgumentError) { @collection[:something, :invalid] } assert_equal 'Invalid index: :invalid', exception.message end def test_should_raise_exception_on_fetch_if_invalid_index_specified exception = assert_raises(ArgumentError) { @collection.fetch(:something, :invalid) } assert_equal 'Invalid index: :invalid', exception.message end end state_machines-0.100.4/test/unit/node_collection/node_collection_with_indices_test.rb000066400000000000000000000025331507333401300312200ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/node' class NodeCollectionWithIndicesTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine, index: %i[name value]) @object = Node.new(:parked, 1) @collection << @object end def test_should_use_first_index_by_default_on_key_retrieval assert_equal [:parked], @collection.keys end def test_should_allow_customizing_index_for_key_retrieval assert_equal [1], @collection.keys(:value) end def test_should_use_first_index_by_default_on_lookup assert_equal @object, @collection[:parked] assert_nil @collection[1] end def test_should_allow_customizing_index_on_lookup assert_equal @object, @collection[1, :value] assert_nil @collection[:parked, :value] end def test_should_use_first_index_by_default_on_fetch assert_equal @object, @collection.fetch(:parked) exception = assert_raises(IndexError) { @collection.fetch(1) } assert_equal '1 is an invalid name', exception.message end def test_should_allow_customizing_index_on_fetch assert_equal @object, @collection.fetch(1, :value) exception = assert_raises(IndexError) { @collection.fetch(:parked, :value) } assert_equal ':parked is an invalid value', exception.message end end state_machines-0.100.4/test/unit/node_collection/node_collection_with_matcher_contexts_test.rb000066400000000000000000000014671507333401300331610ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/node' class NodeCollectionWithMatcherContextsTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine) @collection << Node.new(:parked) end def test_should_always_run_all_matcher_context contexts_run = [] @collection.context([StateMachines::AllMatcher.instance]) { contexts_run << :all } assert_equal [:all], contexts_run end def test_should_only_run_blacklist_matcher_if_not_matched contexts_run = [] @collection.context([StateMachines::BlacklistMatcher.new([:parked])]) { contexts_run << :blacklist } assert_empty contexts_run @collection << Node.new(:idling) assert_equal [:blacklist], contexts_run end end state_machines-0.100.4/test/unit/node_collection/node_collection_with_nodes_test.rb000066400000000000000000000025231507333401300307110ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/node' class NodeCollectionWithNodesTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(@machine) @parked = Node.new(:parked, nil, @machine) @idling = Node.new(:idling, nil, @machine) @collection << @parked @collection << @idling end def test_should_be_able_to_enumerate order = [] @collection.each { |object| order << object } assert_equal [@parked, @idling], order end def test_should_be_able_to_concatenate_multiple_nodes @first_gear = Node.new(:first_gear, nil, @machine) @second_gear = Node.new(:second_gear, nil, @machine) @collection.concat([@first_gear, @second_gear]) order = [] @collection.each { |object| order << object } assert_equal [@parked, @idling, @first_gear, @second_gear], order end def test_should_be_able_to_access_by_index assert_equal @parked, @collection.at(0) assert_equal @idling, @collection.at(1) end def test_should_deep_copy_machine_changes new_machine = StateMachines::Machine.new(Class.new) @collection.machine = new_machine assert_equal new_machine, @collection.machine assert_equal new_machine, @parked.machine assert_equal new_machine, @idling.machine end end state_machines-0.100.4/test/unit/node_collection/node_collection_with_numeric_index_test.rb000066400000000000000000000011461507333401300324320ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/node' class NodeCollectionWithNumericIndexTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine, index: %i[name value]) @parked = Node.new(10, 1) @collection << @parked end def test_should_index_by_name assert_equal @parked, @collection[10] end def test_should_index_by_string_name assert_equal @parked, @collection['10'] end def test_should_index_by_symbol_name assert_equal @parked, @collection[:'10'] end end state_machines-0.100.4/test/unit/node_collection/node_collection_with_postdefined_contexts_test.rb000066400000000000000000000012271507333401300340340ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/node' class NodeCollectionWithPostdefinedContextsTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine) @collection << Node.new(:parked) end def test_should_run_context_if_matched contexts_run = [] @collection.context([:parked]) { contexts_run << :parked } assert_equal [:parked], contexts_run end def test_should_not_run_contexts_if_not_matched contexts_run = [] @collection.context([:idling]) { contexts_run << :idling } assert_empty contexts_run end end state_machines-0.100.4/test/unit/node_collection/node_collection_with_predefined_contexts_test.rb000066400000000000000000000013311507333401300336310ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/node' class NodeCollectionWithPredefinedContextsTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine) @contexts_run = contexts_run = [] @collection.context([:parked]) { contexts_run << :parked } @collection.context([:parked]) { contexts_run << :second_parked } end def test_should_run_contexts_in_the_order_defined @collection << Node.new(:parked) assert_equal %i[parked second_parked], @contexts_run end def test_should_not_run_contexts_if_not_matched @collection << Node.new(:idling) assert_empty @contexts_run end end state_machines-0.100.4/test/unit/node_collection/node_collection_with_string_index_test.rb000066400000000000000000000010301507333401300322660ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/node' class NodeCollectionWithStringIndexTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine, index: %i[name value]) @parked = Node.new(:parked, 1) @collection << @parked end def test_should_index_by_name assert_equal @parked, @collection[:parked] end def test_should_index_by_string_name assert_equal @parked, @collection['parked'] end end state_machines-0.100.4/test/unit/node_collection/node_collection_with_symbol_index_test.rb000066400000000000000000000010311507333401300322660ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'files/node' class NodeCollectionWithSymbolIndexTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine, index: %i[name value]) @parked = Node.new('parked', 1) @collection << @parked end def test_should_index_by_name assert_equal @parked, @collection['parked'] end def test_should_index_by_symbol_name assert_equal @parked, @collection[:parked] end end state_machines-0.100.4/test/unit/node_collection/node_collection_without_indices_test.rb000066400000000000000000000016641507333401300317540ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class NodeCollectionWithoutIndicesTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine, index: {}) end def test_should_allow_adding_node @collection << Object.new assert_equal 1, @collection.length end def test_should_not_allow_keys_retrieval exception = assert_raises(ArgumentError) { @collection.keys } assert_equal 'No indices configured', exception.message end def test_should_not_allow_lookup @collection << Object.new exception = assert_raises(ArgumentError) { @collection[0] } assert_equal 'No indices configured', exception.message end def test_should_not_allow_fetching @collection << Object.new exception = assert_raises(ArgumentError) { @collection.fetch(0) } assert_equal 'No indices configured', exception.message end end state_machines-0.100.4/test/unit/path/000077500000000000000000000000001507333401300175345ustar00rootroot00000000000000state_machines-0.100.4/test/unit/path/path_by_default_test.rb000066400000000000000000000021001507333401300242430ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathByDefaultTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @object = @klass.new @path = StateMachines::Path.new(@object, @machine) end def test_should_have_an_object assert_equal @object, @path.object end def test_should_have_a_machine assert_equal @machine, @path.machine end def test_should_not_have_walked_anywhere assert_empty @path end def test_should_not_have_a_from_name assert_nil @path.from_name end def test_should_have_no_from_states assert_empty @path.from_states end def test_should_not_have_a_to_name assert_nil @path.to_name end def test_should_have_no_to_states assert_empty @path.to_states end def test_should_have_no_events assert_empty @path.events end def test_should_not_be_able_to_walk_anywhere walked = false @path.walk { walked = true } refute walked end def test_should_not_be_complete refute_predicate @path, :complete? end end state_machines-0.100.4/test/unit/path/path_test.rb000066400000000000000000000007361507333401300220620ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @object = @klass.new end def test_should_raise_exception_if_invalid_option_specified exception = assert_raises(ArgumentError) { StateMachines::Path.new(@object, @machine, invalid: true) } assert_equal 'Unknown key: :invalid. Valid keys are: :target, :guard', exception.message end end state_machines-0.100.4/test/unit/path/path_with_available_transitions_after_reaching_target_test.rb000066400000000000000000000022721507333401300341360ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathWithAvailableTransitionsAfterReachingTargetTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite do transition parked: :idling end @machine.event :shift_up do transition parked: :first_gear end @machine.event :park do transition %i[idling first_gear] => :parked end @object = @klass.new @object.state = 'parked' @path = StateMachines::Path.new(@object, @machine, target: :parked) @path.push( @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling), @park_transition = StateMachines::Transition.new(@object, @machine, :park, :idling, :parked) ) end def test_should_be_complete assert @path.complete? end def test_should_be_able_to_walk paths = [] @path.walk { |path| paths << path } assert_equal [ [@ignite_transition, @park_transition, StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :first_gear)] ], paths end end state_machines-0.100.4/test/unit/path/path_with_available_transitions_test.rb000066400000000000000000000027541507333401300275540ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathWithAvailableTransitionsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling, :first_gear @machine.event :ignite @machine.event :shift_up do transition idling: :first_gear end @machine.event :park do transition idling: :parked end @object = @klass.new @object.state = 'parked' @path = StateMachines::Path.new(@object, @machine) @path.push( @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ) end def test_should_not_be_complete refute_predicate @path, :complete? end def test_should_walk_each_available_transition paths = [] @path.walk { |path| paths << path } assert_equal [ [@ignite_transition, StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear)], [@ignite_transition, StateMachines::Transition.new(@object, @machine, :park, :idling, :parked)] ], paths end def test_should_yield_path_instances_when_walking @path.walk do |path| assert_instance_of StateMachines::Path, path end end def test_should_not_modify_current_path_after_walking @path.walk {} assert_equal [@ignite_transition], @path end def test_should_not_modify_object_after_walking @path.walk {} assert_equal 'parked', @object.state end end state_machines-0.100.4/test/unit/path/path_with_deep_target_reached_test.rb000066400000000000000000000027351507333401300271340ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathWithDeepTargetReachedTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite do transition parked: :idling end @machine.event :shift_up do transition parked: :first_gear end @machine.event :park do transition %i[idling first_gear] => :parked end @object = @klass.new @object.state = 'parked' @path = StateMachines::Path.new(@object, @machine, target: :parked) @path.push( @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling), @park_transition = StateMachines::Transition.new(@object, @machine, :park, :idling, :parked), @shift_up_transition = StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :first_gear), @park_transition_2 = StateMachines::Transition.new(@object, @machine, :park, :first_gear, :parked) ) end def test_should_be_complete assert @path.complete? end def test_should_not_be_able_to_walk walked = false @path.walk { walked = true } refute walked end def test_should_not_be_able_to_walk_with_available_transitions @machine.event :park do transition parked: same end walked = false @path.walk { walked = true } refute walked end end state_machines-0.100.4/test/unit/path/path_with_deep_target_test.rb000066400000000000000000000024741507333401300254610ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathWithDeepTargetTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite do transition parked: :idling end @machine.event :shift_up do transition parked: :first_gear end @machine.event :park do transition %i[idling first_gear] => :parked end @object = @klass.new @object.state = 'parked' @path = StateMachines::Path.new(@object, @machine, target: :parked) @path.push( @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling), @park_transition = StateMachines::Transition.new(@object, @machine, :park, :idling, :parked), @shift_up_transition = StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :first_gear) ) end def test_should_not_be_complete refute_predicate @path, :complete? end def test_should_be_able_to_walk paths = [] @path.walk { |path| paths << path } assert_equal [ [@ignite_transition, @park_transition, @shift_up_transition, StateMachines::Transition.new(@object, @machine, :park, :first_gear, :parked)] ], paths end end state_machines-0.100.4/test/unit/path/path_with_duplicates_test.rb000066400000000000000000000021151507333401300253230ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathWithDuplicatesTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :park, :ignite @object = @klass.new @object.state = 'parked' @path = StateMachines::Path.new(@object, @machine) @path.push( @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling), @park_transition = StateMachines::Transition.new(@object, @machine, :park, :idling, :parked), @ignite_again_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ) end def test_should_not_include_duplicates_in_from_states assert_equal %i[parked idling], @path.from_states end def test_should_not_include_duplicates_in_to_states assert_equal %i[idling parked], @path.to_states end def test_should_not_include_duplicates_in_events assert_equal %i[ignite park], @path.events end end state_machines-0.100.4/test/unit/path/path_with_encountered_transitions_test.rb000066400000000000000000000016721507333401300301450ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathWithEncounteredTransitionsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling, :first_gear @machine.event :ignite do transition parked: :idling end @machine.event :park do transition idling: :parked end @object = @klass.new @object.state = 'parked' @path = StateMachines::Path.new(@object, @machine) @path.push( @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling), @park_transition = StateMachines::Transition.new(@object, @machine, :park, :idling, :parked) ) end def test_should_be_complete assert @path.complete? end def test_should_not_be_able_to_walk walked = false @path.walk { walked = true } refute walked end end state_machines-0.100.4/test/unit/path/path_with_guarded_transitions_test.rb000066400000000000000000000023261507333401300272420ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathWithGuardedTransitionsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @machine.event :shift_up do transition idling: :first_gear, if: -> { false } end @object = @klass.new @object.state = 'parked' end def test_should_not_walk_transitions_if_guard_enabled path = StateMachines::Path.new(@object, @machine) path.push( StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ) paths = [] path.walk { |next_path| paths << next_path } assert_empty paths end def test_should_not_walk_transitions_if_guard_disabled path = StateMachines::Path.new(@object, @machine, guard: false) path.push( ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ) paths = [] path.walk { |next_path| paths << next_path } assert_equal [ [ignite_transition, StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear)] ], paths end end state_machines-0.100.4/test/unit/path/path_with_reached_target_test.rb000066400000000000000000000016651507333401300261400ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathWithReachedTargetTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite do transition parked: :idling end @machine.event :park do transition idling: :parked end @object = @klass.new @object.state = 'parked' @path = StateMachines::Path.new(@object, @machine, target: :parked) @path.push( @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling), @park_transition = StateMachines::Transition.new(@object, @machine, :park, :idling, :parked) ) end def test_should_be_complete assert @path.complete? end def test_should_not_be_able_to_walk walked = false @path.walk { walked = true } refute walked end end state_machines-0.100.4/test/unit/path/path_with_transitions_test.rb000066400000000000000000000026201507333401300255440ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathWithTransitionsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling, :first_gear @machine.event :ignite, :shift_up @object = @klass.new @object.state = 'parked' @path = StateMachines::Path.new(@object, @machine) @path.push( @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling), @shift_up_transition = StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear) ) end def test_should_enumerate_transitions assert_equal [@ignite_transition, @shift_up_transition], @path end def test_should_have_a_from_name assert_equal :parked, @path.from_name end def test_should_have_from_states assert_equal %i[parked idling], @path.from_states end def test_should_have_a_to_name assert_equal :first_gear, @path.to_name end def test_should_have_to_states assert_equal %i[idling first_gear], @path.to_states end def test_should_have_events assert_equal %i[ignite shift_up], @path.events end def test_should_not_be_able_to_walk_anywhere walked = false @path.walk { walked = true } refute walked end def test_should_be_complete assert @path.complete? end end state_machines-0.100.4/test/unit/path/path_with_unreached_target_test.rb000066400000000000000000000014211507333401300264710ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathWithUnreachedTargetTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite do transition parked: :idling end @object = @klass.new @object.state = 'parked' @path = StateMachines::Path.new(@object, @machine, target: :parked) @path.push( @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ) end def test_should_not_be_complete refute_predicate @path, :complete? end def test_should_not_be_able_to_walk walked = false @path.walk { walked = true } refute walked end end state_machines-0.100.4/test/unit/path/path_without_transitions_test.rb000066400000000000000000000011571507333401300263000ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathWithoutTransitionsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @path = StateMachines::Path.new(@object, @machine) @path.push( @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ) end def test_should_not_be_able_to_walk_anywhere walked = false @path.walk { walked = true } refute walked end end state_machines-0.100.4/test/unit/path_collection/000077500000000000000000000000001507333401300217475ustar00rootroot00000000000000state_machines-0.100.4/test/unit/path_collection/path_collection_by_default_test.rb000066400000000000000000000017051507333401300307030ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathCollectionByDefaultTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked @object = @klass.new @object.state = 'parked' @paths = StateMachines::PathCollection.new(@object, @machine) end def test_should_have_an_object assert_equal @object, @paths.object end def test_should_have_a_machine assert_equal @machine, @paths.machine end def test_should_have_a_from_name assert_equal :parked, @paths.from_name end def test_should_not_have_a_to_name assert_nil @paths.to_name end def test_should_have_no_from_states assert_empty @paths.from_states end def test_should_have_no_to_states assert_empty @paths.to_states end def test_should_have_no_events assert_empty @paths.events end def test_should_have_no_paths assert_empty @paths end end state_machines-0.100.4/test/unit/path_collection/path_collection_test.rb000066400000000000000000000017641507333401300265120ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathCollectionTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @object = @klass.new end def test_should_raise_exception_if_invalid_option_specified exception = assert_raises(ArgumentError) { StateMachines::PathCollection.new(@object, @machine, invalid: true) } assert_equal 'Unknown key: :invalid. Valid keys are: :from, :to, :deep, :guard', exception.message end def test_should_raise_exception_if_invalid_from_state_specified exception = assert_raises(IndexError) { StateMachines::PathCollection.new(@object, @machine, from: :invalid) } assert_equal ':invalid is an invalid name', exception.message end def test_should_raise_exception_if_invalid_to_state_specified exception = assert_raises(IndexError) { StateMachines::PathCollection.new(@object, @machine, to: :invalid) } assert_equal ':invalid is an invalid name', exception.message end end state_machines-0.100.4/test/unit/path_collection/path_collection_with_deep_paths_test.rb000066400000000000000000000027551507333401300317420ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathCollectionWithDeepPathsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite do transition parked: :idling end @machine.event :shift_up do transition parked: :idling, idling: :first_gear end @machine.event :shift_down do transition first_gear: :idling end @object = @klass.new @object.state = 'parked' @paths = StateMachines::PathCollection.new(@object, @machine, to: :idling, deep: true) end def test_should_allow_target_to_be_reached_more_than_once_per_path assert_equal [ [ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ], [ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling), StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear), StateMachines::Transition.new(@object, @machine, :shift_down, :first_gear, :idling) ], [ StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :idling) ], [ StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :idling), StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear), StateMachines::Transition.new(@object, @machine, :shift_down, :first_gear, :idling) ] ], @paths end end state_machines-0.100.4/test/unit/path_collection/path_collection_with_duplicate_nodes_test.rb000066400000000000000000000015731507333401300327650ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathCollectionWithDuplicateNodesTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :shift_up do transition parked: :idling, idling: :first_gear end @machine.event :park do transition first_gear: :idling end @object = @klass.new @object.state = 'parked' @paths = StateMachines::PathCollection.new(@object, @machine) end def test_should_not_include_duplicates_in_from_states assert_equal %i[parked idling first_gear], @paths.from_states end def test_should_not_include_duplicates_in_to_states assert_equal %i[idling first_gear], @paths.to_states end def test_should_not_include_duplicates_in_events assert_equal %i[shift_up park], @paths.events end end state_machines-0.100.4/test/unit/path_collection/path_collection_with_from_state_test.rb000066400000000000000000000013251507333401300317610ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathCollectionWithFromStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling, :first_gear @machine.event :park do transition idling: :parked end @object = @klass.new @object.state = 'parked' @paths = StateMachines::PathCollection.new(@object, @machine, from: :idling) end def test_should_generate_paths_from_custom_from_state assert_equal [[ StateMachines::Transition.new(@object, @machine, :park, :idling, :parked) ]], @paths end def test_should_have_a_from_name assert_equal :idling, @paths.from_name end end state_machines-0.100.4/test/unit/path_collection/path_collection_with_paths_test.rb000066400000000000000000000022761507333401300307430ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathCollectionWithPathsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling, :first_gear @machine.event :ignite do transition parked: :idling end @machine.event :shift_up do transition idling: :first_gear end @object = @klass.new @object.state = 'parked' @paths = StateMachines::PathCollection.new(@object, @machine) end def test_should_enumerate_paths assert_equal [[ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling), StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear) ]], @paths end def test_should_have_a_from_name assert_equal :parked, @paths.from_name end def test_should_not_have_a_to_name assert_nil @paths.to_name end def test_should_have_from_states assert_equal %i[parked idling], @paths.from_states end def test_should_have_to_states assert_equal %i[idling first_gear], @paths.to_states end def test_should_have_no_events assert_equal %i[ignite shift_up], @paths.events end end state_machines-0.100.4/test/unit/path_collection/path_collection_with_to_state_test.rb000066400000000000000000000015651507333401300314460ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathCollectionWithToStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite do transition parked: :idling end @machine.event :shift_up do transition parked: :idling, idling: :first_gear end @machine.event :shift_down do transition first_gear: :idling end @object = @klass.new @object.state = 'parked' @paths = StateMachines::PathCollection.new(@object, @machine, to: :idling) end def test_should_stop_paths_once_target_state_reached assert_equal [ [StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)], [StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :idling)] ], @paths end end state_machines-0.100.4/test/unit/path_collection/path_with_guarded_paths_test.rb000066400000000000000000000014131507333401300302130ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PathWithGuardedPathsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling, :first_gear @machine.event :ignite do transition parked: :idling, if: -> { false } end @object = @klass.new @object.state = 'parked' end def test_should_not_enumerate_paths_if_guard_enabled assert_empty StateMachines::PathCollection.new(@object, @machine) end def test_should_enumerate_paths_if_guard_disabled paths = StateMachines::PathCollection.new(@object, @machine, guard: false) assert_equal [[ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]], paths end end state_machines-0.100.4/test/unit/state/000077500000000000000000000000001507333401300177205ustar00rootroot00000000000000state_machines-0.100.4/test/unit/state/state_after_being_copied_test.rb000066400000000000000000000010411507333401300262700ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateAfterBeingCopiedTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @machine.states << @state = StateMachines::State.new(@machine, :parked) @copied_state = @state.dup end def test_should_not_have_the_context state_context = nil @state.context { state_context = self } copied_state_context = nil @copied_state.context { copied_state_context = self } refute_same state_context, copied_state_context end end state_machines-0.100.4/test/unit/state/state_by_default_test.rb000066400000000000000000000016621507333401300246270ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateByDefaultTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @machine.states << @state = StateMachines::State.new(@machine, :parked) end def test_should_have_a_machine assert_equal @machine, @state.machine end def test_should_have_a_name assert_equal :parked, @state.name end def test_should_have_a_qualified_name assert_equal :parked, @state.qualified_name end def test_should_have_a_human_name assert_equal 'parked', @state.human_name end def test_should_use_stringify_the_name_as_the_value assert_equal 'parked', @state.value end def test_should_not_be_initial refute @state.initial end def test_should_not_have_a_matcher assert_nil @state.matcher end def test_should_not_have_any_methods expected = {} assert_equal expected, @state.context_methods end end state_machines-0.100.4/test/unit/state/state_final_test.rb000066400000000000000000000012251507333401300235750ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateFinalTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @machine.states << @state = StateMachines::State.new(@machine, :parked) end def test_should_be_final_without_input_transitions assert_predicate @state, :final? end def test_should_be_final_with_input_transitions @machine.event :park do transition idling: :parked end assert_predicate @state, :final? end def test_should_be_final_with_loopback @machine.event :ignite do transition parked: same end assert_predicate @state, :final? end end state_machines-0.100.4/test/unit/state/state_initial_test.rb000066400000000000000000000005541507333401300241410ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateInitialTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @machine.states << @state = StateMachines::State.new(@machine, :parked, initial: true) end def test_should_be_initial assert @state.initial assert_predicate @state, :initial? end end state_machines-0.100.4/test/unit/state/state_not_final_test.rb000066400000000000000000000014441507333401300244600ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateNotFinalTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @machine.states << @state = StateMachines::State.new(@machine, :parked) end def test_should_not_be_final_with_outgoing_whitelist_transitions @machine.event :ignite do transition parked: :idling end refute_predicate @state, :final? end def test_should_not_be_final_with_outgoing_all_transitions @machine.event :ignite do transition all => :idling end refute_predicate @state, :final? end def test_should_not_be_final_with_outgoing_blacklist_transitions @machine.event :ignite do transition all - :first_gear => :idling end refute_predicate @state, :final? end end state_machines-0.100.4/test/unit/state/state_not_initial_test.rb000066400000000000000000000005641507333401300250220ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateNotInitialTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @machine.states << @state = StateMachines::State.new(@machine, :parked, initial: false) end def test_should_not_be_initial refute @state.initial refute_predicate @state, :initial? end end state_machines-0.100.4/test/unit/state/state_test.rb000066400000000000000000000024731507333401300224320ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @machine.states << @state = StateMachines::State.new(@machine, :parked) end def test_should_raise_exception_if_invalid_option_specified exception = assert_raises(ArgumentError) { StateMachines::State.new(@machine, :parked, invalid: true) } assert_equal 'Unknown key: :invalid. Valid keys are: :initial, :value, :cache, :if, :human_name', exception.message end def test_should_allow_changing_machine new_machine = StateMachines::Machine.new(Class.new) @state.machine = new_machine assert_equal new_machine, @state.machine end def test_should_allow_changing_value @state.value = 1 assert_equal 1, @state.value end def test_should_allow_changing_initial @state.initial = true assert @state.initial end def test_should_allow_changing_matcher matcher = -> {} @state.matcher = matcher assert_equal matcher, @state.matcher end def test_should_allow_changing_human_name @state.human_name = 'stopped' assert_equal 'stopped', @state.human_name end def test_should_use_pretty_inspect assert_equal '#', @state.inspect end end state_machines-0.100.4/test/unit/state/state_with_cached_lambda_value_test.rb000066400000000000000000000014411507333401300274420ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithCachedLambdaValueTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @dynamic_value = -> { 'value' } @machine.states << @state = StateMachines::State.new(@machine, :parked, value: @dynamic_value, cache: true) end def test_should_be_caching assert @state.cache end def test_should_evaluate_value assert_equal 'value', @state.value end def test_should_only_evaluate_value_once value = @state.value assert_same value, @state.value end def test_should_update_value_index_for_state_collection @state.value assert_equal @state, @machine.states['value', :value] assert_nil @machine.states[@dynamic_value, :value] end end state_machines-0.100.4/test/unit/state/state_with_conflicting_helpers_after_definition_test.rb000066400000000000000000000015431507333401300331540ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithConflictingHelpersAfterDefinitionTest < StateMachinesTest class SuperKlass def parked? false end end def setup @original_stderr = $stderr $stderr = StringIO.new @klass = Class.new(SuperKlass) @machine = StateMachines::Machine.new(@klass) @output = capture_io { @machine.state :parked }.join @object = @klass.new end def teardown $stderr = @original_stderr end def test_should_not_override_state_predicate refute_predicate @object, :parked? end def test_should_still_allow_super_chaining @klass.class_eval do def parked? super end end refute_predicate @object, :parked? end def test_should_output_warning assert_match(/Instance method "parked\?" is already defined/, @output) end end state_machines-0.100.4/test/unit/state/state_with_conflicting_helpers_before_definition_test.rb000066400000000000000000000014461507333401300333170ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithConflictingHelpersBeforeDefinitionTest < StateMachinesTest def setup @original_stderr = $stderr $stderr = StringIO.new @superclass = Class.new do def parked? 0 end end @klass = Class.new(@superclass) @machine = StateMachines::Machine.new(@klass) @machine.state :parked @object = @klass.new end def teardown $stderr = @original_stderr end def test_should_not_override_state_predicate assert_equal 0, @object.parked? end def test_should_output_warning assert_match( /Instance method "parked\?" is already defined in #, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true./, $stderr.string ) end end state_machines-0.100.4/test/unit/state/state_with_conflicting_helpers_test.rb000066400000000000000000000012461507333401300275630ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../../test_helper' class StateWithConflictingHelpersTest < StateMachinesTest class SuperKlass def parked? true end end def setup @klass = Class.new(SuperKlass) @machine = StateMachines::Machine.new(@klass) @object = @klass.new @original_stderr = $stderr $stderr = StringIO.new begin @machine.state :parked @output = $stderr.string ensure $stderr = @original_stderr end ensure $stderr = @original_stderr end def teardown; end def test_should_output_warning assert_match(/Instance method "parked\?" is already defined/, @output) end end state_machines-0.100.4/test/unit/state/state_with_conflicting_machine_name_test.rb000066400000000000000000000012321507333401300305200ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'stringio' class StateWithConflictingMachineNameTest < StateMachinesTest def setup @original_stderr = $stderr $stderr = StringIO.new @klass = Class.new @state_machine = StateMachines::Machine.new(@klass, :state) end def teardown $stderr = @original_stderr end def test_should_output_warning_if_name_conflicts StateMachines::State.new(@state_machine, :state) assert_match( /Instance method "state\?" is already defined in #, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true./, $stderr.string ) end end state_machines-0.100.4/test/unit/state/state_with_conflicting_machine_test.rb000066400000000000000000000024541507333401300275270ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'stringio' class StateWithConflictingMachineTest < StateMachinesTest def setup @original_stderr = $stderr $stderr = StringIO.new @klass = Class.new @state_machine = StateMachines::Machine.new(@klass, :state) @state_machine.states << @state = StateMachines::State.new(@state_machine, :parked) end def teardown $stderr = @original_stderr end def test_should_output_warning_if_using_different_attribute @status_machine = StateMachines::Machine.new(@klass, :status) @status_machine.states << @state = StateMachines::State.new(@status_machine, :parked) assert_equal "State :parked for :status is already defined in :state\n", $stderr.string end def test_should_not_output_warning_if_using_same_attribute @status_machine = StateMachines::Machine.new(@klass, :status, attribute: :state) @status_machine.states << @state = StateMachines::State.new(@status_machine, :parked) assert_equal '', $stderr.string end def test_should_not_output_warning_if_using_different_namespace @status_machine = StateMachines::Machine.new(@klass, :status, namespace: 'alarm') @status_machine.states << @state = StateMachines::State.new(@status_machine, :parked) assert_equal '', $stderr.string end end state_machines-0.100.4/test/unit/state/state_with_context_test.rb000066400000000000000000000032461507333401300252300ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithContextTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @ancestors = @klass.ancestors @machine.states << @state = StateMachines::State.new(@machine, :idling) context = nil speed_method = nil rpm_method = nil @result = @state.context do context = self def speed 0 end speed_method = instance_method(:speed) def rpm 1000 end rpm_method = instance_method(:rpm) end @context = context @speed_method = speed_method @rpm_method = rpm_method end def test_should_return_true assert @result end def test_should_include_new_module_in_owner_class refute_equal @ancestors, @klass.ancestors assert_equal [@context], @klass.ancestors - @ancestors end def test_should_define_each_context_method_in_owner_class %w[speed rpm].each { |method| assert @klass.method_defined?(method) } end def test_should_define_aliased_context_method_in_owner_class %w[speed rpm].each { |method| assert @klass.method_defined?("__state_idling_#{method}_#{@context.object_id}__") } end def test_should_not_use_context_methods_as_owner_class_methods refute_equal @speed_method, @state.context_methods[:speed] refute_equal @rpm_method, @state.context_methods[:rpm] end def test_should_use_context_methods_as_aliased_owner_class_methods assert_equal @speed_method, @state.context_methods[:"__state_idling_speed_#{@context.object_id}__"] assert_equal @rpm_method, @state.context_methods[:"__state_idling_rpm_#{@context.object_id}__"] end end state_machines-0.100.4/test/unit/state/state_with_dynamic_human_name_test.rb000066400000000000000000000013771507333401300273630ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithDynamicHumanNameTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.states << @state = StateMachines::State.new(@machine, :parked, human_name: ->(_state, object) { ['stopped', object] }) end def test_should_use_custom_human_name human_name, klass = @state.human_name assert_equal 'stopped', human_name assert_equal @klass, klass end def test_should_allow_custom_class_to_be_passed_through human_name, klass = @state.human_name(1) assert_equal 'stopped', human_name assert_equal 1, klass end def test_should_not_cache_value refute_same @state.human_name, @state.human_name end end state_machines-0.100.4/test/unit/state/state_with_existing_context_method_test.rb000066400000000000000000000011021507333401300304670ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithExistingContextMethodTest < StateMachinesTest def setup @klass = Class.new do def speed 60 end end @original_speed_method = @klass.instance_method(:speed) @machine = StateMachines::Machine.new(@klass) @machine.states << @state = StateMachines::State.new(@machine, :idling) @state.context do def speed 0 end end end def test_should_not_override_method assert_equal @original_speed_method, @klass.instance_method(:speed) end end state_machines-0.100.4/test/unit/state/state_with_human_name_test.rb000066400000000000000000000006061507333401300256510ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithHumanNameTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.states << @state = StateMachines::State.new(@machine, :parked, human_name: 'stopped') end def test_should_use_custom_human_name assert_equal 'stopped', @state.human_name end end state_machines-0.100.4/test/unit/state/state_with_integer_value_test.rb000066400000000000000000000015041507333401300263700ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithIntegerValueTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.states << @state = StateMachines::State.new(@machine, :parked, value: 1) end def test_should_use_custom_value assert_equal 1, @state.value end def test_should_include_value_in_description assert_equal 'parked (1)', @state.description end def test_should_allow_human_name_in_description @state.human_name = 'Parked' assert_equal 'Parked (1)', @state.description(human_name: true) end def test_should_match_integer_value assert @state.matches?(1) refute @state.matches?(2) end def test_should_define_predicate object = @klass.new assert_respond_to object, :parked? end end state_machines-0.100.4/test/unit/state/state_with_invalid_method_call_test.rb000066400000000000000000000010261507333401300275170ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithInvalidMethodCallTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @ancestors = @klass.ancestors @machine.states << @state = StateMachines::State.new(@machine, :idling) @state.context do def speed 0 end end @object = @klass.new end def test_should_call_method_missing_arg assert_equal 1, @state.call(@object, :invalid, method_missing: -> { 1 }) end end state_machines-0.100.4/test/unit/state/state_with_lambda_value_test.rb000066400000000000000000000017041507333401300261550ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithLambdaValueTest < StateMachinesTest def setup @klass = Class.new @args = nil @machine = StateMachines::Machine.new(@klass) @value = lambda { |*args| @args = args :parked } @machine.states << @state = StateMachines::State.new(@machine, :parked, value: @value) end def test_should_use_evaluated_value_by_default assert_equal :parked, @state.value end def test_should_allow_access_to_original_value assert_equal @value, @state.value(false) end def test_should_include_masked_value_in_description assert_equal 'parked (*)', @state.description end def test_should_not_pass_in_any_arguments @state.value assert_empty @args end def test_should_define_predicate object = @klass.new assert_respond_to object, :parked? end def test_should_match_evaluated_value assert @state.matches?(:parked) end end state_machines-0.100.4/test/unit/state/state_with_matcher_test.rb000066400000000000000000000007401507333401300251630ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithMatcherTest < StateMachinesTest def setup @klass = Class.new @args = nil @machine = StateMachines::Machine.new(@klass) @machine.states << @state = StateMachines::State.new(@machine, :parked, if: ->(value) { value == 1 }) end def test_should_not_match_actual_value refute @state.matches?('parked') end def test_should_match_evaluated_block assert @state.matches?(1) end end state_machines-0.100.4/test/unit/state/state_with_multiple_contexts_test.rb000066400000000000000000000032121507333401300273170ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithMultipleContextsTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @ancestors = @klass.ancestors @machine.states << @state = StateMachines::State.new(@machine, :idling) context = nil speed_method = nil @state.context do context = self def speed 0 end speed_method = instance_method(:speed) end @context = context @speed_method = speed_method rpm_method = nil @state.context do def rpm 1000 end rpm_method = instance_method(:rpm) end @rpm_method = rpm_method end def test_should_include_new_module_in_owner_class refute_equal @ancestors, @klass.ancestors assert_equal [@context], @klass.ancestors - @ancestors end def test_should_define_each_context_method_in_owner_class %w[speed rpm].each { |method| assert @klass.method_defined?(method) } end def test_should_define_aliased_context_method_in_owner_class %w[speed rpm].each { |method| assert @klass.method_defined?("__state_idling_#{method}_#{@context.object_id}__") } end def test_should_not_use_context_methods_as_owner_class_methods refute_equal @speed_method, @state.context_methods[:speed] refute_equal @rpm_method, @state.context_methods[:rpm] end def test_should_use_context_methods_as_aliased_owner_class_methods assert_equal @speed_method, @state.context_methods[:"__state_idling_speed_#{@context.object_id}__"] assert_equal @rpm_method, @state.context_methods[:"__state_idling_rpm_#{@context.object_id}__"] end end state_machines-0.100.4/test/unit/state/state_with_name_test.rb000066400000000000000000000021021507333401300244520ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithNameTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.states << @state = StateMachines::State.new(@machine, :parked) end def test_should_have_a_name assert_equal :parked, @state.name end def test_should_have_a_qualified_name assert_equal :parked, @state.name end def test_should_have_a_human_name assert_equal 'parked', @state.human_name end def test_should_use_stringify_the_name_as_the_value assert_equal 'parked', @state.value end def test_should_match_stringified_name assert @state.matches?('parked') refute @state.matches?('idling') end def test_should_not_include_value_in_description assert_equal 'parked', @state.description end def test_should_allow_using_human_name_in_description @state.human_name = 'Parked' assert_equal 'Parked', @state.description(human_name: true) end def test_should_define_predicate assert_respond_to @klass.new, :parked? end end state_machines-0.100.4/test/unit/state/state_with_namespace_test.rb000066400000000000000000000011121507333401300254660ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithNamespaceTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, namespace: 'alarm') @machine.states << @state = StateMachines::State.new(@machine, :active) @object = @klass.new end def test_should_have_a_name assert_equal :active, @state.name end def test_should_have_a_qualified_name assert_equal :alarm_active, @state.qualified_name end def test_should_namespace_predicate assert_respond_to @object, :alarm_active? end end state_machines-0.100.4/test/unit/state/state_with_nil_value_test.rb000066400000000000000000000015461507333401300255230ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithNilValueTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.states << @state = StateMachines::State.new(@machine, :parked, value: nil) end def test_should_have_a_name assert_equal :parked, @state.name end def test_should_have_a_nil_value assert_nil @state.value end def test_should_match_nil_values assert @state.matches?(nil) end def test_should_have_a_description assert_equal 'parked (nil)', @state.description end def test_should_have_a_description_with_human_name @state.human_name = 'Parked' assert_equal 'Parked (nil)', @state.description(human_name: true) end def test_should_define_predicate object = @klass.new assert_respond_to object, :parked? end end state_machines-0.100.4/test/unit/state/state_with_redefined_context_method_test.rb000066400000000000000000000021321507333401300305660ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithRedefinedContextMethodTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.states << @state = StateMachines::State.new(@machine, 'on') old_context = nil old_speed_method = nil @state.context do old_context = self def speed 0 end old_speed_method = instance_method(:speed) end @old_context = old_context @old_speed_method = old_speed_method current_context = nil current_speed_method = nil @state.context do current_context = self def speed 'green' end current_speed_method = instance_method(:speed) end @current_context = current_context @current_speed_method = current_speed_method end def test_should_track_latest_defined_method assert_equal @current_speed_method, @state.context_methods[:"__state_on_speed_#{@current_context.object_id}__"] end def test_should_have_the_same_context assert_equal @current_context, @old_context end end state_machines-0.100.4/test/unit/state/state_with_symbolic_value_test.rb000066400000000000000000000015331507333401300265560ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithSymbolicValueTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.states << @state = StateMachines::State.new(@machine, :parked, value: :parked) end def test_should_use_custom_value assert_equal :parked, @state.value end def test_should_not_include_value_in_description assert_equal 'parked', @state.description end def test_should_allow_human_name_in_description @state.human_name = 'Parked' assert_equal 'Parked', @state.description(human_name: true) end def test_should_match_symbolic_value assert @state.matches?(:parked) refute @state.matches?('parked') end def test_should_define_predicate object = @klass.new assert_respond_to object, :parked? end end state_with_valid_inherited_method_call_for_current_state_test.rb000066400000000000000000000016631507333401300347630ustar00rootroot00000000000000state_machines-0.100.4/test/unit/state# frozen_string_literal: true require 'test_helper' class StateWithValidInheritedMethodCallForCurrentStateTest < StateMachinesTest def setup @superclass = Class.new do def speed(arg = nil) [arg] end end @klass = Class.new(@superclass) @machine = StateMachines::Machine.new(@klass, initial: :idling) @ancestors = @klass.ancestors @state = @machine.state(:idling) @state.context do def speed(arg = nil) [arg] + super(2) end end @object = @klass.new end def test_should_not_raise_an_exception @state.call(@object, :speed, method_missing: -> { raise }) end def test_should_be_able_to_call_super assert_equal [1, 2], @state.call(@object, :speed, 1) end def test_should_allow_redefinition @state.context do def speed(arg = nil) [arg] + super(3) end end assert_equal [1, 3], @state.call(@object, :speed, 1) end end state_machines-0.100.4/test/unit/state/state_with_valid_method_call_for_current_state_test.rb000066400000000000000000000016461507333401300330100ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithValidMethodCallForCurrentStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :idling) @ancestors = @klass.ancestors @state = @machine.state(:idling) @state.context do def speed(arg = nil) block_given? ? [arg, yield] : arg end end @object = @klass.new end def test_should_not_raise_an_exception @state.call(@object, :speed, method_missing: -> { raise }) end def test_should_pass_arguments_through assert_equal 1, @state.call(@object, :speed, 1, method_missing: -> {}) end def test_should_pass_blocks_through assert_equal [nil, 1], @state.call(@object, :speed) { 1 } end def test_should_pass_both_arguments_and_blocks_through assert_equal [1, 2], @state.call(@object, :speed, 1, method_missing: -> {}) { 2 } end end state_machines-0.100.4/test/unit/state/state_with_valid_method_call_for_different_state_test.rb000066400000000000000000000026021507333401300332650ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithValidMethodCallForDifferentStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @ancestors = @klass.ancestors @machine.states << @state = StateMachines::State.new(@machine, :idling) @state.context do def speed 0 end end @object = @klass.new end def test_should_call_method_missing_arg assert_equal 1, @state.call(@object, :speed, method_missing: -> { 1 }) end def test_should_raise_invalid_context_on_no_method_error exception = assert_raises(StateMachines::InvalidContext) do @state.call(@object, :speed, method_missing: -> { raise NoMethodError.new('Invalid', :speed, []) }) end assert_equal @object, exception.object assert_equal 'State nil for :state is not a valid context for calling #speed', exception.message end def test_should_raise_original_error_on_no_method_error_with_different_arguments assert_raises(NoMethodError) do @state.call(@object, :speed, method_missing: -> { raise NoMethodError.new('Invalid', :speed, [1]) }) end end def test_should_raise_original_error_on_no_method_error_for_different_method assert_raises(NoMethodError) do @state.call(@object, :speed, method_missing: -> { raise NoMethodError.new('Invalid', :rpm, []) }) end end end state_machines-0.100.4/test/unit/state/state_without_cached_lambda_value_test.rb000066400000000000000000000013521507333401300301730ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithoutCachedLambdaValueTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @dynamic_value = -> { 'value'.dup } @machine.states << @state = StateMachines::State.new(@machine, :parked, value: @dynamic_value) end def test_should_not_be_caching refute @state.cache end def test_should_evaluate_value_each_time value1 = @state.value value2 = @state.value refute_same value1, value2 end def test_should_not_update_value_index_for_state_collection @state.value assert_nil @machine.states['value', :value] assert_equal @state, @machine.states[@dynamic_value, :value] end end state_machines-0.100.4/test/unit/state/state_without_name_test.rb000066400000000000000000000016401507333401300252100ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateWithoutNameTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.states << @state = StateMachines::State.new(@machine, nil) end def test_should_have_a_nil_name assert_nil @state.name end def test_should_have_a_nil_qualified_name assert_nil @state.qualified_name end def test_should_have_an_empty_human_name assert_equal 'nil', @state.human_name end def test_should_have_a_nil_value assert_nil @state.value end def test_should_not_redefine_nil_predicate object = @klass.new refute_nil object refute_respond_to object, '?' end def test_should_have_a_description assert_equal 'nil', @state.description end def test_should_have_a_description_using_human_name assert_equal 'nil', @state.description(human_name: true) end end state_machines-0.100.4/test/unit/state_collection/000077500000000000000000000000001507333401300221335ustar00rootroot00000000000000state_machines-0.100.4/test/unit/state_collection/state_collection_by_default_test.rb000066400000000000000000000007501507333401300312520ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateCollectionByDefaultTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @states = StateMachines::StateCollection.new(@machine) end def test_should_not_have_any_nodes assert_equal 0, @states.length end def test_should_have_a_machine assert_equal @machine, @states.machine end def test_should_be_empty_by_priority assert_empty @states.by_priority end end state_machines-0.100.4/test/unit/state_collection/state_collection_string_test.rb000066400000000000000000000016721507333401300304460ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateCollectionStringTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @states = StateMachines::StateCollection.new(@machine) @states << @nil = StateMachines::State.new(@machine, nil) @states << @parked = StateMachines::State.new(@machine, 'parked') @machine.states.concat(@states) @object = @klass.new end def test_should_index_by_name assert_equal @parked, @states['parked', :name] end def test_should_index_by_name_by_default assert_equal @parked, @states['parked'] end def test_should_index_by_symbol_name assert_equal @parked, @states[:parked] end def test_should_index_by_qualified_name assert_equal @parked, @states['parked', :qualified_name] end def test_should_index_by_symbol_qualified_name assert_equal @parked, @states[:parked, :qualified_name] end end state_machines-0.100.4/test/unit/state_collection/state_collection_test.rb000066400000000000000000000042061507333401300270540ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateCollectionTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @states = StateMachines::StateCollection.new(@machine) @states << @nil = StateMachines::State.new(@machine, nil) @states << @parked = StateMachines::State.new(@machine, :parked) @states << @idling = StateMachines::State.new(@machine, :idling) @machine.states.concat(@states) @object = @klass.new end def test_should_index_by_name assert_equal @parked, @states[:parked, :name] end def test_should_index_by_name_by_default assert_equal @parked, @states[:parked] end def test_should_index_by_string_name assert_equal @parked, @states['parked'] end def test_should_index_by_qualified_name assert_equal @parked, @states[:parked, :qualified_name] end def test_should_index_by_string_qualified_name assert_equal @parked, @states['parked', :qualified_name] end def test_should_index_by_value assert_equal @parked, @states['parked', :value] end def test_should_not_match_if_value_does_not_match refute @states.matches?(@object, :parked) refute @states.matches?(@object, :idling) end def test_should_match_if_value_matches assert @states.matches?(@object, nil) end def test_raise_exception_if_matching_invalid_state assert_raises(IndexError) { @states.matches?(@object, :invalid) } end def test_should_find_state_for_object_if_value_is_known @object.state = 'parked' assert_equal @parked, @states.match(@object) end def test_should_find_bang_state_for_object_if_value_is_known @object.state = 'parked' assert_equal @parked, @states.match!(@object) end def test_should_not_find_state_for_object_with_unknown_value @object.state = 'invalid' assert_nil @states.match(@object) end def test_should_raise_exception_if_finding_bang_state_for_object_with_unknown_value @object.state = 'invalid' exception = assert_raises(ArgumentError) { @states.match!(@object) } assert_equal '"invalid" is not a known state value', exception.message end end state_machines-0.100.4/test/unit/state_collection/state_collection_with_custom_state_values_test.rb000066400000000000000000000014051507333401300342560ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateCollectionWithCustomStateValuesTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @states = StateMachines::StateCollection.new(@machine) @states << @state = StateMachines::State.new(@machine, :parked, value: 1) @machine.states.concat(@states) @object = @klass.new @object.state = 1 end def test_should_match_if_value_matches assert @states.matches?(@object, :parked) end def test_should_not_match_if_value_does_not_match @object.state = 2 refute @states.matches?(@object, :parked) end def test_should_find_state_for_object_if_value_is_known assert_equal @state, @states.match(@object) end end state_machines-0.100.4/test/unit/state_collection/state_collection_with_event_transitions_test.rb000066400000000000000000000021411507333401300337410ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateCollectionWithEventTransitionsTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @states = StateMachines::StateCollection.new(@machine) @states << @parked = StateMachines::State.new(@machine, :parked) @states << @idling = StateMachines::State.new(@machine, :idling) @machine.states.concat(@states) @machine.event :ignite do transition to: :idling end end def test_should_order_states_after_initial_state @parked.initial = true assert_equal [@parked, @idling], @states.by_priority end def test_should_order_states_before_states_with_behaviors @parked.context do def speed 0 end end assert_equal [@idling, @parked], @states.by_priority end def test_should_order_states_before_other_states assert_equal [@idling, @parked], @states.by_priority end def test_should_order_state_before_callback_states @machine.before_transition from: :parked, do: -> {} assert_equal [@idling, @parked], @states.by_priority end end state_machines-0.100.4/test/unit/state_collection/state_collection_with_initial_state_test.rb000066400000000000000000000024461507333401300330240ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateCollectionWithInitialStateTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @states = StateMachines::StateCollection.new(@machine) @states << @parked = StateMachines::State.new(@machine, :parked) @states << @idling = StateMachines::State.new(@machine, :idling) @machine.states.concat(@states) @parked.initial = true end def test_should_order_state_before_transition_states @machine.event :ignite do transition to: :idling end assert_equal [@parked, @idling], @states.by_priority end def test_should_order_state_before_states_with_behaviors @idling.context do def speed 0 end end assert_equal [@parked, @idling], @states.by_priority end def test_should_order_state_before_other_states assert_equal [@parked, @idling], @states.by_priority end def test_should_order_state_before_callback_states @machine.before_transition from: :idling, do: -> {} assert_equal [@parked, @idling], @states.by_priority end def test_should_have_correct_states assert_sm_states_list(@machine, %i[parked idling]) end def test_should_have_correct_initial_state assert_sm_initial_state(@machine, :parked) end end state_machines-0.100.4/test/unit/state_collection/state_collection_with_namespace_test.rb000066400000000000000000000011261507333401300321210ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateCollectionWithNamespaceTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, namespace: 'vehicle') @states = StateMachines::StateCollection.new(@machine) @states << @state = StateMachines::State.new(@machine, :parked) @machine.states.concat(@states) end def test_should_index_by_name assert_equal @state, @states[:parked, :name] end def test_should_index_by_qualified_name assert_equal @state, @states[:vehicle_parked, :qualified_name] end end state_machines-0.100.4/test/unit/state_collection/state_collection_with_state_behaviors_test.rb000066400000000000000000000021341507333401300333470ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateCollectionWithStateBehaviorsTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @states = StateMachines::StateCollection.new(@machine) @states << @parked = StateMachines::State.new(@machine, :parked) @states << @idling = StateMachines::State.new(@machine, :idling) @machine.states.concat(@states) @idling.context do def speed 0 end end end def test_should_order_states_after_initial_state @parked.initial = true assert_equal [@parked, @idling], @states.by_priority end def test_should_order_states_after_transition_states @machine.event :ignite do transition from: :parked end assert_equal [@parked, @idling], @states.by_priority end def test_should_order_states_before_other_states assert_equal [@idling, @parked], @states.by_priority end def test_should_order_state_before_callback_states @machine.before_transition from: :parked, do: -> {} assert_equal [@idling, @parked], @states.by_priority end end state_machines-0.100.4/test/unit/state_collection/state_collection_with_state_matchers_test.rb000066400000000000000000000014301507333401300331710ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateCollectionWithStateMatchersTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @states = StateMachines::StateCollection.new(@machine) @states << @state = StateMachines::State.new(@machine, :parked, if: ->(value) { !value.nil? }) @machine.states.concat(@states) @object = @klass.new @object.state = 1 end def test_should_match_if_value_matches assert @states.matches?(@object, :parked) end def test_should_not_match_if_value_does_not_match @object.state = nil refute @states.matches?(@object, :parked) end def test_should_find_state_for_object_if_value_is_known assert_equal @state, @states.match(@object) end end state_machines-0.100.4/test/unit/state_collection/state_collection_with_transition_callbacks_test.rb000066400000000000000000000021441507333401300343570ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateCollectionWithTransitionCallbacksTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new) @states = StateMachines::StateCollection.new(@machine) @states << @parked = StateMachines::State.new(@machine, :parked) @states << @idling = StateMachines::State.new(@machine, :idling) @machine.states.concat(@states) @machine.before_transition to: :idling, do: -> {} end def test_should_order_states_after_initial_state @parked.initial = true assert_equal [@parked, @idling], @states.by_priority end def test_should_order_states_after_transition_states @machine.event :ignite do transition from: :parked end assert_equal [@parked, @idling], @states.by_priority end def test_should_order_states_after_states_with_behaviors @parked.context do def speed 0 end end assert_equal [@parked, @idling], @states.by_priority end def test_should_order_states_after_other_states assert_equal [@parked, @idling], @states.by_priority end end state_machines-0.100.4/test/unit/state_context/000077500000000000000000000000001507333401300214645ustar00rootroot00000000000000state_machines-0.100.4/test/unit/state_context/state_context_proxy_test.rb000066400000000000000000000013251507333401300271760ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateContextProxyTest < StateMachinesTest def setup @klass = Class.new(Validateable) machine = StateMachines::Machine.new(@klass, initial: :parked) state = machine.state :parked @state_context = StateMachines::StateContext.new(state) end def test_should_call_class_with_same_arguments options = {} validation = @state_context.validate(:name, options) assert_equal [:name, options], validation end def test_should_pass_block_through_to_class options = {} proxy_block = -> {} validation = @state_context.validate(:name, options, &proxy_block) assert_equal [:name, options, proxy_block], validation end end state_context_proxy_with_if_and_unless_conditions_test.rb000066400000000000000000000024371507333401300353010ustar00rootroot00000000000000state_machines-0.100.4/test/unit/state_context# frozen_string_literal: true require 'test_helper' class StateContextProxyWithIfAndUnlessConditionsTest < StateMachinesTest def setup @klass = Class.new(Validateable) machine = StateMachines::Machine.new(@klass, initial: :parked) state = machine.state :parked @state_context = StateMachines::StateContext.new(state) @object = @klass.new @if_condition_result = nil @unless_condition_result = nil @options = @state_context.validate(if: -> { @if_condition_result }, unless: -> { @unless_condition_result })[0] end def test_should_be_false_if_if_condition_is_false @if_condition_result = false @unless_condition_result = false refute @options[:if].call(@object) @if_condition_result = false @unless_condition_result = true refute @options[:if].call(@object) end def test_should_be_false_if_unless_condition_is_true @if_condition_result = false @unless_condition_result = true refute @options[:if].call(@object) @if_condition_result = true @unless_condition_result = true refute @options[:if].call(@object) end def test_should_be_true_if_if_condition_is_true_and_unless_condition_is_false @if_condition_result = true @unless_condition_result = false assert @options[:if].call(@object) end end state_machines-0.100.4/test/unit/state_context/state_context_proxy_with_if_condition_test.rb000066400000000000000000000030741507333401300327600ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateContextProxyWithIfConditionTest < StateMachinesTest def setup @klass = Class.new(Validateable) machine = StateMachines::Machine.new(@klass, initial: :parked) state = machine.state :parked @state_context = StateMachines::StateContext.new(state) @object = @klass.new @condition_result = nil @options = @state_context.validate(if: -> { @condition_result })[0] end def test_should_have_if_option refute_nil @options[:if] end def test_should_be_false_if_state_is_different @object.state = nil refute @options[:if].call(@object) end def test_should_be_false_if_original_condition_is_false @condition_result = false refute @options[:if].call(@object) end def test_should_be_true_if_state_matches_and_original_condition_is_true @condition_result = true assert @options[:if].call(@object) end def test_should_evaluate_symbol_condition @klass.class_eval do attr_accessor :callback end options = @state_context.validate(if: :callback)[0] object = @klass.new object.callback = false refute options[:if].call(object) object.callback = true assert options[:if].call(object) end def test_should_evaluate_string_condition @klass.class_eval do attr_accessor :callback end options = @state_context.validate(if: '@callback')[0] object = @klass.new object.callback = false refute options[:if].call(object) object.callback = true assert options[:if].call(object) end end state_context_proxy_with_multiple_if_conditions_test.rb000066400000000000000000000017631507333401300350020ustar00rootroot00000000000000state_machines-0.100.4/test/unit/state_context# frozen_string_literal: true require 'test_helper' class StateContextProxyWithMultipleIfConditionsTest < StateMachinesTest def setup @klass = Class.new(Validateable) machine = StateMachines::Machine.new(@klass, initial: :parked) state = machine.state :parked @state_context = StateMachines::StateContext.new(state) @object = @klass.new @first_condition_result = nil @second_condition_result = nil @options = @state_context.validate(if: [-> { @first_condition_result }, -> { @second_condition_result }])[0] end def test_should_be_true_if_all_conditions_are_true @first_condition_result = true @second_condition_result = true assert @options[:if].call(@object) end def test_should_be_false_if_any_condition_is_false @first_condition_result = true @second_condition_result = false refute @options[:if].call(@object) @first_condition_result = false @second_condition_result = true refute @options[:if].call(@object) end end state_context_proxy_with_multiple_unless_conditions_test.rb000066400000000000000000000017751507333401300357200ustar00rootroot00000000000000state_machines-0.100.4/test/unit/state_context# frozen_string_literal: true require 'test_helper' class StateContextProxyWithMultipleUnlessConditionsTest < StateMachinesTest def setup @klass = Class.new(Validateable) machine = StateMachines::Machine.new(@klass, initial: :parked) state = machine.state :parked @state_context = StateMachines::StateContext.new(state) @object = @klass.new @first_condition_result = nil @second_condition_result = nil @options = @state_context.validate(unless: [-> { @first_condition_result }, -> { @second_condition_result }])[0] end def test_should_be_true_if_all_conditions_are_false @first_condition_result = false @second_condition_result = false assert @options[:if].call(@object) end def test_should_be_false_if_any_condition_is_true @first_condition_result = true @second_condition_result = false refute @options[:if].call(@object) @first_condition_result = false @second_condition_result = true refute @options[:if].call(@object) end end state_machines-0.100.4/test/unit/state_context/state_context_proxy_with_unless_condition_test.rb000066400000000000000000000031141507333401300336660ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateContextProxyWithUnlessConditionTest < StateMachinesTest def setup @klass = Class.new(Validateable) machine = StateMachines::Machine.new(@klass, initial: :parked) state = machine.state :parked @state_context = StateMachines::StateContext.new(state) @object = @klass.new @condition_result = nil @options = @state_context.validate(unless: -> { @condition_result })[0] end def test_should_have_if_option refute_nil @options[:if] end def test_should_be_false_if_state_is_different @object.state = nil refute @options[:if].call(@object) end def test_should_be_false_if_original_condition_is_true @condition_result = true refute @options[:if].call(@object) end def test_should_be_true_if_state_matches_and_original_condition_is_false @condition_result = false assert @options[:if].call(@object) end def test_should_evaluate_symbol_condition @klass.class_eval do attr_accessor :callback end options = @state_context.validate(unless: :callback)[0] object = @klass.new object.callback = true refute options[:if].call(object) object.callback = false assert options[:if].call(object) end def test_should_evaluate_string_condition @klass.class_eval do attr_accessor :callback end options = @state_context.validate(unless: '@callback')[0] object = @klass.new object.callback = true refute options[:if].call(object) object.callback = false assert options[:if].call(object) end end state_machines-0.100.4/test/unit/state_context/state_context_proxy_without_conditions_test.rb000066400000000000000000000014101507333401300332050ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateContextProxyWithoutConditionsTest < StateMachinesTest def setup @klass = Class.new(Validateable) machine = StateMachines::Machine.new(@klass, initial: :parked) state = machine.state :parked @state_context = StateMachines::StateContext.new(state) @object = @klass.new @options = @state_context.validate[0] end def test_should_have_options_configuration assert_instance_of Hash, @options end def test_should_have_if_option refute_nil @options[:if] end def test_should_be_false_if_state_is_different @object.state = nil refute @options[:if].call(@object) end def test_should_be_true_if_state_matches assert @options[:if].call(@object) end end state_machines-0.100.4/test/unit/state_context/state_context_test.rb000066400000000000000000000011651507333401300257370ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class Validateable class << self def validate(*args, &block) args << block if block_given? args end end end class StateContextTest < StateMachinesTest def setup @klass = Class.new(Validateable) @machine = StateMachines::Machine.new(@klass, initial: :parked) @state = @machine.state :parked @state_context = StateMachines::StateContext.new(@state) end def test_should_have_a_machine assert_equal @machine, @state_context.machine end def test_should_have_a_state assert_equal @state, @state_context.state end end state_machines-0.100.4/test/unit/state_context/state_context_transition_test.rb000066400000000000000000000076311507333401300302150ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateContextTransitionTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @state = @machine.state :parked @state_context = StateMachines::StateContext.new(@state) end def test_should_not_allow_except_to exception = assert_raises(ArgumentError) { @state_context.transition(except_to: :idling) } assert_equal 'Unknown key: :except_to. Valid keys are: :from, :to, :on, :if, :unless', exception.message end def test_should_not_allow_except_from exception = assert_raises(ArgumentError) { @state_context.transition(except_from: :idling) } assert_equal 'Unknown key: :except_from. Valid keys are: :from, :to, :on, :if, :unless', exception.message end def test_should_not_allow_implicit_transitions exception = assert_raises(ArgumentError) { @state_context.transition(parked: :idling) } assert_equal 'Unknown key: :parked. Valid keys are: :from, :to, :on, :if, :unless', exception.message end def test_should_not_allow_except_on exception = assert_raises(ArgumentError) { @state_context.transition(except_on: :park) } assert_equal 'Unknown key: :except_on. Valid keys are: :from, :to, :on, :if, :unless', exception.message end def test_should_require_on_event exception = assert_raises(ArgumentError) { @state_context.transition(to: :idling) } assert_equal 'Must specify :on event', exception.message end def test_should_not_allow_missing_from_and_to exception = assert_raises(ArgumentError) { @state_context.transition(on: :ignite) } assert_equal 'Must specify either :to or :from state', exception.message end def test_should_not_allow_from_and_to exception = assert_raises(ArgumentError) { @state_context.transition(on: :ignite, from: :parked, to: :idling) } assert_equal 'Must specify either :to or :from state', exception.message end def test_should_allow_to_state_if_missing_from_state @state_context.transition(on: :park, from: :parked) end def test_should_allow_from_state_if_missing_to_state @state_context.transition(on: :ignite, to: :idling) end def test_should_automatically_set_to_option_with_from_state branch = @state_context.transition(from: :idling, on: :park) assert_instance_of StateMachines::Branch, branch state_requirements = branch.state_requirements assert_equal 1, state_requirements.length from_requirement = state_requirements[0][:to] assert_instance_of StateMachines::WhitelistMatcher, from_requirement assert_equal [:parked], from_requirement.values end def test_should_automatically_set_from_option_with_to_state branch = @state_context.transition(to: :idling, on: :ignite) assert_instance_of StateMachines::Branch, branch state_requirements = branch.state_requirements assert_equal 1, state_requirements.length from_requirement = state_requirements[0][:from] assert_instance_of StateMachines::WhitelistMatcher, from_requirement assert_equal [:parked], from_requirement.values end def test_should_allow_if_condition @state_context.transition(to: :idling, on: :park, if: :seatbelt_on?) end def test_should_allow_unless_condition @state_context.transition(to: :idling, on: :park, unless: :seatbelt_off?) end def test_should_include_all_transition_states_in_machine_states @state_context.transition(to: :idling, on: :ignite) assert_equal(%i[parked idling], @machine.states.map { |state| state.name }) end def test_should_include_all_transition_events_in_machine_events @state_context.transition(to: :idling, on: :ignite) assert_equal([:ignite], @machine.events.map { |event| event.name }) end def test_should_allow_multiple_events @state_context.transition(to: :idling, on: %i[ignite shift_up]) assert_equal(%i[ignite shift_up], @machine.events.map { |event| event.name }) end end state_machines-0.100.4/test/unit/state_context/state_context_with_matching_transition_test.rb000066400000000000000000000014041507333401300331120ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateContextWithMatchingTransitionTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @state = @machine.state :parked @state_context = StateMachines::StateContext.new(@state) @state_context.transition(to: :idling, on: :ignite) @event = @machine.event(:ignite) @object = @klass.new end def test_should_be_able_to_fire assert @event.can_fire?(@object) end def test_should_have_a_transition transition = @event.transition_for(@object) refute_nil transition assert_equal 'parked', transition.from assert_equal 'idling', transition.to assert_equal :ignite, transition.event end end state_machines-0.100.4/test/unit/state_machine/000077500000000000000000000000001507333401300214045ustar00rootroot00000000000000state_machines-0.100.4/test/unit/state_machine/state_machine_by_default_test.rb000066400000000000000000000004241507333401300277720ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateMachineByDefaultTest < StateMachinesTest def setup @klass = Class.new @machine = @klass.state_machine end def test_should_use_state_attribute assert_equal :state, @machine.attribute end end state_machines-0.100.4/test/unit/state_machine/state_machine_test.rb000066400000000000000000000006631507333401300256010ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StateMachineTest < StateMachinesTest def setup @klass = Class.new end def test_should_allow_state_machines_on_any_class assert_respond_to @klass, :state_machine end def test_should_evaluate_block_within_machine_context responded = false @klass.state_machine(:state) do responded = respond_to?(:event) end assert responded end end state_machines-0.100.4/test/unit/stdio_renderer_test.rb000066400000000000000000000110351507333401300231740ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class STDIORendererTest < Minitest::Test def test_draw_machine machine = StateMachines::Machine.new(Class.new) do state :parked state :idling event :ignite do transition parked: :idling end end io = StringIO.new machine.draw(io: io) assert_includes(io.string, 'Class: ') assert_includes(io.string, 'States:') assert_includes(io.string, 'parked') assert_includes(io.string, 'idling') assert_includes(io.string, 'Events:') assert_includes(io.string, 'ignite') assert_includes(io.string, 'parked => idling') end def test_draw_machine_with_no_events machine = StateMachines::Machine.new(Class.new) do state :parked end io = StringIO.new machine.draw(io: io) assert_includes(io.string, 'Class: ') assert_includes(io.string, 'States:') assert_includes(io.string, 'parked') assert_includes(io.string, 'Events:') assert_includes(io.string, 'None') end def test_draw_machine_with_custom_io machine = StateMachines::Machine.new(Class.new) do state :parked end io = StringIO.new machine.draw(io: io) assert_includes(io.string, 'Class: ') assert_includes(io.string, 'States:') assert_includes(io.string, 'parked') assert_includes(io.string, 'Events:') assert_includes(io.string, 'None') end def test_draw_class machine = StateMachines::Machine.new(Class.new) {} io = StringIO.new machine.renderer.draw_class(machine: machine, io: io) assert_includes(io.string, 'Class: ') end def test_draw_states machine = StateMachines::Machine.new(Class.new) do state :parked state :idling end io = StringIO.new machine.renderer.draw_states(machine: machine, io: io) assert_includes(io.string, 'States:') assert_includes(io.string, 'parked') assert_includes(io.string, 'idling') end def test_draw_event machine = StateMachines::Machine.new(Class.new) {} event = StateMachines::Event.new(machine, :ignite) graph = {} io = StringIO.new machine.renderer.draw_event(event, graph, options: {}, io: io) assert_includes(io.string, 'Event: ignite') end def test_draw_branch machine = StateMachines::Machine.new(Class.new) {} branch = StateMachines::Branch.new graph = {} event = StateMachines::Event.new(machine, :ignite) io = StringIO.new machine.renderer.draw_branch(branch, graph, event, options: {}, io: io) assert_includes(io.string, 'Branch: ') end def test_draw_state machine = StateMachines::Machine.new(Class.new) {} state = StateMachines::State.new(machine, :parked) graph = {} io = StringIO.new machine.renderer.draw_state(state, graph, options: {}, io: io) assert_includes(io.string, 'State: parked') end def test_draw_events machine = StateMachines::Machine.new(Class.new) do state :parked state :idling event :ignite do transition parked: :idling end end io = StringIO.new machine.renderer.draw_events(machine: machine, io: io) assert_includes(io.string, 'Events:') assert_includes(io.string, 'ignite') assert_includes(io.string, 'parked => idling') end def test_draw_if_unless_condition machine = StateMachines::Machine.new(Class.new) do state :parked state :idling event :ignite do transition parked: :idling, if: :key_inserted? end end io = StringIO.new machine.renderer.draw_events(machine: machine, io: io) assert_includes(io.string, 'Events:') assert_includes(io.string, 'ignite') assert_includes(io.string, 'parked => idling IF key_inserted?') end def test_draw_blacklist_matcher machine = StateMachines::Machine.new(Class.new) do state :parked state :idling event :turn_of do transition all - [:parked] => :parked end end io = StringIO.new machine.renderer.draw_events(machine: machine, io: io) assert_includes(io.string, 'Events:') assert_includes(io.string, 'turn_of') assert_includes(io.string, 'ALL EXCEPT parked => parked') end def test_draw_all_and_same_matcher machine = StateMachines::Machine.new(Class.new) do state :parked state :idling event :wave do transition all => same end end io = StringIO.new machine.renderer.draw_events(machine: machine, io: io) assert_includes(io.string, 'Events:') assert_includes(io.string, 'wave') assert_includes(io.string, 'ALL => SAME') end end state_machines-0.100.4/test/unit/transition/000077500000000000000000000000001507333401300207725ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition/transition_after_being_performed_test.rb000066400000000000000000000020721507333401300311410ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionAfterBeingPerformedTest < StateMachinesTest def setup @klass = Class.new do attr_reader :saved, :save_state def save @save_state = state @saved = true 1 end end @machine = StateMachines::Machine.new(@klass, action: :save) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) @result = @transition.perform end def test_should_have_empty_args assert_empty @transition.args end def test_should_have_a_result assert_equal 1, @transition.result end def test_should_be_successful assert @result end def test_should_change_the_current_state assert_equal 'idling', @object.state end def test_should_run_the_action assert @object.saved end def test_should_run_the_action_after_saving_the_state assert_equal 'idling', @object.save_state end end state_machines-0.100.4/test/unit/transition/transition_after_being_persisted_test.rb000066400000000000000000000022351507333401300311610ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionAfterBeingPersistedTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, action: :save) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) @transition.persist end def test_should_update_state_value assert_equal 'idling', @object.state end def test_should_not_change_from_state assert_equal 'parked', @transition.from end def test_should_not_change_to_state assert_equal 'idling', @transition.to end def test_should_not_be_able_to_persist_twice @object.state = 'parked' @transition.persist assert_equal 'parked', @object.state end def test_should_be_able_to_persist_again_after_resetting @object.state = 'parked' @transition.reset @transition.persist assert_equal 'idling', @object.state end def test_should_revert_to_from_state_on_rollback @transition.rollback assert_equal 'parked', @object.state end end state_machines-0.100.4/test/unit/transition/transition_after_being_rolled_back_test.rb000066400000000000000000000015661507333401300314260ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionAfterBeingRolledBackTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, action: :save) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) @object.state = 'idling' @transition.rollback end def test_should_update_state_value_to_from_state assert_equal 'parked', @object.state end def test_should_not_change_from_state assert_equal 'parked', @transition.from end def test_should_not_change_to_state assert_equal 'idling', @transition.to end def test_should_still_be_able_to_persist @transition.persist assert_equal 'idling', @object.state end end state_machines-0.100.4/test/unit/transition/transition_equality_test.rb000066400000000000000000000034531507333401300264720ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionEqualityTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_be_equal_with_same_properties transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) assert_equal transition, @transition end def test_should_not_be_equal_with_different_machines machine = StateMachines::Machine.new(@klass, :status, namespace: :other) machine.state :parked, :idling machine.event :ignite transition = StateMachines::Transition.new(@object, machine, :ignite, :parked, :idling) refute_equal transition, @transition end def test_should_not_be_equal_with_different_objects transition = StateMachines::Transition.new(@klass.new, @machine, :ignite, :parked, :idling) refute_equal transition, @transition end def test_should_not_be_equal_with_different_event_names @machine.event :park transition = StateMachines::Transition.new(@object, @machine, :park, :parked, :idling) refute_equal transition, @transition end def test_should_not_be_equal_with_different_from_state_names @machine.state :first_gear transition = StateMachines::Transition.new(@object, @machine, :ignite, :first_gear, :idling) refute_equal transition, @transition end def test_should_not_be_equal_with_different_to_state_names @machine.state :first_gear transition = StateMachines::Transition.new(@object, @machine, :ignite, :idling, :first_gear) refute_equal transition, @transition end end state_machines-0.100.4/test/unit/transition/transition_fiber_deadlock_test.rb000066400000000000000000000041011507333401300275410ustar00rootroot00000000000000# frozen_string_literal: true require File.expand_path('../../test_helper', __dir__) class TransitionFiberDeadlockTest < StateMachinesTest def setup @klass = Class.new do attr_accessor :state def initialize @state = 'waiting' end state_machine :state, initial: :waiting do event :proceed do transition waiting: :processing end around_transition do |_object, _transition, block| # This test simulates the deadlock scenario mentioned in issue #152 # where nested Fibers cause Thread.current conflicts # Create a nested fiber that uses Thread.current nested_fiber = Fiber.new do # This would previously cause issues with Thread.current[:state_machine_fiber_pausable] Thread.current[:test_marker] = :nested_value Fiber.yield :nested_started Thread.current[:test_marker] = :nested_finished end nested_fiber.resume block.call nested_fiber.resume if nested_fiber.alive? end end end end def test_should_not_cause_deadlock_with_nested_fibers # This test reproduces the scenario described in issue #152 # With the bug using Thread.current, this could cause conflicts # With the fix using Fiber.current, it should work properly object = @klass.new # This should complete without raising any ThreadError begin object.proceed assert_equal 'processing', object.state rescue ThreadError => e flunk "Deadlock occurred: #{e.message}" end end def test_multiple_transitions_in_fibers # Test that multiple state machine transitions can run in separate fibers # without Thread.current conflicts object1 = @klass.new object2 = @klass.new results = [] fiber1 = Fiber.new do object1.proceed results << object1.state end fiber2 = Fiber.new do object2.proceed results << object2.state end fiber1.resume fiber2.resume assert_equal %w[processing processing], results end end state_machines-0.100.4/test/unit/transition/transition_loopback_test.rb000066400000000000000000000007371507333401300264310ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionLoopbackTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked @machine.event :park @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :park, :parked, :parked) end def test_should_be_loopback assert_predicate @transition, :loopback? end end state_machines-0.100.4/test/unit/transition/transition_perform_test.rb000066400000000000000000000020601507333401300263000ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionPerformTest < StateMachinesTest def setup @klass = Class.new do attr_reader :saved def save @saved = true end end @machine = StateMachines::Machine.new(@klass, action: :save) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_run_action_with_true @transition.perform(true) assert @object.saved end def test_should_not_run_action_with_false @transition.perform(false) refute @object.saved end def test_should_run_action_with_run_action_true @transition.perform(run_action: true) assert @object.saved end def test_should_not_run_action_with_run_action_false @transition.perform(run_action: false) refute @object.saved end def test_should_run_action_by_default @transition.perform assert @object.saved end end state_machines-0.100.4/test/unit/transition/transition_test.rb000066400000000000000000000045731507333401300245610ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_have_an_object assert_equal @object, @transition.object end def test_should_have_a_machine assert_equal @machine, @transition.machine end def test_should_have_an_event assert_equal :ignite, @transition.event end def test_should_have_a_qualified_event assert_equal :ignite, @transition.qualified_event end def test_should_have_a_human_event assert_equal 'ignite', @transition.human_event end def test_should_have_a_from_value assert_equal 'parked', @transition.from end def test_should_have_a_from_name assert_equal :parked, @transition.from_name end def test_should_have_a_qualified_from_name assert_equal :parked, @transition.qualified_from_name end def test_should_have_a_human_from_name assert_equal 'parked', @transition.human_from_name end def test_should_have_a_to_value assert_equal 'idling', @transition.to end def test_should_have_a_to_name assert_equal :idling, @transition.to_name end def test_should_have_a_qualified_to_name assert_equal :idling, @transition.qualified_to_name end def test_should_have_a_human_to_name assert_equal 'idling', @transition.human_to_name end def test_should_have_an_attribute assert_equal :state, @transition.attribute end def test_should_not_have_an_action assert_nil @transition.action end def test_should_not_be_transient refute_predicate @transition, :transient? end def test_should_generate_attributes expected = { object: @object, attribute: :state, event: :ignite, from: 'parked', to: 'idling' } assert_equal expected, @transition.attributes end def test_should_have_empty_args assert_empty @transition.args end def test_should_not_have_a_result assert_nil @transition.result end def test_should_use_pretty_inspect assert_equal '#', @transition.inspect end end state_machines-0.100.4/test/unit/transition/transition_thread_current_preservation_test.rb000066400000000000000000000077341507333401300324550ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionThreadCurrentPreservationTest < StateMachinesTest def setup @klass = Class.new do attr_accessor :state def initialize @state = 'waiting' end state_machine :state, initial: :waiting do event :proceed do transition waiting: :processing end end end end def test_should_preserve_thread_current_object_identity_across_callbacks # This test reproduces issue #152 and verifies the fix object_ids = [] # Initialize some thread-local storage Thread.current[:test_store] = { value: 'test' } original_object_id = Thread.current[:test_store].object_id @klass.state_machine.before_transition do |obj, transition| object_ids << Thread.current[:test_store].object_id end @klass.state_machine.around_transition do |obj, transition, block| object_ids << Thread.current[:test_store].object_id block.call object_ids << Thread.current[:test_store].object_id end @klass.state_machine.after_transition do |obj, transition| object_ids << Thread.current[:test_store].object_id end # Run the transition object = @klass.new object.proceed # All callbacks should see the same Thread.current[:test_store] object_id assert_equal [original_object_id] * 4, object_ids, "Thread.current[:test_store] object_id should be preserved across all callbacks" # Verify the object itself is still accessible and correct assert_equal 'test', Thread.current[:test_store][:value] assert_equal original_object_id, Thread.current[:test_store].object_id end def test_should_preserve_multiple_thread_locals # Test that multiple Thread.current keys are preserved Thread.current[:store1] = { name: 'store1' } Thread.current[:store2] = { name: 'store2' } original_ids = { store1: Thread.current[:store1].object_id, store2: Thread.current[:store2].object_id } collected_ids = { store1: [], store2: [] } @klass.state_machine.before_transition do |obj, transition| collected_ids[:store1] << Thread.current[:store1].object_id collected_ids[:store2] << Thread.current[:store2].object_id end @klass.state_machine.after_transition do |obj, transition| collected_ids[:store1] << Thread.current[:store1].object_id collected_ids[:store2] << Thread.current[:store2].object_id end object = @klass.new object.proceed # Both stores should maintain their object identity assert_equal [original_ids[:store1]] * 2, collected_ids[:store1] assert_equal [original_ids[:store2]] * 2, collected_ids[:store2] end def test_should_handle_nil_thread_locals_gracefully # Test that nil thread locals don't cause issues Thread.current[:nil_store] = nil @klass.state_machine.before_transition do |obj, transition| # Should not raise an error Thread.current[:nil_store] end object = @klass.new # Should not raise an error object.proceed assert true end def test_should_allow_thread_local_modification_in_fiber # Test that modifications in callbacks are preserved Thread.current[:modifiable] = { count: 0 } original_object_id = Thread.current[:modifiable].object_id @klass.state_machine.before_transition do |obj, transition| Thread.current[:modifiable][:count] += 1 end @klass.state_machine.around_transition do |obj, transition, block| Thread.current[:modifiable][:count] += 10 block.call Thread.current[:modifiable][:count] += 100 end @klass.state_machine.after_transition do |obj, transition| Thread.current[:modifiable][:count] += 1000 end object = @klass.new object.proceed # The object should be the same and modifications should be preserved assert_equal original_object_id, Thread.current[:modifiable].object_id assert_equal 1111, Thread.current[:modifiable][:count] # 1 + 10 + 100 + 1000 end end state_machines-0.100.4/test/unit/transition/transition_transient_test.rb000066400000000000000000000010211507333401300266310ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionTransientTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) @transition.transient = true end def test_should_be_transient assert_predicate @transition, :transient? end end state_machines-0.100.4/test/unit/transition/transition_with_action_test.rb000066400000000000000000000011541507333401300271410ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithActionTest < StateMachinesTest def setup @klass = Class.new do def save; end end @machine = StateMachines::Machine.new(@klass, action: :save) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_have_an_action assert_equal :save, @transition.action end def test_should_not_have_a_result assert_nil @transition.result end end state_machines-0.100.4/test/unit/transition/transition_with_after_callbacks_skipped_test.rb000066400000000000000000000112671507333401300325110ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithAfterCallbacksSkippedTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_run_before_callbacks @machine.before_transition { @run = true } assert @transition.run_callbacks(after: false) assert @run end def test_should_not_run_after_callbacks @run = false @machine.after_transition { @run = true } assert @transition.run_callbacks(after: false) refute @run end def test_should_run_around_callbacks_before_yield @machine.around_transition do |block| @run = true block.call end assert @transition.run_callbacks(after: false) assert @run end def test_should_not_run_around_callbacks_after_yield @run = false @machine.around_transition do |block| block.call @run = true end assert @transition.run_callbacks(after: false) refute @run end def test_should_continue_around_transition_execution_on_second_call @callbacks = [] @machine.around_transition do |block| @callbacks << :before_around_1 block.call @callbacks << :after_around_1 end @machine.around_transition do |block| @callbacks << :before_around_2 block.call @callbacks << :after_around_2 end @machine.after_transition { @callbacks << :after } assert @transition.run_callbacks(after: false) assert_equal %i[before_around_1 before_around_2], @callbacks assert @transition.run_callbacks assert_equal %i[before_around_1 before_around_2 after_around_2 after_around_1 after], @callbacks end def test_should_not_run_further_callbacks_if_halted_during_continue_around_transition @callbacks = [] @machine.around_transition do |block| @callbacks << :before_around_1 block.call @callbacks << :after_around_1 end @machine.around_transition do |block| @callbacks << :before_around_2 block.call @callbacks << :after_around_2 throw :halt end @machine.after_transition { @callbacks << :after } assert @transition.run_callbacks(after: false) assert_equal %i[before_around_1 before_around_2], @callbacks assert @transition.run_callbacks assert_equal %i[before_around_1 before_around_2 after_around_2], @callbacks end def test_should_not_be_able_to_continue_twice @count = 0 @machine.around_transition do |block| block.call @count += 1 end @machine.after_transition { @count += 1 } @transition.run_callbacks(after: false) 2.times do assert @transition.run_callbacks assert_equal 2, @count end end def test_should_not_be_able_to_continue_again_after_halted @count = 0 @machine.around_transition do |block| block.call @count += 1 throw :halt end @machine.after_transition { @count += 1 } @transition.run_callbacks(after: false) 2.times do assert @transition.run_callbacks assert_equal 1, @count end end def test_should_have_access_to_result_after_continued @machine.around_transition do |block| @around_before_result = @transition.result block.call @around_after_result = @transition.result end @machine.after_transition { @after_result = @transition.result } @transition.run_callbacks(after: false) @transition.run_callbacks { { result: 1 } } assert_nil @around_before_result assert_equal 1, @around_after_result assert_equal 1, @after_result end def test_should_raise_exceptions_during_around_callbacks_after_yield_in_second_execution @machine.around_transition do |block| block.call raise ArgumentError end @transition.run_callbacks(after: false) assert_raises(ArgumentError) { @transition.run_callbacks } end # This test is no longer relevant since all Ruby engines support pause # Previously, it tested that ArgumentError was raised when pause wasn't supported # def test_should_raise_exception_on_second_call # @callbacks = [] # @machine.around_transition do |block| # @callbacks << :before_around_1 # block.call # @callbacks << :after_around_1 # end # @machine.around_transition do |block| # @callbacks << :before_around_2 # block.call # @callbacks << :after_around_2 # end # @machine.after_transition { @callbacks << :after } # # assert_raises(ArgumentError) { @transition.run_callbacks(after: false) } # end end state_machines-0.100.4/test/unit/transition/transition_with_after_callbacks_test.rb000066400000000000000000000051061507333401300307650ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithAfterCallbacksTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_run_after_callbacks @machine.after_transition { |_object| @run = true } result = @transition.run_callbacks assert result assert @run end def test_should_only_run_those_that_match_transition_context @count = 0 callback = -> { @count += 1 } @machine.after_transition from: :parked, to: :idling, on: :park, do: callback @machine.after_transition from: :parked, to: :parked, on: :park, do: callback @machine.after_transition from: :parked, to: :idling, on: :ignite, do: callback @machine.after_transition from: :idling, to: :idling, on: :park, do: callback @transition.run_callbacks assert_equal 1, @count end def test_should_not_run_if_not_successful @run = false @machine.after_transition { |_object| @run = true } @transition.run_callbacks { { success: false } } refute @run end def test_should_run_if_successful @machine.after_transition { |_object| @run = true } @transition.run_callbacks { { success: true } } assert @run end def test_should_pass_transition_as_argument @machine.after_transition { |*args| @args = args } @transition.run_callbacks assert_equal [@object, @transition], @args end def test_should_catch_halts @machine.after_transition { throw :halt } result = @transition.run_callbacks assert result end def test_should_not_catch_exceptions @machine.after_transition { raise ArgumentError } assert_raises(ArgumentError) { @transition.run_callbacks } end def test_should_not_be_able_to_run_twice @count = 0 @machine.after_transition { @count += 1 } @transition.run_callbacks @transition.run_callbacks assert_equal 1, @count end def test_should_not_be_able_to_run_twice_if_halted @count = 0 @machine.after_transition do @count += 1 throw :halt end @transition.run_callbacks @transition.run_callbacks assert_equal 1, @count end def test_should_be_able_to_run_again_after_resetting @count = 0 @machine.after_transition { @count += 1 } @transition.run_callbacks @transition.reset @transition.run_callbacks assert_equal 2, @count end end state_machines-0.100.4/test/unit/transition/transition_with_around_callbacks_test.rb000066400000000000000000000115221507333401300311530ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithAroundCallbacksTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_run_around_callbacks @machine.around_transition do |_object, _transition, block| @run_before = true block.call @run_after = true end result = @transition.run_callbacks assert result assert @run_before assert @run_after end def test_should_only_run_those_that_match_transition_context @count = 0 callback = lambda { |_object, _transition, block| @count += 1 block.call } @machine.around_transition from: :parked, to: :idling, on: :park, do: callback @machine.around_transition from: :parked, to: :parked, on: :park, do: callback @machine.around_transition from: :parked, to: :idling, on: :ignite, do: callback @machine.around_transition from: :idling, to: :idling, on: :park, do: callback @transition.run_callbacks assert_equal 1, @count end def test_should_pass_transition_as_argument @machine.around_transition do |*args| block = args.pop @args = args block.call end @transition.run_callbacks assert_equal [@object, @transition], @args end def test_should_run_block_between_callback @callbacks = [] @machine.around_transition do |block| @callbacks << :before block.call @callbacks << :after end @transition.run_callbacks do @callbacks << :within { success: true } end assert_equal %i[before within after], @callbacks end def test_should_have_access_to_result_after_yield @machine.around_transition do |block| @before_result = @transition.result block.call @after_result = @transition.result end @transition.run_callbacks { { result: 1, success: true } } assert_nil @before_result assert_equal 1, @after_result end def test_should_catch_before_yield_halts @machine.around_transition { throw :halt } result = @transition.run_callbacks refute result end def test_should_catch_after_yield_halts @machine.around_transition do |block| block.call throw :halt end result = @transition.run_callbacks assert result end def test_should_not_catch_before_yield @machine.around_transition { raise ArgumentError } assert_raises(ArgumentError) { @transition.run_callbacks } end def test_should_not_catch_after_yield @machine.around_transition do |block| block.call raise ArgumentError end assert_raises(ArgumentError) { @transition.run_callbacks } end def test_should_fail_if_not_yielded @machine.around_transition {} result = @transition.run_callbacks refute result end def test_should_not_be_able_to_run_twice @before_count = 0 @after_count = 0 @machine.around_transition do |block| @before_count += 1 block.call @after_count += 1 end @transition.run_callbacks @transition.run_callbacks assert_equal 1, @before_count assert_equal 1, @after_count end def test_should_be_able_to_run_again_after_resetting @before_count = 0 @after_count = 0 @machine.around_transition do |block| @before_count += 1 block.call @after_count += 1 end @transition.run_callbacks @transition.reset @transition.run_callbacks assert_equal 2, @before_count assert_equal 2, @after_count end def test_should_succeed_if_block_result_is_false @machine.around_transition do |block| @before_run = true block.call @after_run = true end assert(@transition.run_callbacks { { success: true, result: false } }) assert @before_run assert @after_run end def test_should_succeed_if_block_result_is_true @machine.around_transition do |block| @before_run = true block.call @after_run = true end assert(@transition.run_callbacks { { success: true, result: true } }) assert @before_run assert @after_run end def test_should_only_run_before_if_block_success_is_false @after_run = false @machine.around_transition do |block| @before_run = true block.call @after_run = true end assert(@transition.run_callbacks { { success: false } }) assert @before_run refute @after_run end def test_should_succeed_if_block_success_is_false @machine.around_transition do |block| @before_run = true block.call @after_run = true end assert(@transition.run_callbacks { { success: true } }) assert @before_run assert @after_run end end state_machines-0.100.4/test/unit/transition/transition_with_before_callbacks_skipped_test.rb000066400000000000000000000014031507333401300326410ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithBeforeCallbacksSkippedTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_not_run_before_callbacks @run = false @machine.before_transition { @run = true } refute @transition.run_callbacks(before: false) refute @run end def test_should_run_failure_callbacks @machine.after_failure { @run = true } refute @transition.run_callbacks(before: false) assert @run end end state_machines-0.100.4/test/unit/transition/transition_with_before_callbacks_test.rb000066400000000000000000000056631507333401300311360ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithBeforeCallbacksTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_run_before_callbacks @machine.before_transition { @run = true } result = @transition.run_callbacks assert result assert @run end def test_should_only_run_those_that_match_transition_context @count = 0 callback = -> { @count += 1 } @machine.before_transition from: :parked, to: :idling, on: :park, do: callback @machine.before_transition from: :parked, to: :parked, on: :park, do: callback @machine.before_transition from: :parked, to: :idling, on: :ignite, do: callback @machine.before_transition from: :idling, to: :idling, on: :park, do: callback @transition.run_callbacks assert_equal 1, @count end def test_should_pass_transition_as_argument @machine.before_transition { |*args| @args = args } @transition.run_callbacks assert_equal [@object, @transition], @args end def test_should_catch_halts @machine.before_transition { throw :halt } result = @transition.run_callbacks refute result end def test_should_not_catch_exceptions @machine.before_transition { raise ArgumentError } assert_raises(ArgumentError) { @transition.run_callbacks } end def test_should_not_be_able_to_run_twice @count = 0 @machine.before_transition { @count += 1 } @transition.run_callbacks @transition.run_callbacks assert_equal 1, @count end def test_should_be_able_to_run_again_after_halt @count = 0 @machine.before_transition do @count += 1 throw :halt end @transition.run_callbacks @transition.run_callbacks assert_equal 2, @count end def test_should_be_able_to_run_again_after_resetting @count = 0 @machine.before_transition { @count += 1 } @transition.run_callbacks @transition.reset @transition.run_callbacks assert_equal 2, @count end def test_should_succeed_if_block_result_is_false @machine.before_transition { @run = true } assert(@transition.run_callbacks { { result: false } }) assert @run end def test_should_succeed_if_block_result_is_true @machine.before_transition { @run = true } assert(@transition.run_callbacks { { result: true } }) assert @run end def test_should_succeed_if_block_success_is_false @machine.before_transition { @run = true } assert(@transition.run_callbacks { { success: false } }) assert @run end def test_should_succeed_if_block_success_is_true @machine.before_transition { @run = true } assert(@transition.run_callbacks { { success: true } }) assert @run end end state_machines-0.100.4/test/unit/transition/transition_with_custom_machine_attribute_test.rb000066400000000000000000000013041507333401300327420ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithCustomMachineAttributeTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, :state, attribute: :state_id) @machine.state :off, value: 1 @machine.state :active, value: 2 @machine.event :activate @object = @klass.new @object.state_id = 1 @transition = StateMachines::Transition.new(@object, @machine, :activate, :off, :active) end def test_should_persist @transition.persist assert_equal 2, @object.state_id end def test_should_rollback @object.state_id = 2 @transition.rollback assert_equal 1, @object.state_id end end state_machines-0.100.4/test/unit/transition/transition_with_different_states_test.rb000066400000000000000000000007731507333401300312230ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithDifferentStatesTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_not_be_loopback refute_predicate @transition, :loopback? end end state_machines-0.100.4/test/unit/transition/transition_with_dynamic_to_value_test.rb000066400000000000000000000010251507333401300312030ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithDynamicToValueTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked @machine.state :idling, value: -> { 1 } @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_evaluate_to_value assert_equal 1, @transition.to end end state_machines-0.100.4/test/unit/transition/transition_with_failure_callbacks_test.rb000066400000000000000000000046551507333401300313230ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithFailureCallbacksTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_only_run_those_that_match_transition_context @count = 0 callback = -> { @count += 1 } @machine.after_failure do: callback @machine.after_failure on: :park, do: callback @machine.after_failure on: :ignite, do: callback @transition.run_callbacks { { success: false } } assert_equal 2, @count end def test_should_run_if_not_successful @machine.after_failure { |_object| @run = true } @transition.run_callbacks { { success: false } } assert @run end def test_should_not_run_if_successful @run = false @machine.after_failure { |_object| @run = true } @transition.run_callbacks { { success: true } } refute @run end def test_should_pass_transition_as_argument @machine.after_failure { |*args| @args = args } @transition.run_callbacks { { success: false } } assert_equal [@object, @transition], @args end def test_should_catch_halts @machine.after_failure { throw :halt } result = @transition.run_callbacks { { success: false } } assert result end def test_should_not_catch_exceptions @machine.after_failure { raise ArgumentError } assert_raises(ArgumentError) { @transition.run_callbacks { { success: false } } } end def test_should_not_be_able_to_run_twice @count = 0 @machine.after_failure { @count += 1 } @transition.run_callbacks { { success: false } } @transition.run_callbacks { { success: false } } assert_equal 1, @count end def test_should_not_be_able_to_run_twice_if_halted @count = 0 @machine.after_failure do @count += 1 throw :halt end @transition.run_callbacks { { success: false } } @transition.run_callbacks { { success: false } } assert_equal 1, @count end def test_should_be_able_to_run_again_after_resetting @count = 0 @machine.after_failure { @count += 1 } @transition.run_callbacks { { success: false } } @transition.reset @transition.run_callbacks { { success: false } } assert_equal 2, @count end end state_machines-0.100.4/test/unit/transition/transition_with_fiber_disabled_test.rb000066400000000000000000000103711507333401300306030ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithFiberDisabledTest < StateMachinesTest def setup @klass = Class.new do attr_reader :callbacks def initialize @callbacks = [] end end @machine = StateMachines::Machine.new(@klass, initial: :parked) @machine.state :idling @machine.event :ignite @object = @klass.new @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_run_callbacks_synchronously_with_fiber_false @machine.before_transition do @object.callbacks << :before end @machine.after_transition do @object.callbacks << :after end result = @transition.run_callbacks(fiber: false) { { success: true } } assert result assert_equal %i[before after], @object.callbacks refute_predicate @transition, :paused? end def test_should_not_support_pause_with_fiber_false @machine.around_transition do |block| @object.callbacks << :around_before # This pause should be ignored when fiber: false @transition.send(:pause) @object.callbacks << :around_after_pause block.call @object.callbacks << :around_after end result = @transition.run_callbacks(fiber: false) { { success: true } } assert result # All callbacks should execute in order without pausing assert_equal %i[around_before around_after_pause around_after], @object.callbacks refute_predicate @transition, :paused? end def test_should_handle_exceptions_without_fiber @machine.before_transition do @object.callbacks << :before raise 'Test error' end assert_raises(RuntimeError) do @transition.run_callbacks(fiber: false) { { success: true } } end assert_equal [:before], @object.callbacks refute_predicate @transition, :paused? end def test_should_handle_halted_callbacks_without_fiber @machine.before_transition do @object.callbacks << :before throw :halt end @machine.after_transition do @object.callbacks << :after end result = @transition.run_callbacks(fiber: false) { { success: true } } refute result assert_equal [:before], @object.callbacks refute_predicate @transition, :paused? end def test_should_handle_nested_around_callbacks_without_fiber @machine.around_transition do |block| @object.callbacks << :around_1_before block.call @object.callbacks << :around_1_after end @machine.around_transition do |block| @object.callbacks << :around_2_before block.call @object.callbacks << :around_2_after end result = @transition.run_callbacks(fiber: false) do @object.callbacks << :action { success: true } end assert result assert_equal %i[around_1_before around_2_before action around_2_after around_1_after], @object.callbacks refute_predicate @transition, :paused? end def test_should_not_create_fiber_when_disabled # Track fiber creation by checking if pause is ever available fiber_created = false @machine.around_transition do |block| # In fiber mode, this would create a fiber and allow pausing # With fiber: false, @paused_fiber should never be set fiber_created = true if @transition.instance_variable_get(:@paused_fiber) block.call end @transition.run_callbacks(fiber: false) { { success: true } } refute fiber_created, 'Fiber should not be created when fiber: false' end def test_fiber_option_should_be_passed_through_transition_collection # This test verifies the integration between TransitionCollection and Transition @transitions = [@transition] @collection = StateMachines::TransitionCollection.new(@transitions, fiber: false) @machine.around_transition do |block| @object.callbacks << :around_before # This pause should be ignored @transition.send(:pause) @object.callbacks << :around_after_pause block.call end # Use perform method which properly initializes the collection @collection.perform { { success: true } } # Should execute all callbacks without pausing assert_equal %i[around_before around_after_pause], @object.callbacks refute_predicate @transition, :paused? end end state_machines-0.100.4/test/unit/transition/transition_with_fiber_exceptions_test.rb000066400000000000000000000117341507333401300312210ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithFiberExceptionsTest < StateMachinesTest def setup @klass = Class.new do attr_reader :callbacks def initialize @callbacks = [] end end @machine = StateMachines::Machine.new(@klass, initial: :parked) @machine.state :idling @machine.event :ignite @object = @klass.new @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_catch_and_reraise_exception_in_before_callback @exception = RuntimeError.new('callback error') @machine.before_transition do @object.callbacks << :before raise @exception end assert_raises(RuntimeError) do @transition.run_callbacks { { success: true } } end # Should have cleaned up the paused fiber refute_predicate @transition, :paused? end def test_should_catch_and_reraise_exception_in_after_callback @exception = RuntimeError.new('after callback error') @machine.after_transition do @object.callbacks << :after raise @exception end assert_raises(RuntimeError) do @transition.run_callbacks { { success: true } } end # Should have cleaned up the paused fiber refute_predicate @transition, :paused? end def test_should_catch_and_reraise_exception_in_around_callback_before_yield @exception = RuntimeError.new('around before error') @machine.around_transition do |_block| @object.callbacks << :around_before raise @exception end assert_raises(RuntimeError) do @transition.run_callbacks { { success: true } } end # Should have cleaned up the paused fiber refute_predicate @transition, :paused? end def test_should_catch_and_reraise_exception_in_around_callback_after_yield @exception = RuntimeError.new('around after error') @machine.around_transition do |block| @object.callbacks << :around_before block.call @object.callbacks << :around_after raise @exception end assert_raises(RuntimeError) do @transition.run_callbacks { { success: true } } end # Should have cleaned up the paused fiber refute_predicate @transition, :paused? end def test_should_catch_and_reraise_exception_in_action_block @exception = RuntimeError.new('action error') assert_raises(RuntimeError) do @transition.run_callbacks do @object.callbacks << :action raise @exception end end # Should have cleaned up the paused fiber refute_predicate @transition, :paused? # Should have executed callbacks before the exception assert_includes @object.callbacks, :action end def test_should_catch_and_reraise_exception_when_resuming_paused_transition @exception = RuntimeError.new('resume error') @machine.around_transition do |block| @object.callbacks << :around_before_1 block.call @object.callbacks << :around_after_1 end @machine.around_transition do |block| @object.callbacks << :around_before_2 block.call @object.callbacks << :around_after_2 raise @exception end # First perform with after: false to pause @transition.run_callbacks(after: false) { { success: true } } assert_predicate @transition, :paused? assert_equal %i[around_before_1 around_before_2], @object.callbacks # Resume should catch and reraise the exception assert_raises(RuntimeError) do @transition.run_callbacks(after: true) end # Should have cleaned up the paused fiber refute_predicate @transition, :paused? # The exception is raised AFTER :around_after_2 is added, so we see it in callbacks # But :around_after_1 won't execute because the exception prevents the outer callback from completing assert_equal %i[around_before_1 around_before_2 around_after_2], @object.callbacks end def test_should_preserve_exception_type_and_message @exception = ArgumentError.new('specific error message') @machine.before_transition do raise @exception end begin @transition.run_callbacks { { success: true } } flunk 'Expected exception to be raised' rescue StandardError => e assert_instance_of ArgumentError, e assert_equal 'specific error message', e.message end end def test_should_clean_up_fiber_state_on_exception @exception = RuntimeError.new('cleanup test') @machine.around_transition do |block| @object.callbacks << :around_before block.call raise @exception end # Pause the transition @transition.run_callbacks(after: false) { { success: true } } assert_predicate @transition, :paused? # Resume with exception assert_raises(RuntimeError) do @transition.run_callbacks(after: true) end # Verify state is cleaned up refute_predicate @transition, :paused? assert_nil @transition.instance_variable_get(:@paused_fiber) refute @transition.instance_variable_get(:@resuming) end end state_machines-0.100.4/test/unit/transition/transition_with_invalid_nodes_test.rb000066400000000000000000000017561507333401300305120ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithInvalidNodesTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' end def test_should_raise_exception_without_event assert_raises(IndexError) { StateMachines::Transition.new(@object, @machine, nil, :parked, :idling) } end def test_should_raise_exception_with_invalid_event assert_raises(IndexError) { StateMachines::Transition.new(@object, @machine, :invalid, :parked, :idling) } end def test_should_raise_exception_with_invalid_from_state assert_raises(IndexError) { StateMachines::Transition.new(@object, @machine, :ignite, :invalid, :idling) } end def test_should_raise_exception_with_invalid_to_state assert_raises(IndexError) { StateMachines::Transition.new(@object, @machine, :ignite, :parked, :invalid) } end end state_machines-0.100.4/test/unit/transition/transition_with_mixed_callbacks_test.rb000066400000000000000000000117101507333401300307700ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithMixedCallbacksTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_before_and_around_callbacks_in_order_defined @callbacks = [] @machine.before_transition { @callbacks << :before_1 } @machine.around_transition do |block| @callbacks << :around block.call end @machine.before_transition { @callbacks << :before_2 } assert @transition.run_callbacks assert_equal %i[before_1 around before_2], @callbacks end def test_should_run_around_callbacks_before_after_callbacks @callbacks = [] @machine.after_transition { @callbacks << :after_1 } @machine.around_transition do |block| block.call @callbacks << :after_2 end @machine.after_transition { @callbacks << :after_3 } assert @transition.run_callbacks assert_equal %i[after_2 after_1 after_3], @callbacks end def test_should_have_access_to_result_for_both_after_and_around_callbacks @machine.after_transition { @after_result = @transition.result } @machine.around_transition do |block| block.call @around_result = @transition.result end @transition.run_callbacks { { result: 1, success: true } } assert_equal 1, @after_result assert_equal 1, @around_result end def test_should_not_run_further_callbacks_if_before_callback_halts @callbacks = [] @machine.before_transition { @callbacks << :before_1 } @machine.around_transition do |block| @callbacks << :before_around_1 block.call @callbacks << :after_around_1 end @machine.before_transition do @callbacks << :before_2 throw :halt end @machine.around_transition do |block| @callbacks << :before_around_2 block.call @callbacks << :after_around_2 end @machine.after_transition { @callbacks << :after } refute @transition.run_callbacks assert_equal %i[before_1 before_around_1 before_2], @callbacks end def test_should_not_run_further_callbacks_if_before_yield_halts @callbacks = [] @machine.before_transition { @callbacks << :before_1 } @machine.around_transition do |_block| @callbacks << :before_around_1 throw :halt end @machine.before_transition do @callbacks << :before_2 throw :halt end @machine.around_transition do |block| @callbacks << :before_around_2 block.call @callbacks << :after_around_2 end @machine.after_transition { @callbacks << :after } refute @transition.run_callbacks assert_equal %i[before_1 before_around_1], @callbacks end def test_should_not_run_further_callbacks_if_around_callback_fails_to_yield @callbacks = [] @machine.before_transition { @callbacks << :before_1 } @machine.around_transition { |_block| @callbacks << :before_around_1 } @machine.before_transition do @callbacks << :before_2 throw :halt end @machine.around_transition do |block| @callbacks << :before_around_2 block.call @callbacks << :after_around_2 end @machine.after_transition { @callbacks << :after } refute @transition.run_callbacks assert_equal %i[before_1 before_around_1], @callbacks end def test_should_not_run_further_callbacks_if_after_yield_halts @callbacks = [] @machine.before_transition { @callbacks << :before_1 } @machine.around_transition do |block| @callbacks << :before_around_1 block.call @callbacks << :after_around_1 throw :halt end @machine.before_transition { @callbacks << :before_2 } @machine.around_transition do |block| @callbacks << :before_around_2 block.call @callbacks << :after_around_2 end @machine.after_transition { @callbacks << :after } assert @transition.run_callbacks assert_equal %i[before_1 before_around_1 before_2 before_around_2 after_around_2 after_around_1], @callbacks end def test_should_not_run_further_callbacks_if_after_callback_halts @callbacks = [] @machine.before_transition { @callbacks << :before_1 } @machine.around_transition do |block| @callbacks << :before_around_1 block.call @callbacks << :after_around_1 end @machine.before_transition { @callbacks << :before_2 } @machine.around_transition do |block| @callbacks << :before_around_2 block.call @callbacks << :after_around_2 end @machine.after_transition do @callbacks << :after_1 throw :halt end @machine.after_transition { @callbacks << :after_2 } assert @transition.run_callbacks assert_equal %i[before_1 before_around_1 before_2 before_around_2 after_around_2 after_around_1 after_1], @callbacks end end state_machines-0.100.4/test/unit/transition/transition_with_multiple_after_callbacks_test.rb000066400000000000000000000021571507333401300327030ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithMultipleAfterCallbacksTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_run_in_the_order_they_were_defined @callbacks = [] @machine.after_transition { @callbacks << 1 } @machine.after_transition { @callbacks << 2 } @transition.run_callbacks assert_equal [1, 2], @callbacks end def test_should_not_run_further_callbacks_if_halted @callbacks = [] @machine.after_transition do @callbacks << 1 throw :halt end @machine.after_transition { @callbacks << 2 } assert @transition.run_callbacks assert_equal [1], @callbacks end def test_should_fail_if_any_callback_halted @machine.after_transition { true } @machine.after_transition { throw :halt } assert @transition.run_callbacks end end state_machines-0.100.4/test/unit/transition/transition_with_multiple_around_callbacks_test.rb000066400000000000000000000110641507333401300330670ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithMultipleAroundCallbacksTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_before_yield_in_the_order_they_were_defined @callbacks = [] @machine.around_transition do |block| @callbacks << 1 block.call end @machine.around_transition do |block| @callbacks << 2 block.call end @transition.run_callbacks assert_equal [1, 2], @callbacks end def test_should_before_yield_multiple_methods_in_the_order_they_were_defined @callbacks = [] @machine.around_transition(lambda { |block| @callbacks << 1 block.call }, lambda { |block| @callbacks << 2 block.call }) @machine.around_transition(lambda { |block| @callbacks << 3 block.call }, lambda { |block| @callbacks << 4 block.call }) @transition.run_callbacks assert_equal [1, 2, 3, 4], @callbacks end def test_should_after_yield_in_the_reverse_order_they_were_defined @callbacks = [] @machine.around_transition do |block| block.call @callbacks << 1 end @machine.around_transition do |block| block.call @callbacks << 2 end @transition.run_callbacks assert_equal [2, 1], @callbacks end def test_should_after_yield_multiple_methods_in_the_reverse_order_they_were_defined @callbacks = [] @machine.around_transition(lambda { |block| block.call @callbacks << 1 }) do |block| block.call @callbacks << 2 end @machine.around_transition(lambda { |block| block.call @callbacks << 3 }) do |block| block.call @callbacks << 4 end @transition.run_callbacks assert_equal [4, 3, 2, 1], @callbacks end def test_should_run_block_between_callback @callbacks = [] @machine.around_transition do |block| @callbacks << :before_1 block.call @callbacks << :after_1 end @machine.around_transition do |block| @callbacks << :before_2 block.call @callbacks << :after_2 end @transition.run_callbacks do @callbacks << :within { success: true } end assert_equal %i[before_1 before_2 within after_2 after_1], @callbacks end def test_should_have_access_to_result_after_yield @machine.around_transition do |block| @before_result_1 = @transition.result block.call @after_result_1 = @transition.result end @machine.around_transition do |block| @before_result_2 = @transition.result block.call @after_result_2 = @transition.result end @transition.run_callbacks { { result: 1, success: true } } assert_nil @before_result_1 assert_nil @before_result_2 assert_equal 1, @after_result_1 assert_equal 1, @after_result_2 end def test_should_fail_if_any_before_yield_halted @machine.around_transition { |block| block.call } @machine.around_transition { throw :halt } refute @transition.run_callbacks end def test_should_not_continue_around_callbacks_if_before_yield_halted @callbacks = [] @machine.around_transition do @callbacks << 1 throw :halt end @machine.around_transition do |block| @callbacks << 2 block.call @callbacks << 3 end refute @transition.run_callbacks assert_equal [1], @callbacks end def test_should_not_continue_around_callbacks_if_later_before_yield_halted @callbacks = [] @machine.around_transition do |block| block.call @callbacks << 1 end @machine.around_transition { throw :halt } @transition.run_callbacks assert_empty @callbacks end def test_should_not_run_further_callbacks_if_after_yield_halted @callbacks = [] @machine.around_transition do |block| block.call @callbacks << 1 end @machine.around_transition do |block| block.call throw :halt end assert @transition.run_callbacks assert_empty @callbacks end def test_should_fail_if_any_fail_to_yield @callbacks = [] @machine.around_transition { @callbacks << 1 } @machine.around_transition do |block| @callbacks << 2 block.call @callbacks << 3 end refute @transition.run_callbacks assert_equal [1], @callbacks end end state_machines-0.100.4/test/unit/transition/transition_with_multiple_before_callbacks_test.rb000066400000000000000000000021661507333401300330440ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithMultipleBeforeCallbacksTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_run_in_the_order_they_were_defined @callbacks = [] @machine.before_transition { @callbacks << 1 } @machine.before_transition { @callbacks << 2 } @transition.run_callbacks assert_equal [1, 2], @callbacks end def test_should_not_run_further_callbacks_if_halted @callbacks = [] @machine.before_transition do @callbacks << 1 throw :halt end @machine.before_transition { @callbacks << 2 } refute @transition.run_callbacks assert_equal [1], @callbacks end def test_should_fail_if_any_callback_halted @machine.before_transition { true } @machine.before_transition { throw :halt } refute @transition.run_callbacks end end state_machines-0.100.4/test/unit/transition/transition_with_multiple_failure_callbacks_test.rb000066400000000000000000000022461507333401300332300ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithMultipleFailureCallbacksTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_run_in_the_order_they_were_defined @callbacks = [] @machine.after_failure { @callbacks << 1 } @machine.after_failure { @callbacks << 2 } @transition.run_callbacks { { success: false } } assert_equal [1, 2], @callbacks end def test_should_not_run_further_callbacks_if_halted @callbacks = [] @machine.after_failure do @callbacks << 1 throw :halt end @machine.after_failure { @callbacks << 2 } assert(@transition.run_callbacks { { success: false } }) assert_equal [1], @callbacks end def test_should_fail_if_any_callback_halted @machine.after_failure { true } @machine.after_failure { throw :halt } assert(@transition.run_callbacks { { success: false } }) end end state_machines-0.100.4/test/unit/transition/transition_with_namespace_test.rb000066400000000000000000000023071507333401300276210ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithNamespaceTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, namespace: 'alarm') @machine.state :off, :active @machine.event :activate @object = @klass.new @object.state = 'off' @transition = StateMachines::Transition.new(@object, @machine, :activate, :off, :active) end def test_should_have_an_event assert_equal :activate, @transition.event end def test_should_have_a_qualified_event assert_equal :activate_alarm, @transition.qualified_event end def test_should_have_a_from_name assert_equal :off, @transition.from_name end def test_should_have_a_qualified_from_name assert_equal :alarm_off, @transition.qualified_from_name end def test_should_have_a_human_from_name assert_equal 'off', @transition.human_from_name end def test_should_have_a_to_name assert_equal :active, @transition.to_name end def test_should_have_a_qualified_to_name assert_equal :alarm_active, @transition.qualified_to_name end def test_should_have_a_human_to_name assert_equal 'active', @transition.human_to_name end end state_machines-0.100.4/test/unit/transition/transition_with_perform_arguments_test.rb000066400000000000000000000014721507333401300314260ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithPerformArgumentsTest < StateMachinesTest def setup @klass = Class.new do attr_reader :saved def save @saved = true end end @machine = StateMachines::Machine.new(@klass, action: :save) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_have_arguments @transition.perform(1, 2) assert_equal [1, 2], @transition.args assert @object.saved end def test_should_not_include_run_action_in_arguments @transition.perform(1, 2, false) assert_equal [1, 2], @transition.args refute @object.saved end end state_machines-0.100.4/test/unit/transition/transition_with_transactions_test.rb000066400000000000000000000017661507333401300304050ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithTransactionsTest < StateMachinesTest def setup @klass = Class.new do class << self attr_accessor :running_transaction end attr_accessor :result def save @result = self.class.running_transaction true end end @machine = StateMachines::Machine.new(@klass, action: :save) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) class << @machine def within_transaction(object) owner_class.running_transaction = object yield owner_class.running_transaction = false end end end def test_should_run_blocks_within_transaction_for_object @transition.within_transaction do @result = @klass.running_transaction end assert_equal @object, @result end end state_machines-0.100.4/test/unit/transition/transition_without_callbacks_test.rb000066400000000000000000000015551507333401300303400ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithoutCallbacksTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def test_should_succeed assert @transition.run_callbacks end def test_should_succeed_if_after_callbacks_skipped assert @transition.run_callbacks(after: false) end def test_should_call_block_if_provided @transition.run_callbacks do @ran_block = true {} end assert @ran_block end def test_should_track_block_result @transition.run_callbacks { { result: 1 } } assert_equal 1, @transition.result end end state_machines-0.100.4/test/unit/transition/transition_without_reading_state_test.rb000066400000000000000000000011421507333401300312220ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithoutReadingStateTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new @object.state = 'idling' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling, false) end def test_should_not_read_from_value_from_object assert_equal 'parked', @transition.from end def test_should_have_to_value assert_equal 'idling', @transition.to end end state_machines-0.100.4/test/unit/transition/transition_without_running_action_test.rb000066400000000000000000000020461507333401300314320ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionWithoutRunningActionTest < StateMachinesTest def setup @klass = Class.new do attr_reader :saved def save @saved = true end end @machine = StateMachines::Machine.new(@klass, action: :save) @machine.state :parked, :idling @machine.event :ignite @machine.after_transition { |_object| @run_after = true } @object = @klass.new @object.state = 'parked' @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) @result = @transition.perform(false) end def test_should_have_empty_args assert_empty @transition.args end def test_should_not_have_a_result assert_nil @transition.result end def test_should_be_successful assert @result end def test_should_change_the_current_state assert_equal 'idling', @object.state end def test_should_not_run_the_action refute @object.saved end def test_should_run_after_callbacks assert @run_after end end state_machines-0.100.4/test/unit/transition_collection/000077500000000000000000000000001507333401300232055ustar00rootroot00000000000000attribute_transition_collection_by_default_test.rb000066400000000000000000000010011507333401300353700ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionByDefaultTest < StateMachinesTest def setup @transitions = StateMachines::AttributeTransitionCollection.new end def test_should_skip_actions assert @transitions.skip_actions end def test_should_not_skip_after refute @transitions.skip_after end def test_should_not_use_transaction refute @transitions.use_transactions end def test_should_be_empty assert_empty @transitions end end attribute_transition_collection_marshalling_test.rb000066400000000000000000000036351507333401300355720ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionMarshallingTest < StateMachinesTest def setup @klass = Class.new self.class.const_set('Example', @klass) @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @object = @klass.new @object.state_event = 'ignite' end def teardown self.class.send(:remove_const, 'Example') end def test_should_marshal_during_before_callbacks @machine.before_transition { |object, _transition| Marshal.dump(object) } transitions(after: false).perform { true } transitions.perform { true } end def test_should_marshal_during_action transitions(after: false).perform do Marshal.dump(@object) true end transitions.perform do Marshal.dump(@object) true end end def test_should_marshal_during_after_callbacks @machine.after_transition { |object, _transition| Marshal.dump(object) } transitions(after: false).perform { true } transitions.perform { true } end def test_should_marshal_during_around_callbacks_before_yield @machine.around_transition do |object, _transition, block| Marshal.dump(object) block.call end transitions(after: false).perform { true } transitions.perform { true } end def test_should_marshal_during_around_callbacks_after_yield @machine.around_transition do |object, _transition, block| block.call Marshal.dump(object) end transitions(after: false).perform { true } transitions.perform { true } end private def transitions(options = {}) StateMachines::AttributeTransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ], options) end end attribute_transition_collection_with_action_error_test.rb000066400000000000000000000030371507333401300370060ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithActionErrorTest < StateMachinesTest def setup @klass = Class.new @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @state.state :idling @state.event :ignite @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save) @status.state :second_gear @status.event :shift_up @object = @klass.new @object.state_event = 'ignite' @object.status_event = 'shift_up' @transitions = StateMachines::AttributeTransitionCollection.new([ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling), @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear) ]) begin @transitions.perform { raise ArgumentError } rescue StandardError end end def test_should_not_persist_states assert_equal 'parked', @object.state assert_equal 'first_gear', @object.status end def test_should_not_clear_events assert_equal :ignite, @object.state_event assert_equal :shift_up, @object.status_event end def test_should_not_write_event_transitions assert_nil @object.send(:state_event_transition) assert_nil @object.send(:status_event_transition) end end attribute_transition_collection_with_action_failed_test.rb000066400000000000000000000030461507333401300371010ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithActionFailedTest < StateMachinesTest def setup @klass = Class.new @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @state.state :idling @state.event :ignite @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save) @status.state :second_gear @status.event :shift_up @object = @klass.new @object.state_event = 'ignite' @object.status_event = 'shift_up' @transitions = StateMachines::AttributeTransitionCollection.new([ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling), @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear) ]) @result = @transitions.perform { false } end def test_should_not_succeed refute @result end def test_should_not_persist_states assert_equal 'parked', @object.state assert_equal 'first_gear', @object.status end def test_should_not_clear_events assert_equal :ignite, @object.state_event assert_equal :shift_up, @object.status_event end def test_should_not_write_event_transitions assert_nil @object.send(:state_event_transition) assert_nil @object.send(:status_event_transition) end end attribute_transition_collection_with_after_callback_error_test.rb000066400000000000000000000017331507333401300404470ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithBeforeCallbackErrorTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @machine.before_transition { raise ArgumentError } @object = @klass.new @object.state_event = 'ignite' @transitions = StateMachines::AttributeTransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) begin @transitions.perform rescue StandardError end end def test_should_not_clear_event assert_equal :ignite, @object.state_event end def test_should_not_write_event_transition assert_nil @object.send(:state_event_transition) end end attribute_transition_collection_with_after_callback_halt_test.rb000066400000000000000000000017471507333401300402530ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithBeforeCallbackHaltTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @machine.before_transition { throw :halt } @object = @klass.new @object.state_event = 'ignite' @transitions = StateMachines::AttributeTransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) @result = @transitions.perform end def test_should_not_succeed refute @result end def test_should_not_clear_event assert_equal :ignite, @object.state_event end def test_should_not_write_event_transition assert_nil @object.send(:state_event_transition) end end attribute_transition_collection_with_around_after_yield_callback_error_test.rb000066400000000000000000000017451507333401300432100ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithAroundAfterYieldCallbackErrorTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @machine.before_transition { raise ArgumentError } @object = @klass.new @object.state_event = 'ignite' @transitions = StateMachines::AttributeTransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) begin @transitions.perform rescue StandardError end end def test_should_not_clear_event assert_equal :ignite, @object.state_event end def test_should_not_write_event_transition assert_nil @object.send(:state_event_transition) end end attribute_transition_collection_with_around_callback_after_yield_error_test.rb000066400000000000000000000017741507333401300432120ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithAroundCallbackAfterYieldErrorTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @machine.around_transition do |block| block.call raise ArgumentError end @object = @klass.new @object.state_event = 'ignite' @transitions = StateMachines::AttributeTransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) begin @transitions.perform rescue StandardError end end def test_should_clear_event assert_nil @object.state_event end def test_should_not_write_event_transition assert_nil @object.send(:state_event_transition) end end attribute_transition_collection_with_around_callback_after_yield_halt_test.rb000066400000000000000000000020041507333401300427740ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithAroundCallbackAfterYieldHaltTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @machine.around_transition do |block| block.call throw :halt end @object = @klass.new @object.state_event = 'ignite' @transitions = StateMachines::AttributeTransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) @result = @transitions.perform end def test_should_succeed assert @result end def test_should_clear_event assert_nil @object.state_event end def test_should_not_write_event_transition assert_nil @object.send(:state_event_transition) end end attribute_transition_collection_with_around_callback_before_yield_halt_test.rb000066400000000000000000000017621507333401300431470ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithAroundCallbackBeforeYieldHaltTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @machine.around_transition { throw :halt } @object = @klass.new @object.state_event = 'ignite' @transitions = StateMachines::AttributeTransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) @result = @transitions.perform end def test_should_not_succeed refute @result end def test_should_not_clear_event assert_equal :ignite, @object.state_event end def test_should_not_write_event_transition assert_nil @object.send(:state_event_transition) end end attribute_transition_collection_with_before_callback_error_test.rb000066400000000000000000000017121507333401300406050ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithAfterCallbackErrorTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @machine.after_transition { raise ArgumentError } @object = @klass.new @object.state_event = 'ignite' @transitions = StateMachines::AttributeTransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) begin @transitions.perform rescue StandardError end end def test_should_clear_event assert_nil @object.state_event end def test_should_not_write_event_transition assert_nil @object.send(:state_event_transition) end end attribute_transition_collection_with_before_callback_halt_test.rb000066400000000000000000000017221507333401300404050ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithAfterCallbackHaltTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @machine.after_transition { throw :halt } @object = @klass.new @object.state_event = 'ignite' @transitions = StateMachines::AttributeTransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) @result = @transitions.perform end def test_should_succeed assert @result end def test_should_clear_event assert_nil @object.state_event end def test_should_not_write_event_transition assert_nil @object.send(:state_event_transition) end end attribute_transition_collection_with_callbacks_test.rb000066400000000000000000000054141507333401300362400ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithCallbacksTest < StateMachinesTest def setup @klass = Class.new @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @state.state :idling @state.event :ignite @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save) @status.state :second_gear @status.event :shift_up @object = @klass.new @transitions = StateMachines::AttributeTransitionCollection.new([ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling), @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear) ]) end def test_should_not_have_events_during_before_callbacks @state.before_transition { |object, _transition| @before_state_event = object.state_event } @state.around_transition do |object, _transition, block| @around_state_event = object.state_event block.call end @transitions.perform assert_nil @before_state_event assert_nil @around_state_event end def test_should_not_have_events_during_action @transitions.perform { @state_event = @object.state_event } assert_nil @state_event end def test_should_not_have_events_during_after_callbacks @state.after_transition { |object, _transition| @after_state_event = object.state_event } @state.around_transition do |object, _transition, block| block.call @around_state_event = object.state_event end @transitions.perform assert_nil @after_state_event assert_nil @around_state_event end def test_should_not_have_event_transitions_during_before_callbacks @state.before_transition { |object, _transition| @state_event_transition = object.send(:state_event_transition) } @transitions.perform assert_nil @state_event_transition end def test_should_not_have_event_transitions_during_action @transitions.perform { @state_event_transition = @object.send(:state_event_transition) } assert_nil @state_event_transition end def test_should_not_have_event_transitions_during_after_callbacks @state.after_transition { |object, _transition| @after_state_event_transition = object.send(:state_event_transition) } @state.around_transition do |object, _transition, block| block.call @around_state_event_transition = object.send(:state_event_transition) end @transitions.perform assert_nil @after_state_event_transition assert_nil @around_state_event_transition end end attribute_transition_collection_with_event_transitions_test.rb000066400000000000000000000025401507333401300400740ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithEventTransitionsTest < StateMachinesTest def setup @klass = Class.new @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @state.state :idling @state.event :ignite @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save) @status.state :second_gear @status.event :shift_up @object = @klass.new @object.send(:state_event_transition=, @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling)) @object.send(:status_event_transition=, @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)) @transitions = StateMachines::AttributeTransitionCollection.new([@state_transition, @status_transition]) @result = @transitions.perform end def test_should_succeed assert @result end def test_should_persist_states assert_equal 'idling', @object.state assert_equal 'second_gear', @object.status end def test_should_not_write_events assert_nil @object.state_event assert_nil @object.status_event end def test_should_clear_event_transitions assert_nil @object.send(:state_event_transition) assert_nil @object.send(:status_event_transition) end end attribute_transition_collection_with_events_test.rb000066400000000000000000000027631507333401300356310ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithEventsTest < StateMachinesTest def setup @klass = Class.new @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @state.state :idling @state.event :ignite @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save) @status.state :second_gear @status.event :shift_up @object = @klass.new @object.state_event = 'ignite' @object.status_event = 'shift_up' @transitions = StateMachines::AttributeTransitionCollection.new([ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling), @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear) ]) @result = @transitions.perform end def test_should_succeed assert @result end def test_should_persist_states assert_equal 'idling', @object.state assert_equal 'second_gear', @object.status end def test_should_clear_events assert_nil @object.state_event assert_nil @object.status_event end def test_should_not_write_event_transitions assert_nil @object.send(:state_event_transition) assert_nil @object.send(:status_event_transition) end end attribute_transition_collection_with_skipped_after_callbacks_test.rb000066400000000000000000000031661507333401300411420ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class AttributeTransitionCollectionWithSkippedAfterCallbacksTest < StateMachinesTest def setup @klass = Class.new @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @state.state :idling @state.event :ignite @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save) @status.state :second_gear @status.event :shift_up @object = @klass.new @object.state_event = 'ignite' @object.status_event = 'shift_up' @transitions = StateMachines::AttributeTransitionCollection.new([ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling), @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear) ], after: false) end def test_should_clear_events @transitions.perform assert_nil @object.state_event assert_nil @object.status_event end def test_should_write_event_transitions_if_success @transitions.perform { true } assert_equal @state_transition, @object.send(:state_event_transition) assert_equal @status_transition, @object.send(:status_event_transition) end def test_should_not_write_event_transitions_if_failed @transitions.perform { false } assert_nil @object.send(:state_event_transition) assert_nil @object.send(:status_event_transition) end end state_machines-0.100.4/test/unit/transition_collection/transition_collection_by_default_test.rb000066400000000000000000000007571507333401300334050ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionCollectionByDefaultTest < StateMachinesTest def setup @transitions = StateMachines::TransitionCollection.new end def test_should_not_skip_actions refute @transitions.skip_actions end def test_should_not_skip_after refute @transitions.skip_after end def test_should_use_transaction assert @transitions.use_transactions end def test_should_be_empty assert_empty @transitions end end transition_collection_empty_with_block_test.rb000066400000000000000000000012041507333401300345370ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionEmptyWithBlockTest < StateMachinesTest def setup @transitions = StateMachines::TransitionCollection.new end def test_should_raise_exception_if_perform_raises_exception assert_raises(ArgumentError) { @transitions.perform { raise ArgumentError } } end def test_should_use_block_result_if_non_boolean assert_equal(1, @transitions.perform { 1 }) end def test_should_use_block_result_if_false refute(@transitions.perform { false }) end def test_should_use_block_reslut_if_nil assert_nil(@transitions.perform { nil }) end end transition_collection_empty_without_block_test.rb000066400000000000000000000004421507333401300352720ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionEmptyWithoutBlockTest < StateMachinesTest def setup @transitions = StateMachines::TransitionCollection.new @result = @transitions.perform end def test_should_succeed assert @result end end state_machines-0.100.4/test/unit/transition_collection/transition_collection_invalid_test.rb000066400000000000000000000007301507333401300327040ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionCollectionInvalidTest < StateMachinesTest def setup @transitions = StateMachines::TransitionCollection.new([false]) end def test_should_be_empty assert_empty @transitions end def test_should_not_succeed refute @transitions.perform end def test_should_not_run_perform_block ran_block = false @transitions.perform { ran_block = true } refute ran_block end end state_machines-0.100.4/test/unit/transition_collection/transition_collection_partial_invalid_test.rb000066400000000000000000000036161507333401300344260ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionCollectionPartialInvalidTest < StateMachinesTest def setup @klass = Class.new do attr_accessor :ran_transaction end @callbacks = [] @machine = StateMachines::Machine.new(@klass, initial: :parked) @machine.state :idling @machine.event :ignite @machine.before_transition { @callbacks << :before } @machine.after_transition { @callbacks << :after } @machine.around_transition do |block| @callbacks << :around_before block.call @callbacks << :around_after end class << @machine def within_transaction(object) object.ran_transaction = true end end @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling), false ]) end def test_should_not_store_invalid_values assert_equal 1, @transitions.length end def test_should_not_succeed refute @transitions.perform end def test_should_not_start_transaction refute @object.ran_transaction end def test_should_not_run_perform_block ran_block = false @transitions.perform { ran_block = true } refute ran_block end def test_should_not_run_before_callbacks refute_includes @callbacks, :before end def test_should_not_persist_states assert_equal 'parked', @object.state end def test_should_not_run_after_callbacks refute_includes @callbacks, :after end def test_should_not_run_around_callbacks_before_yield refute_includes @callbacks, :around_before end def test_should_not_run_around_callbacks_after_yield refute_includes @callbacks, :around_after end end state_machines-0.100.4/test/unit/transition_collection/transition_collection_test.rb000066400000000000000000000022671507333401300312050ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionCollectionTest < StateMachinesTest def test_should_raise_exception_if_invalid_option_specified exception = assert_raises(ArgumentError) { StateMachines::TransitionCollection.new([], invalid: true) } assert_equal 'Unknown key: :invalid. Valid keys are: :actions, :after, :use_transactions, :fiber', exception.message end def test_should_raise_exception_if_multiple_transitions_for_same_attribute_specified @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked) @machine.state :parked, :idling @machine.event :ignite @object = @klass.new exception = assert_raises(ArgumentError) do StateMachines::TransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling), StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) end assert_equal 'Cannot perform multiple transitions in parallel for the same state machine attribute', exception.message end end state_machines-0.100.4/test/unit/transition_collection/transition_collection_valid_test.rb000066400000000000000000000031351507333401300323570ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionCollectionValidTest < StateMachinesTest def setup @klass = Class.new do attr_reader :persisted def initialize @persisted = nil super @persisted = [] end def state=(value) @persisted << 'state' if @persisted @state = value end def status=(value) @persisted << 'status' if @persisted @status = value end end @state = StateMachines::Machine.new(@klass, initial: :parked) @state.state :idling @state.event :ignite @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear) @status.state :second_gear @status.event :shift_up @object = @klass.new @result = StateMachines::TransitionCollection.new([ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling), @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear) ]).perform end def test_should_succeed assert @result end def test_should_persist_each_state assert_equal 'idling', @object.state assert_equal 'second_gear', @object.status end def test_should_persist_in_order assert_equal %w[state status], @object.persisted end def test_should_store_results_in_transitions assert_nil @state_transition.result assert_nil @status_transition.result end end transition_collection_with_action_error_test.rb000066400000000000000000000033571507333401300347300ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithActionErrorTest < StateMachinesTest def setup @klass = Class.new do def save raise ArgumentError end end @before_count = 0 @around_before_count = 0 @after_count = 0 @around_after_count = 0 @failure_count = 0 @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @machine.before_transition { @before_count += 1 } @machine.after_transition { @after_count += 1 } @machine.around_transition do |block| @around_before_count += 1 block.call @around_after_count += 1 end @machine.after_failure { @failure_count += 1 } @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) @raised = true begin @transitions.perform @raised = false rescue ArgumentError end end def test_should_not_catch_exception assert @raised end def test_should_not_persist_state assert_equal 'parked', @object.state end def test_should_run_before_callbacks assert_equal 1, @before_count end def test_should_run_around_callbacks_before_yield assert_equal 1, @around_before_count end def test_should_not_run_after_callbacks assert_equal 0, @after_count end def test_should_not_run_around_callbacks_after_yield assert_equal 0, @around_after_count end def test_should_not_run_failure_callbacks assert_equal 0, @failure_count end end transition_collection_with_action_failed_test.rb000066400000000000000000000031751507333401300350210ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithActionFailedTest < StateMachinesTest def setup @klass = Class.new do def save false end end @before_count = 0 @around_before_count = 0 @after_count = 0 @around_after_count = 0 @failure_count = 0 @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @machine.before_transition { @before_count += 1 } @machine.after_transition { @after_count += 1 } @machine.around_transition do |block| @around_before_count += 1 block.call @around_after_count += 1 end @machine.after_failure { @failure_count += 1 } @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) @result = @transitions.perform end def test_should_not_succeed refute @result end def test_should_not_persist_state assert_equal 'parked', @object.state end def test_should_run_before_callbacks assert_equal 1, @before_count end def test_should_run_around_callbacks_before_yield assert_equal 1, @around_before_count end def test_should_not_run_after_callbacks assert_equal 0, @after_count end def test_should_not_run_around_callbacks assert_equal 0, @around_after_count end def test_should_run_failure_callbacks assert_equal 1, @failure_count end end transition_collection_with_action_hook_and_block_test.rb000066400000000000000000000007071507333401300365270ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' require_relative 'transition_collection_with_action_hook_base_test' class TransitionCollectionWithActionHookAndBlockTest < TransitionCollectionWithActionHookBaseTest def setup super @result = StateMachines::TransitionCollection.new([@transition]).perform { true } end def test_should_succeed assert @result end def test_should_not_run_action refute @object.saved end end transition_collection_with_action_hook_and_skipped_action_test.rb000066400000000000000000000007261507333401300404320ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' require_relative 'transition_collection_with_action_hook_base_test' class TransitionCollectionWithActionHookAndSkippedActionTest < TransitionCollectionWithActionHookBaseTest def setup super @result = StateMachines::TransitionCollection.new([@transition], actions: false).perform end def test_should_succeed assert @result end def test_should_not_run_action refute @object.saved end end transition_collection_with_action_hook_and_skipped_after_callbacks_test.rb000066400000000000000000000017271507333401300422570ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' require_relative 'transition_collection_with_action_hook_base_test' class TransitionCollectionWithActionHookAndSkippedAfterCallbacksTest < TransitionCollectionWithActionHookBaseTest def setup super @result = StateMachines::TransitionCollection.new([@transition], after: false).perform end def test_should_succeed assert @result end def test_should_run_action assert @object.saved end def test_should_have_already_persisted_when_running_action assert_equal 'idling', @object.state_on_save end def test_should_not_have_event_during_action assert_nil @object.state_event_on_save end def test_should_not_write_event assert_nil @object.state_event end def test_should_not_have_event_transition_during_save assert_nil @object.state_event_transition_on_save end def test_should_not_write_event_attribute assert_nil @object.send(:state_event_transition) end end transition_collection_with_action_hook_base_test.rb000066400000000000000000000015231507333401300355220ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithActionHookBaseTest < StateMachinesTest def setup @superclass = Class.new do def save true end end @klass = Class.new(@superclass) do attr_reader :saved, :state_on_save, :state_event_on_save, :state_event_transition_on_save def save @saved = true @state_on_save = state @state_event_on_save = state_event @state_event_transition_on_save = state_event_transition super end end @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @object = @klass.new @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) end def default_test; end end transition_collection_with_action_hook_error_test.rb000066400000000000000000000012261507333401300357410ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' require_relative 'transition_collection_with_action_hook_base_test' class TransitionCollectionWithActionHookErrorTest < TransitionCollectionWithActionHookBaseTest def setup super @superclass.class_eval do remove_method :save def save raise ArgumentError end end begin StateMachines::TransitionCollection.new([@transition]).perform rescue StandardError end end def test_should_not_write_event assert_nil @object.state_event end def test_should_not_write_event_transition assert_nil @object.send(:state_event_transition) end end transition_collection_with_action_hook_invalid_test.rb000066400000000000000000000007061507333401300362400ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' require_relative 'transition_collection_with_action_hook_base_test' class TransitionCollectionWithActionHookInvalidTest < TransitionCollectionWithActionHookBaseTest def setup super @result = StateMachines::TransitionCollection.new([@transition, nil]).perform end def test_should_not_succeed refute @result end def test_should_not_run_action refute @object.saved end end transition_collection_with_action_hook_multiple_test.rb000066400000000000000000000047051507333401300364500ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' require_relative 'transition_collection_with_action_hook_base_test' class TransitionCollectionWithActionHookMultipleTest < TransitionCollectionWithActionHookBaseTest def setup super @status_machine = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save) @status_machine.state :second_gear @status_machine.event :shift_up @klass.class_eval do attr_reader :status_on_save, :status_event_on_save, :status_event_transition_on_save remove_method :save def save @saved = true @state_on_save = state @state_event_on_save = state_event @state_event_transition_on_save = state_event_transition @status_on_save = status @status_event_on_save = status_event @status_event_transition_on_save = status_event_transition super 1 end end @object = @klass.new @state_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) @status_transition = StateMachines::Transition.new(@object, @status_machine, :shift_up, :first_gear, :second_gear) @result = StateMachines::TransitionCollection.new([@state_transition, @status_transition]).perform end def test_should_succeed assert_equal 1, @result end def test_should_run_action assert @object.saved end def test_should_not_have_already_persisted_when_running_action assert_equal 'parked', @object.state_on_save assert_equal 'first_gear', @object.status_on_save end def test_should_persist assert_equal 'idling', @object.state assert_equal 'second_gear', @object.status end def test_should_not_have_events_during_action assert_nil @object.state_event_on_save assert_nil @object.status_event_on_save end def test_should_not_write_events assert_nil @object.state_event assert_nil @object.status_event end def test_should_have_event_transitions_during_action assert_equal @state_transition, @object.state_event_transition_on_save assert_equal @status_transition, @object.status_event_transition_on_save end def test_should_not_write_event_transitions assert_nil @object.send(:state_event_transition) assert_nil @object.send(:status_event_transition) end def test_should_mark_event_transitions_as_transient assert_predicate @state_transition, :transient? assert_predicate @status_transition, :transient? end end transition_collection_with_action_hook_test.rb000066400000000000000000000021671507333401300345350ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' require_relative 'transition_collection_with_action_hook_base_test' class TransitionCollectionWithActionHookTest < TransitionCollectionWithActionHookBaseTest def setup super @result = StateMachines::TransitionCollection.new([@transition]).perform end def test_should_succeed assert @result end def test_should_run_action assert @object.saved end def test_should_not_have_already_persisted_when_running_action assert_equal 'parked', @object.state_on_save end def test_should_persist assert_equal 'idling', @object.state end def test_should_not_have_event_during_action assert_nil @object.state_event_on_save end def test_should_not_write_event assert_nil @object.state_event end def test_should_have_event_transition_during_action assert_equal @transition, @object.state_event_transition_on_save end def test_should_not_write_event_transition assert_nil @object.send(:state_event_transition) end def test_should_mark_event_transition_as_transient assert_predicate @transition, :transient? end end transition_collection_with_action_hook_with_different_actions_test.rb000066400000000000000000000024221507333401300413300ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' require_relative 'transition_collection_with_action_hook_base_test' class TransitionCollectionWithActionHookWithDifferentActionsTest < TransitionCollectionWithActionHookBaseTest def setup super @klass.class_eval do def save_status true end end @machine = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save_status) @machine.state :second_gear @machine.event :shift_up @result = StateMachines::TransitionCollection.new([@transition, StateMachines::Transition.new(@object, @machine, :shift_up, :first_gear, :second_gear)]).perform end def test_should_succeed assert @result end def test_should_run_action assert @object.saved end def test_should_have_already_persisted_when_running_action assert_equal 'idling', @object.state_on_save end def test_should_not_have_event_during_action assert_nil @object.state_event_on_save end def test_should_not_write_event assert_nil @object.state_event end def test_should_not_have_event_transition_during_save assert_nil @object.state_event_transition_on_save end def test_should_not_write_event_attribute assert_nil @object.send(:state_event_transition) end end transition_collection_with_action_hook_with_nil_action_test.rb000066400000000000000000000022461507333401300377650ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' require_relative 'transition_collection_with_action_hook_base_test' class TransitionCollectionWithActionHookWithNilActionTest < TransitionCollectionWithActionHookBaseTest def setup super @machine = StateMachines::Machine.new(@klass, :status, initial: :first_gear) @machine.state :second_gear @machine.event :shift_up @result = StateMachines::TransitionCollection.new([@transition, StateMachines::Transition.new(@object, @machine, :shift_up, :first_gear, :second_gear)]).perform end def test_should_succeed assert @result end def test_should_run_action assert @object.saved end def test_should_have_already_persisted_when_running_action assert_equal 'idling', @object.state_on_save end def test_should_not_have_event_during_action assert_nil @object.state_event_on_save end def test_should_not_write_event assert_nil @object.state_event end def test_should_not_have_event_transition_during_save assert_nil @object.state_event_transition_on_save end def test_should_not_write_event_attribute assert_nil @object.send(:state_event_transition) end end transition_collection_with_after_callback_halt_test.rb000066400000000000000000000025261507333401300361640ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithAfterCallbackHaltTest < StateMachinesTest def setup @klass = Class.new do attr_reader :saved def save @saved = true end end @before_count = 0 @after_count = 0 @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @machine.before_transition { @before_count += 1 } @machine.after_transition do @after_count += 1 throw :halt end @machine.after_transition { @after_count += 1 } @machine.around_transition do |block| @before_count += 1 block.call @after_count += 1 end @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) @result = @transitions.perform end def test_should_succeed assert @result end def test_should_persist_state assert_equal 'idling', @object.state end def test_should_run_before_callbacks assert_equal 2, @before_count end def test_should_not_run_further_after_callbacks assert_equal 2, @after_count end end transition_collection_with_before_callback_halt_test.rb000066400000000000000000000026461507333401300363300ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithBeforeCallbackHaltTest < StateMachinesTest def setup @klass = Class.new do attr_reader :saved def save @saved = true end end @before_count = 0 @after_count = 0 @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @machine.state :idling @machine.event :ignite @machine.before_transition do @before_count += 1 throw :halt end @machine.before_transition { @before_count += 1 } @machine.after_transition { @after_count += 1 } @machine.around_transition do |block| @before_count += 1 block.call @after_count += 1 end @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) @result = @transitions.perform end def test_should_not_succeed refute @result end def test_should_not_persist_state assert_equal 'parked', @object.state end def test_should_not_run_action refute @object.saved end def test_should_not_run_further_before_callbacks assert_equal 1, @before_count end def test_should_not_run_after_callbacks assert_equal 0, @after_count end end state_machines-0.100.4/test/unit/transition_collection/transition_collection_with_block_test.rb000066400000000000000000000027141507333401300334070ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithBlockTest < StateMachinesTest def setup @klass = Class.new do attr_reader :actions def save (@actions ||= []) << :save end end @state = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save) @state.state :idling @state.event :ignite @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save) @status.state :second_gear @status.event :shift_up @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling), @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear) ]) @result = @transitions.perform { 1 } end def test_should_succeed assert_equal 1, @result end def test_should_persist_states assert_equal 'idling', @object.state assert_equal 'second_gear', @object.status end def test_should_not_run_machine_actions assert_nil @object.actions end def test_should_use_result_as_transition_result assert_equal 1, @state_transition.result assert_equal 1, @status_transition.result end end state_machines-0.100.4/test/unit/transition_collection/transition_collection_with_callbacks_test.rb000066400000000000000000000112231507333401300342270ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithCallbacksTest < StateMachinesTest def setup @klass = Class.new do attr_reader :saved def save @saved = true end end @before_callbacks = [] @after_callbacks = [] @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @state.state :idling @state.event :ignite @state.before_transition { @before_callbacks << :state_before } @state.after_transition { @after_callbacks << :state_after } @state.around_transition do |block| @before_callbacks << :state_around block.call @after_callbacks << :state_around end @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save) @status.state :second_gear @status.event :shift_up @status.before_transition { @before_callbacks << :status_before } @status.after_transition { @after_callbacks << :status_after } @status.around_transition do |block| @before_callbacks << :status_around block.call @after_callbacks << :status_around end @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling), StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear) ]) end def test_should_run_before_callbacks_in_order @transitions.perform assert_equal %i[state_before state_around status_before status_around], @before_callbacks end def test_should_halt_if_before_callback_halted_for_first_transition @state.before_transition { throw :halt } refute @transitions.perform assert_equal %i[state_before state_around], @before_callbacks end def test_should_halt_if_before_callback_halted_for_second_transition @status.before_transition { throw :halt } refute @transitions.perform assert_equal %i[state_before state_around status_before status_around], @before_callbacks end def test_should_halt_if_around_callback_halted_before_yield_for_first_transition @state.around_transition { throw :halt } refute @transitions.perform assert_equal %i[state_before state_around], @before_callbacks end def test_should_halt_if_around_callback_halted_before_yield_for_second_transition @status.around_transition { throw :halt } refute @transitions.perform assert_equal %i[state_before state_around status_before status_around], @before_callbacks end def test_should_run_after_callbacks_in_reverse_order @transitions.perform assert_equal %i[status_around status_after state_around state_after], @after_callbacks end def test_should_not_halt_if_after_callback_halted_for_first_transition @state.after_transition { throw :halt } assert @transitions.perform assert_equal %i[status_around status_after state_around state_after], @after_callbacks end def test_should_not_halt_if_around_callback_halted_for_second_transition @status.around_transition do |block| block.call throw :halt end assert @transitions.perform assert_equal %i[state_around state_after], @after_callbacks end def test_should_run_before_callbacks_before_persisting_the_state @state.before_transition { |object| @before_state = object.state } @state.around_transition do |object, _transition, block| @around_state = object.state block.call end @transitions.perform assert_equal 'parked', @before_state assert_equal 'parked', @around_state end def test_should_persist_state_before_running_action @klass.class_eval do attr_reader :saved_on_persist def state=(value) @state = value @saved_on_persist = saved end end @transitions.perform refute @object.saved_on_persist end def test_should_persist_state_before_running_action_block @klass.class_eval do attr_writer :saved attr_reader :saved_on_persist def state=(value) @state = value @saved_on_persist = saved end end @transitions.perform { @object.saved = true } refute @object.saved_on_persist end def test_should_run_after_callbacks_after_running_the_action @state.after_transition { |object| @after_saved = object.saved } @state.around_transition do |object, _transition, block| block.call @around_saved = object.saved end @transitions.perform assert @after_saved assert @around_saved end end transition_collection_with_different_actions_test.rb000066400000000000000000000117521507333401300357260ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithDifferentActionsTest < StateMachinesTest def setup @klass = Class.new do attr_reader :actions def save_state (@actions ||= []) << :save_state :save_state end def save_status (@actions ||= []) << :save_status :save_status end end @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save_state) @state.state :idling @state.event :ignite @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save_status) @status.state :second_gear @status.event :shift_up @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling), @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear) ]) end def test_should_succeed assert @transitions.perform end def test_should_persist_states @transitions.perform assert_equal 'idling', @object.state assert_equal 'second_gear', @object.status end def test_should_run_actions_in_order @transitions.perform assert_equal %i[save_state save_status], @object.actions end def test_should_store_results_in_transitions @transitions.perform assert_equal :save_state, @state_transition.result assert_equal :save_status, @status_transition.result end def test_should_not_halt_if_action_fails_for_first_transition @klass.class_eval do remove_method :save_state def save_state (@actions ||= []) << :save_state false end end refute @transitions.perform assert_equal %i[save_state save_status], @object.actions end def test_should_halt_if_action_fails_for_second_transition @klass.class_eval do remove_method :save_status def save_status (@actions ||= []) << :save_status false end end refute @transitions.perform assert_equal %i[save_state save_status], @object.actions end def test_should_rollback_if_action_errors_for_first_transition @klass.class_eval do remove_method :save_state def save_state raise ArgumentError end end begin @transitions.perform rescue StandardError end assert_equal 'parked', @object.state assert_equal 'first_gear', @object.status end def test_should_rollback_if_action_errors_for_second_transition @klass.class_eval do remove_method :save_status def save_status raise ArgumentError end end begin @transitions.perform rescue StandardError end assert_equal 'parked', @object.state assert_equal 'first_gear', @object.status end def test_should_not_run_after_callbacks_if_action_fails_for_first_transition @klass.class_eval do remove_method :save_state def save_state false end end @callbacks = [] @state.after_transition { @callbacks << :state_after } @state.around_transition do |block| block.call @callbacks << :state_around end @status.after_transition { @callbacks << :status_after } @status.around_transition do |block| block.call @callbacks << :status_around end @transitions.perform assert_empty @callbacks end def test_should_not_run_after_callbacks_if_action_fails_for_second_transition @klass.class_eval do remove_method :save_status def save_status false end end @callbacks = [] @state.after_transition { @callbacks << :state_after } @state.around_transition do |block| block.call @callbacks << :state_around end @status.after_transition { @callbacks << :status_after } @status.around_transition do |block| block.call @callbacks << :status_around end @transitions.perform assert_empty @callbacks end def test_should_run_after_failure_callbacks_if_action_fails_for_first_transition @klass.class_eval do remove_method :save_state def save_state false end end @callbacks = [] @state.after_failure { @callbacks << :state_after } @status.after_failure { @callbacks << :status_after } @transitions.perform assert_equal %i[status_after state_after], @callbacks end def test_should_run_after_failure_callbacks_if_action_fails_for_second_transition @klass.class_eval do remove_method :save_status def save_status false end end @callbacks = [] @state.after_failure { @callbacks << :state_after } @status.after_failure { @callbacks << :status_after } @transitions.perform assert_equal %i[status_after state_after], @callbacks end end transition_collection_with_duplicate_actions_test.rb000066400000000000000000000027441507333401300357330ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithDuplicateActionsTest < StateMachinesTest def setup @klass = Class.new do attr_reader :actions def save (@actions ||= []) << :save :save end end @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @state.state :idling @state.event :ignite @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save) @status.state :second_gear @status.event :shift_up @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling), @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear) ]) @result = @transitions.perform end def test_should_succeed assert_equal :save, @result end def test_should_persist_states assert_equal 'idling', @object.state assert_equal 'second_gear', @object.status end def test_should_run_action_once assert_equal [:save], @object.actions end def test_should_store_results_in_transitions assert_equal :save, @state_transition.result assert_equal :save, @status_transition.result end end transition_collection_with_empty_actions_test.rb000066400000000000000000000024411507333401300351110ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithEmptyActionsTest < StateMachinesTest def setup @klass = Class.new @state = StateMachines::Machine.new(@klass, initial: :parked) @state.state :idling @state.event :ignite @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear) @status.state :second_gear @status.event :shift_up @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling), @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear) ]) @object.state = 'idling' @object.status = 'second_gear' @result = @transitions.perform end def test_should_succeed assert @result end def test_should_persist_states assert_equal 'idling', @object.state assert_equal 'second_gear', @object.status end def test_should_store_results_in_transitions assert_nil @state_transition.result assert_nil @status_transition.result end end transition_collection_with_mixed_actions_test.rb000066400000000000000000000024331507333401300350620ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithMixedActionsTest < StateMachinesTest def setup @klass = Class.new do def save true end end @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save) @state.state :idling @state.event :ignite @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear) @status.state :second_gear @status.event :shift_up @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling), @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear) ]) @result = @transitions.perform end def test_should_succeed assert @result end def test_should_persist_states assert_equal 'idling', @object.state assert_equal 'second_gear', @object.status end def test_should_store_results_in_transitions assert @state_transition.result assert_nil @status_transition.result end end transition_collection_with_skipped_actions_and_block_test.rb000066400000000000000000000017731507333401300374150ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithSkippedActionsAndBlockTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save_state) @machine.state :idling @machine.event :ignite @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ @state_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ], actions: false) @result = @transitions.perform do @ran_block = true 1 end end def test_should_succeed assert_equal 1, @result end def test_should_persist_states assert_equal 'idling', @object.state end def test_should_run_block assert @ran_block end def test_should_store_results_in_transitions assert_equal 1, @state_transition.result end end transition_collection_with_skipped_actions_test.rb000066400000000000000000000046221507333401300354150ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithSkippedActionsTest < StateMachinesTest def setup @klass = Class.new do attr_reader :actions def save_state (@actions ||= []) << :save_state :save_state end def save_status (@actions ||= []) << :save_status :save_status end end @callbacks = [] @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save_state) @state.state :idling @state.event :ignite @state.before_transition { @callbacks << :state_before } @state.after_transition { @callbacks << :state_after } @state.around_transition do |block| @callbacks << :state_around_before block.call @callbacks << :state_around_after end @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save_status) @status.state :second_gear @status.event :shift_up @status.before_transition { @callbacks << :status_before } @status.after_transition { @callbacks << :status_after } @status.around_transition do |block| @callbacks << :status_around_before block.call @callbacks << :status_around_after end @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling), @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear) ], actions: false) @result = @transitions.perform end def test_should_skip_actions assert @transitions.skip_actions end def test_should_succeed assert @result end def test_should_persist_states assert_equal 'idling', @object.state assert_equal 'second_gear', @object.status end def test_should_not_run_actions assert_nil @object.actions end def test_should_store_results_in_transitions assert_nil @state_transition.result assert_nil @status_transition.result end def test_should_run_all_callbacks assert_equal %i[state_before state_around_before status_before status_around_before status_around_after status_after state_around_after state_after], @callbacks end end transition_collection_with_skipped_after_callbacks_and_around_callbacks_test.rb000066400000000000000000000031671507333401300432510ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithSkippedAfterCallbacksAndAroundCallbacksTest < StateMachinesTest def setup @klass = Class.new @callbacks = [] @machine = StateMachines::Machine.new(@klass, initial: :parked) @machine.state :idling @machine.event :ignite @machine.around_transition do |block| @callbacks << :around_before block.call @callbacks << :around_after end @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ], after: false) end # This test is no longer needed since all Ruby engines support pause # def test_should_raise_exception # assert_raises(ArgumentError) { @transitions.perform } # end def test_should_succeed @transitions.perform assert @transitions.perform end def test_should_not_run_around_callbacks_after_yield @transitions.perform refute_includes @callbacks, :around_after end def test_should_run_around_callbacks_after_yield_on_subsequent_perform @transitions.perform StateMachines::TransitionCollection.new([@transition]).perform assert_includes @callbacks, :around_after end def test_should_not_rerun_around_callbacks_before_yield_on_subsequent_perform @transitions.perform @callbacks = [] StateMachines::TransitionCollection.new([@transition]).perform refute_includes @callbacks, :around_before end end transition_collection_with_skipped_after_callbacks_test.rb000066400000000000000000000020161507333401300370500ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithSkippedAfterCallbacksTest < StateMachinesTest def setup @klass = Class.new @callbacks = [] @machine = StateMachines::Machine.new(@klass, initial: :parked) @machine.state :idling @machine.event :ignite @machine.after_transition { @callbacks << :after } @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ], after: false) @result = @transitions.perform end def test_should_succeed assert @result end def test_should_not_run_after_callbacks refute_includes @callbacks, :after end def test_should_run_after_callbacks_on_subsequent_perform StateMachines::TransitionCollection.new([@transition]).perform assert_includes @callbacks, :after end end transition_collection_with_transactions_test.rb000066400000000000000000000036531507333401300347510ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithTransactionsTest < StateMachinesTest def setup @klass = Class.new do attr_accessor :running_transaction, :cancelled_transaction end @machine = StateMachines::Machine.new(@klass, initial: :parked) @machine.state :idling @machine.event :ignite class << @machine def within_transaction(object) object.running_transaction = true object.cancelled_transaction = yield == false object.running_transaction = false end end @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ], use_transactions: true) end def test_should_run_before_callbacks_within_transaction @machine.before_transition { |object| @in_transaction = object.running_transaction } @transitions.perform assert @in_transaction end def test_should_run_action_within_transaction @transitions.perform { @in_transaction = @object.running_transaction } assert @in_transaction end def test_should_run_after_callbacks_within_transaction @machine.after_transition { |object| @in_transaction = object.running_transaction } @transitions.perform assert @in_transaction end def test_should_cancel_the_transaction_on_before_halt @machine.before_transition { throw :halt } @transitions.perform assert @object.cancelled_transaction end def test_should_cancel_the_transaction_on_action_failure @transitions.perform { false } assert @object.cancelled_transaction end def test_should_not_cancel_the_transaction_on_after_halt @machine.after_transition { throw :halt } @transitions.perform refute @object.cancelled_transaction end end transition_collection_without_transactions_test.rb000066400000000000000000000015571507333401300355020ustar00rootroot00000000000000state_machines-0.100.4/test/unit/transition_collection# frozen_string_literal: true require 'test_helper' class TransitionCollectionWithoutTransactionsTest < StateMachinesTest def setup @klass = Class.new do attr_accessor :ran_transaction end @machine = StateMachines::Machine.new(@klass, initial: :parked) @machine.state :idling @machine.event :ignite class << @machine def within_transaction(object) object.ran_transaction = true end end @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ], use_transactions: false) @transitions.perform end def test_should_not_run_within_transaction refute @object.ran_transaction end end