pax_global_header00006660000000000000000000000064144466577570014543gustar00rootroot0000000000000052 comment=4747004917893dad56a3c13e565de879c0a87f0f state_machines-0.6.0/000077500000000000000000000000001444665775700145355ustar00rootroot00000000000000state_machines-0.6.0/.github/000077500000000000000000000000001444665775700160755ustar00rootroot00000000000000state_machines-0.6.0/.github/workflows/000077500000000000000000000000001444665775700201325ustar00rootroot00000000000000state_machines-0.6.0/.github/workflows/engines.yml000066400000000000000000000007331444665775700223100ustar00rootroot00000000000000name: Ruby on: push: branches: [ master ] pull_request: branches: [ master ] jobs: test: runs-on: ubuntu-latest strategy: matrix: ruby-version: ['jruby', 'truffleruby'] steps: - uses: actions/checkout@v3 - 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 continue-on-error: true state_machines-0.6.0/.github/workflows/ruby.yml000066400000000000000000000006721444665775700216430ustar00rootroot00000000000000name: Ruby on: push: branches: [ master ] pull_request: branches: [ master ] jobs: test: runs-on: ubuntu-latest strategy: matrix: ruby-version: ['3.0', '3.1', '3.2'] steps: - uses: actions/checkout@v3 - 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.6.0/.gitignore000066400000000000000000000002641444665775700165270ustar00rootroot00000000000000*.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.6.0/Changelog.md000066400000000000000000000013661444665775700167540ustar00rootroot00000000000000## 0.6.0 * Drop support to EOL rubies. ## 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.6.0/Contributors.md000066400000000000000000000011501444665775700175510ustar00rootroot00000000000000- Aaron Gibralter - Aaron Pfeifer - Abdelkader Boudih - Akira Matsuda - Andrea Longhi - Brad Heller - Brandon Dimcheff - Casey Howard - Chinasaur - Daniel Huckstep - Durran Jordan - Gareth Adams - Jahangir Zinedine - Jeremy Wells - Joe Lind - Jon Evans - Markus Schirp - Michael Klishin - Mikhail Shirkov - Mohamed Alouane - Nate Murray - Nathan Long - Nicolas Blanco - Pawel Pierzchala - Pete Forde - Peter Lampesberger - Rin Raeuber - Robert Poor - Rustam Zagirov - Sandro Turriate and Tim Pope - Sean O'Brien - Stefan Penner - Steve Richert - Wojciech Wnętrzak - @chris - @gmitrev - @nblumoe - @reiner - @sanematstate_machines-0.6.0/Gemfile000066400000000000000000000001511444665775700160250ustar00rootroot00000000000000source 'https://rubygems.org' gemspec platform :mri do gem 'pry-byebug' end gem 'minitest-reporters' state_machines-0.6.0/LICENSE.txt000066400000000000000000000021351444665775700163610ustar00rootroot00000000000000Copyright (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.6.0/README.md000066400000000000000000000461361444665775700160260ustar00rootroot00000000000000![Build Status](https://github.com/state-machines/state_machines/actions/workflows/ruby.yml/badge.svg) [![Code Climate](https://codeclimate.com/github/state-machines/state_machines.svg)](https://codeclimate.com/github/state-machines/state_machines) # 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: gem 'state_machines' And then execute: $ bundle Or install it yourself as: $ 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 * 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 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 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 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 # 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 # 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 ``` ## 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 ``` However, 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) 2.6.0+ * JRuby * Rubinius For graphing state machine: * [state_machines-graphviz](http://github.com/state-machines/state_machines-graphviz) For documenting state machines: * [state_machines-yard](http://github.com/state-machines/state_machines-yard) ## TODO * Add matchers/assertions for rspec and minitest ## Contributing 1. Fork it ( https://github.com/state-machines/state_machines/fork ) 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.6.0/Rakefile000066400000000000000000000005311444665775700162010ustar00rootroot00000000000000require '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 default: [:unit, :functional] state_machines-0.6.0/lib/000077500000000000000000000000001444665775700153035ustar00rootroot00000000000000state_machines-0.6.0/lib/state_machines.rb000066400000000000000000000001401444665775700206120ustar00rootroot00000000000000require 'state_machines/version' require 'state_machines/core' require 'state_machines/core_ext'state_machines-0.6.0/lib/state_machines/000077500000000000000000000000001444665775700202725ustar00rootroot00000000000000state_machines-0.6.0/lib/state_machines/assertions.rb000066400000000000000000000035651444665775700230220ustar00rootroot00000000000000class Hash # Provides a set of helper methods for making assertions about the content # of various objects unless respond_to?(:assert_valid_keys) # Validate all keys in a hash match *valid_keys, raising ArgumentError # on a mismatch. Note that keys are NOT treated indifferently, meaning if you # use strings for keys but assert symbols as keys, this will fail. # # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age" # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'" # { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing # Code from ActiveSupport def assert_valid_keys(*valid_keys) valid_keys.flatten! each_key do |k| unless valid_keys.include?(k) raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}") end end end end # Validates that the given hash only includes at *most* one of a set of # exclusive keys. If more than one key is found, an ArgumentError will be # raised. # # == Examples # # options = {:only => :on, :except => :off} # options.assert_exclusive_keys(:only) # => nil # options.assert_exclusive_keys(:except) # => nil # options.assert_exclusive_keys(:only, :except) # => ArgumentError: Conflicting keys: only, except # options.assert_exclusive_keys(:only, :except, :with) # => ArgumentError: Conflicting keys: only, except def assert_exclusive_keys(*exclusive_keys) conflicting_keys = exclusive_keys & keys raise ArgumentError, "Conflicting keys: #{conflicting_keys.join(', ')}" unless conflicting_keys.length <= 1 end end state_machines-0.6.0/lib/state_machines/branch.rb000066400000000000000000000170461444665775700220640ustar00rootroot00000000000000module 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) # Build event requirement @event_requirement = build_matcher(options, :on, :except_on) if (options.keys - [: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| [: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 # 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. # # == 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 = {}) query.assert_valid_keys(:from, :to, :on, :guard) if (match = match_query(query)) && matches_conditions?(object, query) match end end def draw(graph, event, valid_states) fail NotImplementedError 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) options.assert_exclusive_keys(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| [: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 def matches_conditions?(object, query) query[:guard] == false || Array(if_condition).all? { |condition| evaluate_method(object, condition) } && !Array(unless_condition).any? { |condition| evaluate_method(object, condition) } end end end state_machines-0.6.0/lib/state_machines/callback.rb000066400000000000000000000166111444665775700223600ustar00rootroot00000000000000require '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 [: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 = {}, *args, &block) if @branch.matches?(object, context) run_methods(object, context, 0, *args, &block) 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) if type == :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 else yield if block_given? end else @methods.each do |method| result = evaluate_method(object, method, *args) throw :halt if @terminator && @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 = lambda { |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.6.0/lib/state_machines/core.rb000066400000000000000000000023461444665775700215540ustar00rootroot00000000000000# 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/assertions' 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/state' 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.6.0/lib/state_machines/core_ext.rb000066400000000000000000000001641444665775700224300ustar00rootroot00000000000000# Loads all of the extensions to be made to Ruby core classes require 'state_machines/core_ext/class/state_machine' state_machines-0.6.0/lib/state_machines/core_ext/000077500000000000000000000000001444665775700221025ustar00rootroot00000000000000state_machines-0.6.0/lib/state_machines/core_ext/class/000077500000000000000000000000001444665775700232075ustar00rootroot00000000000000state_machines-0.6.0/lib/state_machines/core_ext/class/state_machine.rb000066400000000000000000000000761444665775700263430ustar00rootroot00000000000000Class.class_eval do include StateMachines::MacroMethods end state_machines-0.6.0/lib/state_machines/error.rb000066400000000000000000000053521444665775700217550ustar00rootroot00000000000000module 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.6.0/lib/state_machines/eval_helpers.rb000066400000000000000000000065011444665775700232720ustar00rootroot00000000000000module 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 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) def evaluate_method(object, method, *args, &block) case method when Symbol klass = (class << object; self; end) args = [] if (klass.method_defined?(method) || klass.private_method_defined?(method)) && object.method(method).arity == 0 object.send(method, *args, &block) when Proc, Method args.unshift(object) arity = method.arity # Procs don't support blocks in < Ruby 1.9, so it's tacked on as an # argument for consistency across versions of Ruby if block_given? && Proc === method && arity != 0 if [1, 2].include?(arity) # Force the block to be either the only argument or the 2nd one # after the object (may mean additional arguments get discarded) args = args[0, arity - 1] + [block] else # Tack the block to the end of the args args << block end else # These method types are only called with 0, 1, or n arguments args = args[0, arity] if [0, 1].include?(arity) end method.is_a?(Proc) ? method.call(*args) : method.call(*args, &block) when String eval(method, object.instance_eval { binding }, &block) 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 end end state_machines-0.6.0/lib/state_machines/event.rb000066400000000000000000000216661444665775700217530ustar00rootroot00000000000000module 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 = {}) #:nodoc: options.assert_valid_keys(:human_name) @machine = machine @name = name @qualified_name = machine.namespace ? :"#{name}_#{machine.namespace}" : name @human_name = options[: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(&block) instance_eval(&block) 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 options.assert_valid_keys(:from, :to, :except_from, :except_to, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).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. def transition_for(object, requirements = {}) requirements.assert_valid_keys(:from, :to, :guard) requirements[:from] = machine.states.match!(object).name unless (custom_from_state = requirements.include?(:from)) branches.each do |branch| if (match = branch.match(object, requirements)) # 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 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, *args) machine.reset(object) if (transition = transition_for(object)) transition.perform(*args) else on_failure(object, *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 = {}) fail NotImplementedError 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.map do |branch| branch.state_requirements.map do |state_requirement| "#{state_requirement[:from].description} => #{state_requirement[:to].description}" end * ', ' end "#<#{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.6.0/lib/state_machines/event_collection.rb000066400000000000000000000142711444665775700241600ustar00rootroot00000000000000module StateMachines # Represents a collection of events in a state machine class EventCollection < NodeCollection def initialize(machine) #:nodoc: super(machine, index: [: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 machine.read(object, :event_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.6.0/lib/state_machines/extensions.rb000066400000000000000000000124621444665775700230230ustar00rootroot00000000000000module 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])) || fail(StateMachines::InvalidParallelTransition.new(self, events)) end protected def initialize_state_machines(options = {}, &block) #:nodoc: self.class.state_machines.initialize_states(self, options, &block) end end end state_machines-0.6.0/lib/state_machines/helper_module.rb000066400000000000000000000010451444665775700234430ustar00rootroot00000000000000module 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.6.0/lib/state_machines/integrations.rb000066400000000000000000000101631444665775700233260ustar00rootroot00000000000000module 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 fail 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] def integrations # Register all namespaced integrations @integrations end alias_method :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) if integration.respond_to?(:integration_name) @integrations.insert(0, integration) unless @integrations.include?(integration) end end end end end state_machines-0.6.0/lib/state_machines/integrations/000077500000000000000000000000001444665775700230005ustar00rootroot00000000000000state_machines-0.6.0/lib/state_machines/integrations/base.rb000066400000000000000000000022751444665775700242450ustar00rootroot00000000000000module 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 end def self.included(base) #:nodoc: base.extend ClassMethods end end end end state_machines-0.6.0/lib/state_machines/machine.rb000066400000000000000000002556711444665775700222430ustar00rootroot00000000000000module 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 include EvalHelpers include MatcherHelpers class << self # 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, &block) 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 end # Evaluate DSL machine.instance_eval(&block) if block_given? else # No existing machine: create a new one machine = new(owner_class, name, options, &block) end machine end def draw(*) fail NotImplementedError end # Default messages to use for validation errors in ORM integrations attr_accessor :default_messages attr_accessor :ignore_method_conflicts end @default_messages = { invalid: 'is invalid', invalid_event: 'cannot transition when %s', invalid_transition: 'cannot transition via "%1$s"' } # Whether to ignore any conflicts that are detected for helper methods that # get generated for a machine's owner class. Default is false. @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 def initialize(owner_class, *args, &block) options = args.last.is_a?(Hash) ? args.pop : {} options.assert_valid_keys(:attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions) # Find an integration that matches this machine's owner class if options.include?(:integration) @integration = options[:integration] && StateMachines::Integrations.find_by_name(options[:integration]) else @integration = StateMachines::Integrations.match(owner_class) end 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 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(&block) if block_given? 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} 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 { |state| state.initial } if !owner_class_attribute_default.nil? && (dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state)) 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 end # 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) # => # 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) if 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 end # Gets the actual name of the attribute on the machine's owner class that # stores data with the given name. def attribute(name = :state) name == :state ? @attribute : :"#{self.name}_#{name}" end # 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, *args, **kwargs, &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 |*block_args, **block_kwargs| block.call((scope == :instance ? self.class : self).state_machine(name), self, *block_args, **block_kwargs) end end end else helper_module.class_eval(method, *args, **kwargs) 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. def state(*names, &block) options = names.last.is_a?(Hash) ? names.pop : {} options.assert_valid_keys(: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, &block) 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_method :other_states, :state # Gets the current value stored in the given object's attribute. # # For example, # # class Vehicle # state_machine :initial => :parked do # ... # end # end # # vehicle = Vehicle.new # => # # Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state # Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event 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. # # For example, # # class Vehicle # state_machine :initial => :parked do # ... # end # end # # vehicle = Vehicle.new # => # # Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling' # Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park' # vehicle.state # => "idling" # vehicle.event # => "park" def write(object, attribute, value, ivar = false) attribute = self.attribute(attribute) ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value) end # 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 def event(*names, &block) options = names.last.is_a?(Hash) ? names.pop : {} options.assert_valid_keys(:human_name) # Store the context so that it can be used for / matched against any event # that gets added @events.context(names, &block) 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_method :on, :event # 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. 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 # 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, &block) options = (args.last.is_a?(Hash) ? args.pop : {}) options[:do] = args if args.any? add_callback(:before, options, &block) 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, &block) options = (args.last.is_a?(Hash) ? args.pop : {}) options[:do] = args if args.any? add_callback(:after, options, &block) 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, &block) options = (args.last.is_a?(Hash) ? args.pop : {}) options[:do] = args if args.any? add_callback(:around, options, &block) 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, &block) options = (args.last.is_a?(Hash) ? args.pop : {}) options[:do] = args if args.any? options.assert_valid_keys(:on, :do, :if, :unless) add_callback(:failure, options, &block) 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] def paths_for(object, requirements = {}) PathCollection.new(object, self, requirements) end # 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 { |value| value.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) { yield } else yield end end def draw(*) fail NotImplementedError 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 # 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.inject([]) do |machines, (name, machine)| if machine.attribute == attribute && machine != self machines << (owner_class.state_machine(name) {}) end machines end end # 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 # 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}?") 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 { |event| event.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 if 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 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 # 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.any? { |name, machine| machine.action == action && machine != self } end # Adds helper methods for automatically firing events when an action # is invoked def define_action_helpers if action_hook @action_hook_defined = true define_action_hook end 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 # 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. 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..-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 ancestors = ancestors[ancestors.index(@helper_modules[scope])..-1].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 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 # 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 # 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) [:with, :without].each do |kind| [name, plural].map { |s| s.to_s }.uniq.each do |suffix| method = "#{kind}_#{suffix}" if (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 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 # 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 "#{name}s" 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 # 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 # Updates this machine based on the configuration of other machines in the # owner class that share the same target 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 # Adds a new transition callback of the given type. def add_callback(type, options, &block) callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block) 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 state_machines-0.6.0/lib/state_machines/machine_collection.rb000066400000000000000000000076171444665775700244510ustar00rootroot00000000000000module 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 = {}) options.assert_valid_keys( :static, :dynamic, :to) options = {static: true, dynamic: true}.merge(options) result = yield if block_given? 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 if options[:static] each_value do |machine| machine.initialize_state(object, force: options[:dynamic] == :force, to: options[:to]) if machine.dynamic_initial_state? end if options[:dynamic] 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.6.0/lib/state_machines/macro_methods.rb000066400000000000000000000525611444665775700234540ustar00rootroot00000000000000# 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(*args, &block) StateMachines::Machine.find_or_create(self, *args, &block) end end end state_machines-0.6.0/lib/state_machines/matcher.rb000066400000000000000000000070331444665775700222450ustar00rootroot00000000000000module 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 -(blacklist) BlacklistMatcher.new(blacklist) end # 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.6.0/lib/state_machines/matcher_helpers.rb000066400000000000000000000027001444665775700237630ustar00rootroot00000000000000module 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_method :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.6.0/lib/state_machines/node_collection.rb000066400000000000000000000163621444665775700237670ustar00rootroot00000000000000module 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 = {}) options.assert_valid_keys(:index) options = { index: :name }.merge(options) @machine = machine @nodes = [] @index_names = Array(options[:index]) @indices = @index_names.reduce({}) do |indices, name| indices[name] = {} indices[:"#{name}_to_s"] = {} indices[:"#{name}_to_sym"] = {} indices 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.reduce({}) { |indices, (name, *)| indices[name] = {}; indices } # 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 { |node| yield node } 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] || fail(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) fail ArgumentError, 'No indices configured' unless @indices.any? @indices[name] || fail(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 if old_key != new_key remove_from_index(name, old_key) add_to_index(name, new_key, node) end 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.6.0/lib/state_machines/path.rb000066400000000000000000000100361444665775700215530ustar00rootroot00000000000000module 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 = {}) options.assert_valid_keys(: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.6.0/lib/state_machines/path_collection.rb000066400000000000000000000052651444665775700237760ustar00rootroot00000000000000module 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) options.assert_valid_keys( :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.6.0/lib/state_machines/state.rb000066400000000000000000000257151444665775700217510ustar00rootroot00000000000000module 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_method :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 = {}) #:nodoc: options.assert_valid_keys(:initial, :value, :cache, :if, :human_name) @machine = machine @name = name @qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name @human_name = options[:human_name] || (@name ? @name.to_s.tr('_', ' ') : 'nil') @value = options.include?(:value) ? options[:value] : name&.to_s @cache = options[:cache] @matcher = options[:if] @initial = options[:initial] == true @context = StateContext.new(self) if name conflicting_machines = machine.owner_class.state_machines.select { |other_name, other_machine| other_machine != machine && other_machine.states[qualified_name, :qualified_name] } # Output a warning if another machine has a conflicting qualified name # for a different attribute if (conflict = conflicting_machines.detect { |_other_name, other_machine| other_machine.attribute != machine.attribute }) _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 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.any? 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(&block) # 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(&block) new_methods = context_methods.to_a.select { |(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, &block) 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, &block) 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 => ex if ex.name.to_s == options[:method_name].to_s && ex.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]}") else raise end end end end def draw(graph, options = {}) fail NotImplementedError 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 # Checks whether the current value matches this state machine.define_helper(:instance, "#{qualified_name}?") do |machine, object| machine.states.matches?(object, name) 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 end end state_machines-0.6.0/lib/state_machines/state_collection.rb000066400000000000000000000076651444665775700241700ustar00rootroot00000000000000module StateMachines # Represents a collection of states in a state machine class StateCollection < NodeCollection def initialize(machine) #:nodoc: super(machine, index: [: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.6.0/lib/state_machines/state_context.rb000066400000000000000000000113511444665775700235040ustar00rootroot00000000000000module 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 = lambda { |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) options.assert_valid_keys(: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, &block) # 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, &block) end end end state_machines-0.6.0/lib/state_machines/transition.rb000066400000000000000000000326341444665775700230210ustar00rootroot00000000000000module StateMachines # 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 # Determines whether the current ruby implementation supports pausing and # resuming transitions def self.pause_supported? %w(ruby maglev).include?(RUBY_ENGINE) end # 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 @resume_block = 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(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 def perform(*args) run_action = [true, false].include?(args.last) ? args.pop : true 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) do yield end 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) @success = false halted = pausable { before(options[:after], &block) } if options[:before] # After callbacks are only run if: # * An around callback didn't halt after yielding # * They're enabled or the run didn't succeed after if !(@before_run && halted) && (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 unless @persisted machine.write(object, :state, to) @persisted = true end 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_block = 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 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. def pausable begin halted = !catch(:halt) { yield; true } rescue => error raise unless @resume_block end if @resume_block @resume_block.call(halted, error) else halted 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 raise ArgumentError, 'around_transition callbacks cannot be called in multiple execution contexts in java implementations of Ruby. Use before/after_transitions instead.' unless self.class.pause_supported? unless @resume_block require 'continuation' unless defined?(callcc) callcc do |block| @paused_block = block throw :halt, true end end end # Resumes the execution of a previously paused callback execution. Once # the paused callbacks complete, the current execution will continue. def resume if @paused_block halted, error = callcc do |block| @resume_block = block @paused_block.call end @resume_block = @paused_block = nil raise error if error !halted else true end 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) unless @before_run while callback = machine.callbacks[:before][index] index += 1 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) return if catch(:cancel) do callback.call(object, context, self) do before(complete, index, &block) pause if @success && !complete throw :cancel, true unless @success end end else # Normal before callback callback.call(object, context, self) end end @before_run = true end action = {success: true}.merge(block_given? ? yield : {}) @result, @success = action[:result], action[:success] 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 unless @after_run # First resume previously paused callbacks if resume catch(:halt) do type = @success ? :after : :failure machine.callbacks[type].each { |callback| callback.call(object, context, self) } end end @after_run = true end 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.6.0/lib/state_machines/transition_collection.rb000066400000000000000000000172511444665775700252320ustar00rootroot00000000000000module 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 # 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! { |transition| !transition } attributes = map { |transition| transition.attribute }.uniq fail ArgumentError, 'Cannot perform multiple transitions in parallel for the same state machine attribute' if attributes.length != length options.assert_valid_keys(:actions, :after, :use_transactions) options = {actions: true, after: true, use_transactions: true}.merge(options) @skip_actions = !options[:actions] @skip_after = !options[:after] @use_transactions = options[:use_transactions] 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 { |transition| transition.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] throw :halt unless transition.run_callbacks(after: !skip_after) do run_callbacks(index + 1, &block) {result: results[transition.action], success: success?} end 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 { |transition| transition.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 { |transition| transition.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 begin yield rescue rollback raise end 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 == 0 # 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 # Rollback only if exceptions occur during before callbacks begin super rescue 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. each { |transition| transition.machine.write(object, :event_transition, transition) } if skip_after && success? 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.6.0/lib/state_machines/version.rb000066400000000000000000000000551444665775700223040ustar00rootroot00000000000000module StateMachines VERSION = '0.6.0' end state_machines-0.6.0/state_machines.gemspec000066400000000000000000000015431444665775700210740ustar00rootroot00000000000000require_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 aaron@pluginaweek.org) spec.summary = %q(State machines for attributes) spec.description = %q(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.required_ruby_version = '>= 3.0.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 'rake' spec.add_development_dependency 'minitest', '>= 5.4' end state_machines-0.6.0/test/000077500000000000000000000000001444665775700155145ustar00rootroot00000000000000state_machines-0.6.0/test/files/000077500000000000000000000000001444665775700166165ustar00rootroot00000000000000state_machines-0.6.0/test/files/integrations/000077500000000000000000000000001444665775700213245ustar00rootroot00000000000000state_machines-0.6.0/test/files/integrations/event_on_failure_integration.rb000066400000000000000000000004031444665775700275750ustar00rootroot00000000000000module EventOnFailureIntegration include StateMachines::Integrations::Base def invalidate(object, _attribute, message, values = []) (object.errors ||= []) << generate_message(message, values) end def reset(object) object.errors = [] end endstate_machines-0.6.0/test/files/integrations/vehicle.rb000066400000000000000000000001741444665775700232720ustar00rootroot00000000000000module VehicleIntegration include StateMachines::Integrations::Base def self.matching_ancestors [Vehicle] end endstate_machines-0.6.0/test/files/models/000077500000000000000000000000001444665775700201015ustar00rootroot00000000000000state_machines-0.6.0/test/files/models/auto_shop.rb000066400000000000000000000011701444665775700224260ustar00rootroot00000000000000class 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.6.0/test/files/models/car.rb000066400000000000000000000005661444665775700212020ustar00rootroot00000000000000require_relative 'vehicle' class Car < Vehicle state_machine do event :reverse do transition [: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.6.0/test/files/models/driver.rb000066400000000000000000000003631444665775700217230ustar00rootroot00000000000000require_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.6.0/test/files/models/hybrid_car.rb000066400000000000000000000026061444665775700225400ustar00rootroot00000000000000require_relative 'vehicle' class HybridCar < Vehicle attr_accessor :propulsion_mode, :driving_profile, :target_year attr_accessor :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.6.0/test/files/models/model_base.rb000066400000000000000000000001001444665775700225070ustar00rootroot00000000000000class ModelBase def save @saved = true self end end state_machines-0.6.0/test/files/models/motorcycle.rb000066400000000000000000000003771444665775700226150ustar00rootroot00000000000000require '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.6.0/test/files/models/traffic_light.rb000066400000000000000000000013301444665775700232300ustar00rootroot00000000000000class 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.6.0/test/files/models/vehicle.rb000066400000000000000000000061361444665775700220530ustar00rootroot00000000000000require_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 => lambda { |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 [: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 [: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.6.0/test/files/node.rb000066400000000000000000000001231444665775700200640ustar00rootroot00000000000000class Node < Struct.new(:name, :value, :machine) def context yield end end state_machines-0.6.0/test/files/switch.rb000066400000000000000000000003421444665775700204430ustar00rootroot00000000000000class 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.6.0/test/functional/000077500000000000000000000000001444665775700176565ustar00rootroot00000000000000state_machines-0.6.0/test/functional/auto_shop_available_test.rb000066400000000000000000000006251444665775700252460ustar00rootroot00000000000000require '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.6.0/test/functional/auto_shop_busy_test.rb000066400000000000000000000010141444665775700243010ustar00rootroot00000000000000require '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.6.0/test/functional/car_backing_up_test.rb000066400000000000000000000013561444665775700241760ustar00rootroot00000000000000require '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.6.0/test/functional/car_test.rb000066400000000000000000000015041444665775700220070ustar00rootroot00000000000000require 'test_helper' require 'files/models/car' class CarTest < MiniTest::Test def setup @car = Car.new end def test_should_be_in_parked_state assert_equal 'parked', @car.state end def test_should_not_have_the_seatbelt_on refute @car.seatbelt_on end def test_should_not_allow_park refute @car.park end def test_should_allow_ignite assert @car.ignite assert_equal 'idling', @car.state end def test_should_not_allow_idle refute @car.idle end def test_should_not_allow_shift_up refute @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_allow_reverse assert @car.reverse end end state_machines-0.6.0/test/functional/driver_default_nonstandard_test.rb000066400000000000000000000004231444665775700266330ustar00rootroot00000000000000require '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.6.0/test/functional/hybrid_car_test.rb000066400000000000000000000042761444665775700233610ustar00rootroot00000000000000require '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 @hybrid_car.electric? assert_equal @hybrid_car.propulsion_mode, 'electric' assert_equal @hybrid_car.driving_profile, :eco end def test_should_accept_keyword_argument assert @hybrid_car.go_gas(driving_profile: :sport) assert @hybrid_car.gas? assert_equal @hybrid_car.propulsion_mode, 'gas' assert_equal @hybrid_car.driving_profile, :sport end def test_should_accept_positional_and_keyword_arguments assert @hybrid_car.go_back_in_time(1995, driving_profile: '1.21 gigawatts') assert @hybrid_car.flux_capacitor? assert_equal @hybrid_car.target_year, 1995 assert_equal @hybrid_car.propulsion_mode, 'flux_capacitor' assert_equal @hybrid_car.driving_profile, '1.21 gigawatts' end def test_should_accept_positional_arguments_in_unsafe_method assert @hybrid_car.go_green!(:eco) assert @hybrid_car.electric? assert_equal @hybrid_car.propulsion_mode, 'electric' assert_equal @hybrid_car.driving_profile, :eco end def test_should_accept_keyword_argument_in_unsafe_method assert @hybrid_car.go_gas!(driving_profile: :sport) assert @hybrid_car.gas? assert_equal @hybrid_car.propulsion_mode, 'gas' assert_equal @hybrid_car.driving_profile, :sport 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 @hybrid_car.flux_capacitor? assert_equal @hybrid_car.target_year, 1995 assert_equal @hybrid_car.propulsion_mode, 'flux_capacitor' assert_equal @hybrid_car.driving_profile, '1.21 gigawatts' end def test_should_accept_hashes_as_option assert @hybrid_car.teleport('wakanda', { engine: :nuclear } , { world: :parallel } ) assert_equal @hybrid_car.destination, 'wakanda' assert_equal({ engine: :nuclear }, @hybrid_car.energy_source) assert_equal({ world: :parallel }, @hybrid_car.universe) end end state_machines-0.6.0/test/functional/motorcycle_test.rb000066400000000000000000000020611444665775700234210ustar00rootroot00000000000000require '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_equal 0.0, @motorcycle.decibels end def test_should_use_decibels_defined_in_state @motorcycle.shift_up assert_equal 1.0, @motorcycle.decibels end def test_should_not_inherit_from_superclass_if_value_is_set vehicle = Vehicle.new @motorcycle.shift_up assert_equal 0.0, vehicle.decibels end end state_machines-0.6.0/test/functional/traffic_light_caution_test.rb000066400000000000000000000005711444665775700255740ustar00rootroot00000000000000require '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_equal true, @light.capture_violations? end end state_machines-0.6.0/test/functional/traffic_light_proceed_test.rb000066400000000000000000000005711444665775700255530ustar00rootroot00000000000000require '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 assert_equal false, @light.capture_violations? end end state_machines-0.6.0/test/functional/traffic_light_stop_test.rb000066400000000000000000000011061444665775700251120ustar00rootroot00000000000000require '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_equal true, @light.capture_violations? end end state_machines-0.6.0/test/functional/vehicle_first_gear_test.rb000066400000000000000000000013621444665775700250700ustar00rootroot00000000000000require '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 @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.6.0/test/functional/vehicle_idling_test.rb000066400000000000000000000020161444665775700242060ustar00rootroot00000000000000require 'test_helper' require 'files/models/vehicle' class VehicleIdlingTest < MiniTest::Test def setup @vehicle = Vehicle.new @vehicle.ignite end def test_should_be_in_idling_state assert_equal 'idling', @vehicle.state end def test_should_be_idling assert @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 @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 refute @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_not_allow_crash refute @vehicle.crash end def test_should_not_allow_repair refute @vehicle.repair end end state_machines-0.6.0/test/functional/vehicle_locked_test.rb000066400000000000000000000010661444665775700242050ustar00rootroot00000000000000require '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 @vehicle.parked? end def test_should_be_parked_after_ignite @vehicle.ignite assert @vehicle.parked? end def test_should_be_parked_after_shift_up @vehicle.shift_up assert @vehicle.parked? end def test_should_be_parked_after_shift_down @vehicle.shift_down assert @vehicle.parked? end end state_machines-0.6.0/test/functional/vehicle_parked_test.rb000066400000000000000000000022321444665775700242060ustar00rootroot00000000000000require 'test_helper' require 'files/models/vehicle' class VehicleParkedTest < MiniTest::Test 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 @vehicle.ignite assert_equal 'idling', @vehicle.state 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_raise_exception_if_repair_not_allowed! exception = assert_raises(StateMachines::InvalidTransition) { @vehicle.repair! } assert_equal @vehicle, exception.object assert_equal Vehicle.state_machine(:state), exception.machine assert_equal :repair, exception.event assert_equal 'parked', exception.from end end state_machines-0.6.0/test/functional/vehicle_repaired_test.rb000066400000000000000000000006251444665775700245370ustar00rootroot00000000000000require '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 @vehicle.auto_shop.available? end end state_machines-0.6.0/test/functional/vehicle_second_gear_test.rb000066400000000000000000000014071444665775700252140ustar00rootroot00000000000000require '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 @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.6.0/test/functional/vehicle_stalled_test.rb000066400000000000000000000024461444665775700243770ustar00rootroot00000000000000require '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 @vehicle.stalled? end def test_should_be_towed assert @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.6.0/test/functional/vehicle_test.rb000066400000000000000000000007231444665775700226630ustar00rootroot00000000000000require '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 @vehicle.respond_to?(: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.6.0/test/functional/vehicle_third_gear_test.rb000066400000000000000000000014061444665775700250520ustar00rootroot00000000000000require '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 @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.6.0/test/functional/vehicle_unsaved_test.rb000066400000000000000000000107721444665775700244150ustar00rootroot00000000000000require '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 @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 @vehicle.idling? end def test_should_not_be_first_gear refute @vehicle.first_gear? end def test_should_not_be_second_gear refute @vehicle.second_gear? end def test_should_not_be_stalled refute @vehicle.stalled? end def test_should_not_be_able_to_park refute @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 @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) assert_equal false, @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 @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 @vehicle.new_record? end def test_should_be_saved_after_successful_event @vehicle.ignite refute @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 @vehicle.insurance_inactive? end def test_should_be_able_to_buy assert @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 @vehicle.new_record? end def test_should_not_be_insurance_active refute @vehicle.insurance_active? end def test_should_not_be_able_to_cancel refute @vehicle.can_cancel_insurance? end def test_should_not_allow_cancelling_insurance refute @vehicle.cancel_insurance end end state_machines-0.6.0/test/functional/vehicle_with_event_attributes_test.rb000066400000000000000000000013241444665775700273630ustar00rootroot00000000000000require '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.6.0/test/functional/vehicle_with_parallel_events_test.rb000066400000000000000000000021671444665775700271620ustar00rootroot00000000000000require 'test_helper' require 'files/models/vehicle' class VehicleWithParallelEventsTest < MiniTest::Test def setup @vehicle = Vehicle.new end def test_should_fail_if_any_event_cannot_transition refute @vehicle.fire_events(:ignite, :cancel_insurance) end def test_should_be_successful_if_all_events_transition assert @vehicle.fire_events(:ignite, :buy_insurance) end def test_should_not_save_if_skipping_action assert @vehicle.fire_events(:ignite, :buy_insurance, false) refute @vehicle.saved end def test_should_raise_exception_if_any_event_cannot_transition_on_bang exception = assert_raises(StateMachines::InvalidParallelTransition) { @vehicle.fire_events!(:ignite, :cancel_insurance) } assert_equal @vehicle, exception.object assert_equal [:ignite, :cancel_insurance], exception.events end def test_should_not_raise_exception_if_all_events_transition_on_bang assert @vehicle.fire_events!(:ignite, :buy_insurance) end def test_should_not_save_if_skipping_action_on_bang assert @vehicle.fire_events!(:ignite, :buy_insurance, false) refute @vehicle.saved end end state_machines-0.6.0/test/test_helper.rb000066400000000000000000000004771444665775700203670ustar00rootroot00000000000000require 'state_machines' require 'minitest/autorun' begin require 'pry-byebug' rescue LoadError end require 'minitest/reporters' Minitest::Reporters.use! [Minitest::Reporters::ProgressReporter.new] class StateMachinesTest < MiniTest::Test def before_setup super StateMachines::Integrations.reset end end state_machines-0.6.0/test/unit/000077500000000000000000000000001444665775700164735ustar00rootroot00000000000000state_machines-0.6.0/test/unit/assertions/000077500000000000000000000000001444665775700206655ustar00rootroot00000000000000state_machines-0.6.0/test/unit/assertions/assert_exclusive_keys_test.rb000066400000000000000000000016051444665775700266760ustar00rootroot00000000000000require 'test_helper' class AssertExclusiveKeysTest < StateMachinesTest def test_should_not_raise_exception_if_no_keys_found { on: :park }.assert_exclusive_keys(:only, :except) end def test_should_not_raise_exception_if_one_key_found { only: :parked }.assert_exclusive_keys(:only, :except) { except: :parked }.assert_exclusive_keys(:only, :except) end def test_should_raise_exception_if_two_keys_found exception = assert_raises(ArgumentError) { { only: :parked, except: :parked }.assert_exclusive_keys(:only, :except) } assert_equal 'Conflicting keys: only, except', exception.message end def test_should_raise_exception_if_multiple_keys_found exception = assert_raises(ArgumentError) { { only: :parked, except: :parked, on: :park }.assert_exclusive_keys(:only, :except, :with) } assert_equal 'Conflicting keys: only, except', exception.message end end state_machines-0.6.0/test/unit/assertions/assert_valid_key_test.rb000066400000000000000000000007651444665775700256110ustar00rootroot00000000000000require 'test_helper' class AssertValidKeysTest < StateMachinesTest def test_should_not_raise_exception_if_key_is_valid { name: 'foo', value: 'bar' }.assert_valid_keys(:name, :value, :force) end def test_should_raise_exception_if_key_is_invalid exception = assert_raises(ArgumentError) { { name: 'foo', value: 'bar', invalid: true }.assert_valid_keys(:name, :value, :force) } assert_equal 'Unknown key: :invalid. Valid keys are: :name, :value, :force', exception.message end end state_machines-0.6.0/test/unit/branch/000077500000000000000000000000001444665775700177305ustar00rootroot00000000000000state_machines-0.6.0/test/unit/branch/branch_test.rb000066400000000000000000000014701444665775700225530ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_conflicting_conditionals_test.rb000066400000000000000000000015621444665775700307350ustar00rootroot00000000000000require '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: lambda { true }, unless: lambda { false }) assert branch.match(@object) end def test_should_not_match_if_if_is_false_and_unless_is_true branch = StateMachines::Branch.new(if: lambda { false }, unless: lambda { true }) refute branch.match(@object) end def test_should_not_match_if_if_is_false_and_unless_is_false branch = StateMachines::Branch.new(if: lambda { false }, unless: lambda { false }) refute branch.match(@object) end def test_should_not_match_if_if_is_true_and_unless_is_true branch = StateMachines::Branch.new(if: lambda { true }, unless: lambda { true }) refute branch.match(@object) end end state_machines-0.6.0/test/unit/branch/branch_with_conflicting_from_requirements_test.rb000066400000000000000000000005041444665775700320100ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_conflicting_on_requirements_test.rb000066400000000000000000000004721444665775700314650ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_conflicting_to_requirements_test.rb000066400000000000000000000004721444665775700314730ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_different_requirements_test.rb000066400000000000000000000022071444665775700304360ustar00rootroot00000000000000require '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 [: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.6.0/test/unit/branch/branch_with_except_from_matcher_requirement_test.rb000066400000000000000000000005501444665775700323220ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_except_from_requirement_test.rb000066400000000000000000000016351444665775700306240ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_except_on_matcher_requirement_test.rb000066400000000000000000000005401444665775700317720ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_except_on_requirement_test.rb000066400000000000000000000015741444665775700302770ustar00rootroot00000000000000require '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_equal [], @branch.known_states end end state_machines-0.6.0/test/unit/branch/branch_with_except_to_matcher_requirement_test.rb000066400000000000000000000005401444665775700320000ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_except_to_requirement_test.rb000066400000000000000000000016211444665775700302760ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_from_matcher_requirement_test.rb000066400000000000000000000010411444665775700307460ustar00rootroot00000000000000require 'test_helper' class BranchWithFromMatcherRequirementTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(from: StateMachines::BlacklistMatcher.new([: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 [:idling, :parked], @branch.known_states end end state_machines-0.6.0/test/unit/branch/branch_with_from_requirement_test.rb000066400000000000000000000022451444665775700272520ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_if_conditional_test.rb000066400000000000000000000012541444665775700266470ustar00rootroot00000000000000require 'test_helper' class BranchWithIfConditionalTest < StateMachinesTest def setup @object = Object.new end def test_should_have_an_if_condition branch = StateMachines::Branch.new(if: lambda { true }) refute_nil branch.if_condition end def test_should_match_if_true branch = StateMachines::Branch.new(if: lambda { true }) assert branch.matches?(@object) end def test_should_not_match_if_false branch = StateMachines::Branch.new(if: lambda { false }) refute branch.matches?(@object) end def test_should_be_nil_if_unmatched branch = StateMachines::Branch.new(if: lambda { false }) assert_nil branch.match(@object) end end state_machines-0.6.0/test/unit/branch/branch_with_implicit_and_explicit_requirements_test.rb000066400000000000000000000014611444665775700330260ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_implicit_from_requirement_matcher_test.rb000066400000000000000000000010021444665775700326350ustar00rootroot00000000000000require 'test_helper' class BranchWithMultipleFromRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(from: [: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 [:idling, :parked], @branch.known_states end end state_machines-0.6.0/test/unit/branch/branch_with_implicit_requirement_test.rb000066400000000000000000000012331444665775700301150ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_implicit_to_requirement_matcher_test.rb000066400000000000000000000010011444665775700323130ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_multiple_except_from_requirements_test.rb000066400000000000000000000010171444665775700327140ustar00rootroot00000000000000require 'test_helper' class BranchWithMultipleExceptFromRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(except_from: [: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 [:idling, :parked], @branch.known_states end end state_machines-0.6.0/test/unit/branch/branch_with_multiple_except_on_requirements_test.rb000066400000000000000000000006241444665775700323700ustar00rootroot00000000000000require 'test_helper' class BranchWithMultipleExceptOnRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(except_on: [: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.6.0/test/unit/branch/branch_with_multiple_except_to_requirements_test.rb000066400000000000000000000010071444665775700323720ustar00rootroot00000000000000require 'test_helper' class BranchWithMultipleExceptToRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(except_to: [: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 [:idling, :parked], @branch.known_states end end state_machines-0.6.0/test/unit/branch/branch_with_multiple_from_requirements_test.rb000066400000000000000000000010061444665775700313420ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_multiple_if_conditionals_test.rb000066400000000000000000000010731444665775700307440ustar00rootroot00000000000000require 'test_helper' class BranchWithMultipleIfConditionalsTest < StateMachinesTest def setup @object = Object.new end def test_should_match_if_all_are_true branch = StateMachines::Branch.new(if: [lambda { true }, lambda { true }]) assert branch.match(@object) end def test_should_not_match_if_any_are_false branch = StateMachines::Branch.new(if: [lambda { true }, lambda { false }]) refute branch.match(@object) branch = StateMachines::Branch.new(if: [lambda { false }, lambda { true }]) refute branch.match(@object) end end state_machines-0.6.0/test/unit/branch/branch_with_multiple_implicit_requirements_test.rb000066400000000000000000000034421444665775700322170ustar00rootroot00000000000000require '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 [: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 [:first_gear, :idling, :parked], branch.known_states.sort_by { |state| state.to_s } end end state_machines-0.6.0/test/unit/branch/branch_with_multiple_to_requirements_test.rb000066400000000000000000000007721444665775700310320ustar00rootroot00000000000000require 'test_helper' class BranchWithMultipleToRequirementsTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(to: [: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 [:idling, :parked], @branch.known_states end end state_machines-0.6.0/test/unit/branch/branch_with_multiple_unless_conditionals_test.rb000066400000000000000000000011151444665775700316540ustar00rootroot00000000000000require 'test_helper' class BranchWithMultipleUnlessConditionalsTest < StateMachinesTest def setup @object = Object.new end def test_should_match_if_all_are_false branch = StateMachines::Branch.new(unless: [lambda { false }, lambda { false }]) assert branch.match(@object) end def test_should_not_match_if_any_are_true branch = StateMachines::Branch.new(unless: [lambda { true }, lambda { false }]) refute branch.match(@object) branch = StateMachines::Branch.new(unless: [lambda { false }, lambda { true }]) refute branch.match(@object) end end state_machines-0.6.0/test/unit/branch/branch_with_nil_requirements_test.rb000066400000000000000000000012571444665775700272560ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_no_requirements_test.rb000066400000000000000000000021461444665775700271060ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_on_matcher_requirement_test.rb000066400000000000000000000006521444665775700304260ustar00rootroot00000000000000require 'test_helper' class BranchWithOnMatcherRequirementTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(on: StateMachines::BlacklistMatcher.new([: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.6.0/test/unit/branch/branch_with_on_requirement_test.rb000066400000000000000000000021701444665775700267200ustar00rootroot00000000000000require '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_equal [], @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.6.0/test/unit/branch/branch_with_to_matcher_requirement_test.rb000066400000000000000000000010311444665775700304240ustar00rootroot00000000000000require 'test_helper' class BranchWithToMatcherRequirementTest < StateMachinesTest def setup @object = Object.new @branch = StateMachines::Branch.new(to: StateMachines::BlacklistMatcher.new([: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 [:idling, :parked], @branch.known_states end end state_machines-0.6.0/test/unit/branch/branch_with_to_requirement_test.rb000066400000000000000000000022251444665775700267270ustar00rootroot00000000000000require '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.6.0/test/unit/branch/branch_with_unless_conditional_test.rb000066400000000000000000000013071444665775700275610ustar00rootroot00000000000000require 'test_helper' class BranchWithUnlessConditionalTest < StateMachinesTest def setup @object = Object.new end def test_should_have_an_unless_condition branch = StateMachines::Branch.new(unless: lambda { true }) refute_nil branch.unless_condition end def test_should_match_if_false branch = StateMachines::Branch.new(unless: lambda { false }) assert branch.matches?(@object) end def test_should_not_match_if_true branch = StateMachines::Branch.new(unless: lambda { true }) refute branch.matches?(@object) end def test_should_be_nil_if_unmatched branch = StateMachines::Branch.new(unless: lambda { true }) assert_nil branch.match(@object) end end state_machines-0.6.0/test/unit/branch/branch_without_guards_test.rb000066400000000000000000000013701444665775700257020ustar00rootroot00000000000000require 'test_helper' class BranchWithoutGuardsTest < StateMachinesTest def setup @object = Object.new end def test_should_match_if_if_is_false branch = StateMachines::Branch.new(if: lambda { false }) assert branch.matches?(@object, guard: false) end def test_should_match_if_if_is_true branch = StateMachines::Branch.new(if: lambda { true }) assert branch.matches?(@object, guard: false) end def test_should_match_if_unless_is_false branch = StateMachines::Branch.new(unless: lambda { false }) assert branch.matches?(@object, guard: false) end def test_should_match_if_unless_is_true branch = StateMachines::Branch.new(unless: lambda { true }) assert branch.matches?(@object, guard: false) end end state_machines-0.6.0/test/unit/callback/000077500000000000000000000000001444665775700202275ustar00rootroot00000000000000state_machines-0.6.0/test/unit/callback/callback_by_default_test.rb000066400000000000000000000013721444665775700255500ustar00rootroot00000000000000require '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_equal [], @callback.known_states end end state_machines-0.6.0/test/unit/callback/callback_test.rb000066400000000000000000000032341444665775700233510ustar00rootroot00000000000000require '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.6.0/test/unit/callback/callback_with_application_bound_object_test.rb000066400000000000000000000011501444665775700314770ustar00rootroot00000000000000require '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: lambda { |*_args| context = self }) @object = Object.new @callback.call(@object) @context = context end def test_should_call_method_within_the_context_of_the_object assert_equal @object, @context end def teardown StateMachines::Callback.bind_to_object = @original_bind_to_object end end state_machines-0.6.0/test/unit/callback/callback_with_application_terminator_test.rb000066400000000000000000000012731444665775700312340ustar00rootroot00000000000000require 'test_helper' class CallbackWithApplicationTerminatorTest < StateMachinesTest def setup @original_terminator = StateMachines::Callback.terminator StateMachines::Callback.terminator = lambda { |result| result == false } @object = Object.new end def test_should_not_halt_if_terminator_does_not_match callback = StateMachines::Callback.new(:before, do: lambda { true }) callback.call(@object) end def test_should_halt_if_terminator_matches callback = StateMachines::Callback.new(:before, do: lambda { false }) assert_throws(:halt) { callback.call(@object) } end def teardown StateMachines::Callback.terminator = @original_terminator end end state_machines-0.6.0/test/unit/callback/callback_with_arguments_test.rb000066400000000000000000000005411444665775700264670ustar00rootroot00000000000000require 'test_helper' class CallbackWithArgumentsTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, do: lambda { |*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.6.0/test/unit/callback/callback_with_around_type_and_arguments_test.rb000066400000000000000000000016011444665775700317200ustar00rootroot00000000000000require '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.6.0/test/unit/callback/callback_with_around_type_and_block_test.rb000066400000000000000000000030641444665775700310120ustar00rootroot00000000000000require '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, lambda { |_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 [: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 [: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_equal [], @callbacks end end state_machines-0.6.0/test/unit/callback/callback_with_around_type_and_bound_method_test.rb000066400000000000000000000013171444665775700323660ustar00rootroot00000000000000require '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.6.0/test/unit/callback/callback_with_around_type_and_multiple_methods_test.rb000066400000000000000000000046451444665775700333040ustar00rootroot00000000000000require 'test_helper' class CallbackWithAroundTypeAndMultipleMethodsTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:around, :run_1, :run_2) class << @object = Object.new attr_accessor :before_callbacks attr_accessor :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 [:run_1, :run_2], @object.before_callbacks end def test_should_evaluate_after_callbacks_in_reverse_order @callback.call(@object) assert_equal [: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 [: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 [: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 [: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 [:run_1, :run_2], @object.before_callbacks assert_equal [:run_2], @object.after_callbacks end end state_machines-0.6.0/test/unit/callback/callback_with_around_type_and_terminator_test.rb000066400000000000000000000011401444665775700320750ustar00rootroot00000000000000require '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: lambda { |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: lambda { |result| result == false }) callback.call(@object) end end state_machines-0.6.0/test/unit/callback/callback_with_block_test.rb000066400000000000000000000006051444665775700255550ustar00rootroot00000000000000require '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.6.0/test/unit/callback/callback_with_bound_method_and_arguments_test.rb000066400000000000000000000016731444665775700320470ustar00rootroot00000000000000require '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: lambda { |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: lambda { |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: lambda { |*args| context = args }, bind_to_object: true) callback.call(@object, {}, 1, 2, 3) assert_equal [1, 2, 3], context end end state_machines-0.6.0/test/unit/callback/callback_with_bound_method_test.rb000066400000000000000000000017011444665775700271300ustar00rootroot00000000000000require '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: lambda { |*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_equal [], @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.6.0/test/unit/callback/callback_with_do_method_test.rb000066400000000000000000000006071444665775700264270ustar00rootroot00000000000000require 'test_helper' class CallbackWithDoMethodTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, do: lambda { |*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.6.0/test/unit/callback/callback_with_explicit_requirements_test.rb000066400000000000000000000015521444665775700311110ustar00rootroot00000000000000require 'test_helper' class CallbackWithExplicitRequirementsTest < StateMachinesTest def setup @object = Object.new @callback = StateMachines::Callback.new(:before, from: :parked, to: :idling, on: :ignite, do: lambda {}) 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 [:parked, :idling], @callback.known_states end end state_machines-0.6.0/test/unit/callback/callback_with_if_condition_test.rb000066400000000000000000000007021444665775700271250ustar00rootroot00000000000000require 'test_helper' class CallbackWithIfConditionTest < StateMachinesTest def setup @object = Object.new end def test_should_call_if_true callback = StateMachines::Callback.new(:before, if: lambda { true }, do: lambda {}) assert callback.call(@object) end def test_should_not_call_if_false callback = StateMachines::Callback.new(:before, if: lambda { false }, do: lambda {}) refute callback.call(@object) end end state_machines-0.6.0/test/unit/callback/callback_with_implicit_requirements_test.rb000066400000000000000000000015371444665775700311050ustar00rootroot00000000000000require 'test_helper' class CallbackWithImplicitRequirementsTest < StateMachinesTest def setup @object = Object.new @callback = StateMachines::Callback.new(:before, parked: :idling, on: :ignite, do: lambda {}) 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 [:parked, :idling], @callback.known_states end end state_machines-0.6.0/test/unit/callback/callback_with_method_argument_test.rb000066400000000000000000000006111444665775700276420ustar00rootroot00000000000000require 'test_helper' class CallbackWithMethodArgumentTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, lambda { |*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.6.0/test/unit/callback/callback_with_mixed_methods_test.rb000066400000000000000000000012211444665775700273070ustar00rootroot00000000000000require '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 [:argument, :do, :block], @object.callbacks end end state_machines-0.6.0/test/unit/callback/callback_with_multiple_bound_methods_test.rb000066400000000000000000000011171444665775700312270ustar00rootroot00000000000000require 'test_helper' class CallbackWithMultipleBoundMethodsTest < StateMachinesTest def setup @object = Object.new first_context = nil second_context = nil @callback = StateMachines::Callback.new(:before, do: [lambda { first_context = self }, lambda { 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.6.0/test/unit/callback/callback_with_multiple_do_methods_test.rb000066400000000000000000000011131444665775700305160ustar00rootroot00000000000000require 'test_helper' class CallbackWithMultipleDoMethodsTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, do: [: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 [:run_1, :run_2], @object.callbacks end end state_machines-0.6.0/test/unit/callback/callback_with_multiple_method_arguments_test.rb000066400000000000000000000011131444665775700317360ustar00rootroot00000000000000require '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 [:run_1, :run_2], @object.callbacks end end state_machines-0.6.0/test/unit/callback/callback_with_terminator_test.rb000066400000000000000000000014631444665775700266520ustar00rootroot00000000000000require '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: lambda { false }, terminator: lambda { |result| result == true }) callback.call(@object) end def test_should_halt_if_terminator_matches callback = StateMachines::Callback.new(:before, do: lambda { false }, terminator: lambda { |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: [lambda { true }, lambda { false }], terminator: lambda { |result| result == false }) assert_throws(:halt) { callback.call(@object) } end end state_machines-0.6.0/test/unit/callback/callback_with_unbound_method_test.rb000066400000000000000000000006161444665775700274770ustar00rootroot00000000000000require 'test_helper' class CallbackWithUnboundMethodTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, do: lambda { |*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.6.0/test/unit/callback/callback_with_unless_condition_test.rb000066400000000000000000000007161444665775700300450ustar00rootroot00000000000000require 'test_helper' class CallbackWithUnlessConditionTest < StateMachinesTest def setup @object = Object.new end def test_should_call_if_false callback = StateMachines::Callback.new(:before, unless: lambda { false }, do: lambda {}) assert callback.call(@object) end def test_should_not_call_if_true callback = StateMachines::Callback.new(:before, unless: lambda { true }, do: lambda {}) refute callback.call(@object) end end state_machines-0.6.0/test/unit/callback/callback_without_arguments_test.rb000066400000000000000000000005371444665775700272240ustar00rootroot00000000000000require 'test_helper' class CallbackWithoutArgumentsTest < StateMachinesTest def setup @callback = StateMachines::Callback.new(:before, do: lambda { |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.6.0/test/unit/callback/callback_without_terminator_test.rb000066400000000000000000000004511444665775700273760ustar00rootroot00000000000000require '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: lambda { false }, terminator: nil) callback.call(@object) end end state_machines-0.6.0/test/unit/error/000077500000000000000000000000001444665775700176245ustar00rootroot00000000000000state_machines-0.6.0/test/unit/error/error_by_default_test.rb000066400000000000000000000010071444665775700245350ustar00rootroot00000000000000require '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.6.0/test/unit/error/error_with_message_test.rb000066400000000000000000000016271444665775700251060ustar00rootroot00000000000000require '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.6.0/test/unit/eval_helper/000077500000000000000000000000001444665775700207615ustar00rootroot00000000000000state_machines-0.6.0/test/unit/eval_helper/eval_helpers_base_test.rb000066400000000000000000000002101444665775700260010ustar00rootroot00000000000000require 'test_helper' class EvalHelpersBaseTest < StateMachinesTest include StateMachines::EvalHelpers def default_test end end state_machines-0.6.0/test/unit/eval_helper/eval_helpers_proc_block_and_explicit_arguments_test.rb000066400000000000000000000007411444665775700340250ustar00rootroot00000000000000require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcBlockAndExplicitArgumentsTest < EvalHelpersBaseTest def setup @object = Object.new @proc = lambda { |object, arg1, arg2, arg3, block| [object, arg1, arg2, arg3, block] } end def test_should_call_method_on_object_with_all_arguments_and_block block = lambda { true } assert_equal [@object, 1, 2, 3, block], evaluate_method(@object, @proc, 1, 2, 3, &block) end end state_machines-0.6.0/test/unit/eval_helper/eval_helpers_proc_block_and_implicit_arguments_test.rb000066400000000000000000000006521444665775700340170ustar00rootroot00000000000000require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcBlockAndImplicitArgumentsTest < EvalHelpersBaseTest def setup @object = Object.new @proc = lambda { |*args| args } end def test_should_call_method_on_object_with_all_arguments_and_block block = lambda { true } assert_equal [@object, 1, 2, 3, block], evaluate_method(@object, @proc, 1, 2, 3, &block) end end state_machines-0.6.0/test/unit/eval_helper/eval_helpers_proc_test.rb000066400000000000000000000005021444665775700260360ustar00rootroot00000000000000require '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.6.0/test/unit/eval_helper/eval_helpers_proc_with_arguments_test.rb000066400000000000000000000005361444665775700311650ustar00rootroot00000000000000require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcWithArgumentsTest < EvalHelpersBaseTest def setup @object = Object.new @proc = lambda { |*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 endstate_machines-0.6.0/test/unit/eval_helper/eval_helpers_proc_with_block_test.rb000066400000000000000000000005441444665775700302510ustar00rootroot00000000000000require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcWithBlockTest < EvalHelpersBaseTest def setup @object = Object.new @proc = lambda { |_obj, block| block.call } end def test_should_call_method_on_object_with_block assert_equal true, evaluate_method(@object, @proc, 1, 2, 3) { true } end end state_machines-0.6.0/test/unit/eval_helper/eval_helpers_proc_with_block_without_arguments_test.rb000066400000000000000000000006161444665775700341210ustar00rootroot00000000000000require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcWithoutArgumentsTest < EvalHelpersBaseTest def setup @object = Object.new @proc = lambda { |*args| args } class << @proc def arity 0 end end end def test_should_call_proc_with_no_arguments assert_equal [], evaluate_method(@object, @proc, 1, 2, 3) end end state_machines-0.6.0/test/unit/eval_helper/eval_helpers_proc_with_block_without_object_test.rb000066400000000000000000000005771444665775700333700ustar00rootroot00000000000000require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcWithBlockWithoutObjectTest < EvalHelpersBaseTest def setup @object = Object.new @proc = lambda { |block| [block] } end def test_should_call_proc_with_block_only block = lambda { true } assert_equal [block], evaluate_method(@object, @proc, 1, 2, 3, &block) end end state_machines-0.6.0/test/unit/eval_helper/eval_helpers_proc_without_arguments_test.rb000066400000000000000000000006731444665775700317170ustar00rootroot00000000000000require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersProcWithBlockWithoutArgumentsTest < EvalHelpersBaseTest def setup @object = Object.new @proc = lambda { |*args| args } class << @proc def arity 0 end end end def test_should_call_proc_without_arguments block = lambda { true } assert_equal [], evaluate_method(@object, @proc, 1, 2, 3, &block) end end state_machines-0.6.0/test/unit/eval_helper/eval_helpers_string_test.rb000066400000000000000000000010411444665775700264000ustar00rootroot00000000000000require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test.rb' 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.6.0/test/unit/eval_helper/eval_helpers_string_with_block_test.rb000066400000000000000000000004511444665775700306110ustar00rootroot00000000000000require '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.6.0/test/unit/eval_helper/eval_helpers_symbol_method_missing_test.rb000066400000000000000000000007651444665775700315040ustar00rootroot00000000000000require 'test_helper' require 'unit/eval_helper/eval_helpers_base_test' class EvalHelpersSymbolMethodMissingTest < EvalHelpersBaseTest def setup class << (@object = Object.new) def method_missing(symbol, *args) send("method_missing_#{symbol}", *args) 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 endstate_machines-0.6.0/test/unit/eval_helper/eval_helpers_symbol_private_test.rb000066400000000000000000000006011444665775700301320ustar00rootroot00000000000000require '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_equal true, evaluate_method(@object, :callback, 1, 2, 3) end end state_machines-0.6.0/test/unit/eval_helper/eval_helpers_symbol_protected_test.rb000066400000000000000000000006051444665775700304550ustar00rootroot00000000000000require '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_equal true, evaluate_method(@object, :callback, 1, 2, 3) end end state_machines-0.6.0/test/unit/eval_helper/eval_helpers_symbol_tainted_method_test.rb000066400000000000000000000006771444665775700314650ustar00rootroot00000000000000require '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.6.0/test/unit/eval_helper/eval_helpers_symbol_test.rb000066400000000000000000000005541444665775700264070ustar00rootroot00000000000000require '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_equal true, evaluate_method(@object, :callback, 1, 2, 3) end end state_machines-0.6.0/test/unit/eval_helper/eval_helpers_symbol_with_arguments_and_block_test.rb000066400000000000000000000006601444665775700335210ustar00rootroot00000000000000require '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.6.0/test/unit/eval_helper/eval_helpers_symbol_with_arguments_test.rb000066400000000000000000000005741444665775700315310ustar00rootroot00000000000000require '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.6.0/test/unit/eval_helper/eval_helpers_symbol_with_block_test.rb000066400000000000000000000005571444665775700306170ustar00rootroot00000000000000require '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_equal true, evaluate_method(@object, :callback) { true } end end state_machines-0.6.0/test/unit/eval_helper/eval_helpers_test.rb000066400000000000000000000005651444665775700250240ustar00rootroot00000000000000require '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.6.0/test/unit/event/000077500000000000000000000000001444665775700176145ustar00rootroot00000000000000state_machines-0.6.0/test/unit/event/event_after_being_copied_test.rb000066400000000000000000000010011444665775700261610ustar00rootroot00000000000000require '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.6.0/test/unit/event/event_by_default_test.rb000066400000000000000000000024351444665775700245230ustar00rootroot00000000000000require '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 @event.branches.empty? end def test_should_have_no_known_states assert @event.known_states.empty? 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 @object.respond_to?(:can_ignite?) end def test_should_define_a_transition_accessor assert @object.respond_to?(:ignite_transition) end def test_should_define_an_action assert @object.respond_to?(:ignite) end def test_should_define_a_bang_action assert @object.respond_to?(:ignite!) end end state_machines-0.6.0/test/unit/event/event_context_test.rb000066400000000000000000000006111444665775700240630ustar00rootroot00000000000000require '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.6.0/test/unit/event/event_on_failure_test.rb000066400000000000000000000031161444665775700245250ustar00rootroot00000000000000require '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 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_equal [], 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 def teardown StateMachines::Integrations.reset end end state_machines-0.6.0/test/unit/event/event_test.rb000066400000000000000000000017561444665775700223320ustar00rootroot00000000000000require '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.6.0/test/unit/event/event_transitions_test.rb000066400000000000000000000036511444665775700247630ustar00rootroot00000000000000require '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', 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', 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([:parked, :idling] => :idling) end def test_should_allow_transitions_to_multiple_states assert @event.transition(parked: [:parked, :idling]) end def test_should_have_transitions branch = @event.transition(to: :idling) assert_equal [branch], @event.branches end end state_machines-0.6.0/test/unit/event/event_with_conflicting_helpers_after_definition_test.rb000066400000000000000000000027401444665775700330510ustar00rootroot00000000000000require '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 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 assert_equal false, @object.can_ignite? assert_equal nil, @object.ignite_transition assert_equal false, @object.ignite assert_raises(StateMachines::InvalidTransition) { @object.ignite! } end def test_should_not_output_warning assert_equal '', $stderr.string end def teardown $stderr = @original_stderr end end state_machines-0.6.0/test/unit/event/event_with_conflicting_helpers_before_definition_test.rb000066400000000000000000000024751444665775700332170ustar00rootroot00000000000000require '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 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 def teardown $stderr = @original_stderr end end state_machines-0.6.0/test/unit/event/event_with_conflicting_machine_test.rb000066400000000000000000000031001444665775700274110ustar00rootroot00000000000000require '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 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 def teardown $stderr = @original_stderr end end state_machines-0.6.0/test/unit/event/event_with_dynamic_human_name_test.rb000066400000000000000000000013361444665775700272530ustar00rootroot00000000000000require '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: lambda { |_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.6.0/test/unit/event/event_with_human_name_test.rb000066400000000000000000000005431444665775700255460ustar00rootroot00000000000000require '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.6.0/test/unit/event/event_with_invalid_current_state_test.rb000066400000000000000000000017751444665775700300360ustar00rootroot00000000000000require '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.6.0/test/unit/event/event_with_machine_action_test.rb000066400000000000000000000012641444665775700264000ustar00rootroot00000000000000require '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.6.0/test/unit/event/event_with_marshalling_test.rb000066400000000000000000000020401444665775700257310ustar00rootroot00000000000000require '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 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 def teardown self.class.send(:remove_const, 'Example') end end state_machines-0.6.0/test/unit/event/event_with_matching_disabled_transitions_test.rb000066400000000000000000000060551444665775700315200ustar00rootroot00000000000000require '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: lambda { false }) @object = @klass.new @object.state = 'parked' 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: lambda { 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 def teardown StateMachines::Integrations.reset end end state_machines-0.6.0/test/unit/event/event_with_matching_enabled_transitions_test.rb000066400000000000000000000032761444665775700313450ustar00rootroot00000000000000require '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 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_equal [], @object.errors end def test_should_not_invalidate_the_state @event.fire(@object) assert_equal [], @object.errors end def test_should_not_be_able_to_fire_on_reset @event.reset refute @event.can_fire?(@object) end def teardown StateMachines::Integrations.reset end end state_machines-0.6.0/test/unit/event/event_with_multiple_transitions_test.rb000066400000000000000000000034171444665775700277310ustar00rootroot00000000000000require '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.6.0/test/unit/event/event_with_namespace_test.rb000066400000000000000000000015261444665775700253740ustar00rootroot00000000000000require '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 @object.respond_to?(:can_enable_alarm?) end def test_should_namespace_transition_accessor assert @object.respond_to?(:enable_alarm_transition) end def test_should_namespace_action assert @object.respond_to?(:enable_alarm) end def test_should_namespace_bang_action assert @object.respond_to?(:enable_alarm!) end end state_machines-0.6.0/test/unit/event/event_with_transition_with_blacklisted_to_state_test.rb000066400000000000000000000035551444665775700331340ustar00rootroot00000000000000require '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([: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([: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.6.0/test/unit/event/event_with_transition_with_loopback_state_test.rb000066400000000000000000000016361444665775700317410ustar00rootroot00000000000000require '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.6.0/test/unit/event/event_with_transition_with_nil_to_state_test.rb000066400000000000000000000015311444665775700314250ustar00rootroot00000000000000require '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.6.0/test/unit/event/event_with_transition_with_whitelisted_to_state_test.rb000066400000000000000000000027371444665775700332010ustar00rootroot00000000000000require '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([: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.6.0/test/unit/event/event_with_transition_without_to_state_test.rb000066400000000000000000000015561444665775700313220ustar00rootroot00000000000000require '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.6.0/test/unit/event/event_with_transitions_test.rb000066400000000000000000000017241444665775700260150ustar00rootroot00000000000000require '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 [: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 [:parked, :idling, :first_gear, :stalled], @event.known_states end def test_should_clear_known_states_on_reset @event.reset assert_equal [], @event.known_states end def test_should_use_pretty_inspect assert_match '# :idling, :first_gear => :idling]>', @event.inspect end end state_machines-0.6.0/test/unit/event/event_without_matching_transitions_test.rb000066400000000000000000000017401444665775700304150ustar00rootroot00000000000000require '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.6.0/test/unit/event/event_without_transitions_test.rb000066400000000000000000000011571444665775700265450ustar00rootroot00000000000000require '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.6.0/test/unit/event/invalid_event_test.rb000066400000000000000000000007521444665775700240330ustar00rootroot00000000000000require '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.6.0/test/unit/event_collection/000077500000000000000000000000001444665775700220275ustar00rootroot00000000000000event_collection_attribute_with_machine_action_test.rb000066400000000000000000000037111444665775700350310ustar00rootroot00000000000000state_machines-0.6.0/test/unit/event_collectionrequire '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' assert_equal false, @events.attribute_transition_for(@object) end def test_should_have_invalid_transition_if_event_cannot_be_fired @object.state_event = 'ignite' assert_equal false, @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.rb000066400000000000000000000020441444665775700356520ustar00rootroot00000000000000state_machines-0.6.0/test/unit/event_collectionrequire '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' assert_equal false, @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.6.0/test/unit/event_collection/event_collection_by_default_test.rb000066400000000000000000000012041444665775700311420ustar00rootroot00000000000000require '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 @events.valid_for(@object).empty? end def test_should_not_have_any_transitions_for_an_object assert @events.transitions_for(@object).empty? end end state_machines-0.6.0/test/unit/event_collection/event_collection_test.rb000066400000000000000000000014771444665775700267600ustar00rootroot00000000000000require '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.rb000066400000000000000000000015741444665775700350730ustar00rootroot00000000000000state_machines-0.6.0/test/unit/event_collectionrequire '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.rb000066400000000000000000000056431444665775700350070ustar00rootroot00000000000000state_machines-0.6.0/test/unit/event_collectionrequire '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: lambda { 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_equal [], @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_equal [], @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.6.0/test/unit/event_collection/event_collection_with_multiple_events_test.rb000066400000000000000000000013141444665775700333000ustar00rootroot00000000000000require '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.6.0/test/unit/event_collection/event_collection_with_validations_test.rb000066400000000000000000000036511444665775700324040ustar00rootroot00000000000000require '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 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_equal [], @object.errors end def teardown StateMachines::Integrations.reset end end state_machines-0.6.0/test/unit/event_collection/event_collection_without_machine_action_test.rb000066400000000000000000000007621444665775700335600ustar00rootroot00000000000000require '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.6.0/test/unit/event_collection/event_string_collection_test.rb000066400000000000000000000014771444665775700303460ustar00rootroot00000000000000require '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.6.0/test/unit/helper_module_test.rb000066400000000000000000000007011444665775700227010ustar00rootroot00000000000000require '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.6.0/test/unit/integrations/000077500000000000000000000000001444665775700212015ustar00rootroot00000000000000state_machines-0.6.0/test/unit/integrations/integration_finder_test.rb000066400000000000000000000007751444665775700264500ustar00rootroot00000000000000require '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_equal([], StateMachines::Integrations.list) end end state_machines-0.6.0/test/unit/integrations/integration_matcher_test.rb000066400000000000000000000016471444665775700266230ustar00rootroot00000000000000require '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.6.0/test/unit/invalid_transition/000077500000000000000000000000001444665775700223735ustar00rootroot00000000000000state_machines-0.6.0/test/unit/invalid_transition/invalid_parallel_transition_test.rb000066400000000000000000000006671444665775700315440ustar00rootroot00000000000000require 'test_helper' class InvalidParallelTransitionTest < StateMachinesTest def setup @object = Object.new @events = [: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.6.0/test/unit/invalid_transition/invalid_transition_test.rb000066400000000000000000000023011444665775700276530ustar00rootroot00000000000000require '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.6.0/test/unit/invalid_transition/invalid_transition_with_integration_test.rb000066400000000000000000000024211444665775700333140ustar00rootroot00000000000000require '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 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 def teardown StateMachines::Integrations.reset end end state_machines-0.6.0/test/unit/invalid_transition/invalid_transition_with_namespace_test.rb000066400000000000000000000015131444665775700327260ustar00rootroot00000000000000require '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.6.0/test/unit/machine/000077500000000000000000000000001444665775700200775ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machine/machine_after_being_copied_test.rb000066400000000000000000000035771444665775700267530ustar00rootroot00000000000000require 'test_helper' class MachineAfterBeingCopiedTest < StateMachinesTest def setup @machine = StateMachines::Machine.new(Class.new, :state, initial: :parked) @machine.event(:ignite) {} @machine.before_transition(lambda {}) @machine.after_transition(lambda {}) @machine.around_transition(lambda {}) @machine.after_failure(lambda {}) @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.6.0/test/unit/machine/machine_after_changing_initial_state.rb000066400000000000000000000012631444665775700277620ustar00rootroot00000000000000require '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 [: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.6.0/test/unit/machine/machine_after_changing_owner_class_test.rb000066400000000000000000000015131444665775700305050ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_by_default_test.rb000066400000000000000000000104121444665775700252630ustar00rootroot00000000000000require '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 @machine.events.any? end def test_should_not_have_any_before_callbacks assert @machine.callbacks[:before].empty? end def test_should_not_have_any_after_callbacks assert @machine.callbacks[:after].empty? end def test_should_not_have_any_failure_callbacks assert @machine.callbacks[:failure].empty? end def test_should_not_have_an_action assert_nil @machine.action end def test_should_use_tranactions assert_equal true, @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, [[:state, :parked]]) assert_equal 'cannot transition via "park"', @machine.generate_message(:invalid_transition, [[:event, :park]]) end def test_should_define_a_reader_attribute_for_the_attribute assert @object.respond_to?(:state) end def test_should_define_a_writer_attribute_for_the_attribute assert @object.respond_to?(:state=) end def test_should_define_a_predicate_for_the_attribute assert @object.respond_to?(:state?) end def test_should_define_a_name_reader_for_the_attribute assert @object.respond_to?(:state_name) end def test_should_define_an_event_reader_for_the_attribute assert @object.respond_to?(:state_events) end def test_should_define_a_transition_reader_for_the_attribute assert @object.respond_to?(:state_transitions) end def test_should_define_a_path_reader_for_the_attribute assert @object.respond_to?(:state_paths) end def test_should_define_an_event_runner_for_the_attribute assert @object.respond_to?(:fire_state_event) end def test_should_not_define_an_event_attribute_reader refute @object.respond_to?(:state_event) end def test_should_not_define_an_event_attribute_writer refute @object.respond_to?(:state_event=) end def test_should_not_define_an_event_transition_attribute_reader refute @object.respond_to?(:state_event_transition) end def test_should_not_define_an_event_transition_attribute_writer refute @object.respond_to?(:state_event_transition=) end def test_should_define_a_human_attribute_name_reader_for_the_attribute assert @klass.respond_to?(:human_state_name) end def test_should_define_a_human_event_name_reader_for_the_attribute assert @klass.respond_to?(:human_state_event_name) end def test_should_not_define_singular_with_scope refute @klass.respond_to?(:with_state) end def test_should_not_define_singular_without_scope refute @klass.respond_to?(:without_state) end def test_should_not_define_plural_with_scope refute @klass.respond_to?(:with_states) end def test_should_not_define_plural_without_scope refute @klass.respond_to?(:without_states) end def test_should_extend_owner_class_with_class_methods assert((class << @klass; ancestors; end).include?(StateMachines::ClassMethods)) end def test_should_include_instance_methods_in_owner_class assert @klass.included_modules.include?(StateMachines::InstanceMethods) end def test_should_define_state_machines_reader expected = { state: @machine } assert_equal expected, @klass.state_machines end end state_machines-0.6.0/test/unit/machine/machine_finder_custom_options_test.rb000066400000000000000000000006721444665775700275700ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_finder_with_existing_machine_on_superclass_test.rb000066400000000000000000000044221444665775700340110ustar00rootroot00000000000000require '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 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(class_ancestors.include?(MachineFinderWithExistingMachineOnSuperclassTest::Custom)) end def teardown StateMachines::Integrations.reset end end state_machines-0.6.0/test/unit/machine/machine_finder_with_existing_on_same_class_test.rb000066400000000000000000000010141444665775700322450ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_finder_without_existing_machine_test.rb000066400000000000000000000010211444665775700315710ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_persistence_test.rb000066400000000000000000000026161444665775700255000ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_state_initialization_test.rb000066400000000000000000000025221444665775700273770ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_test.rb000066400000000000000000000015731444665775700230750ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_action_already_overridden_test.rb000066400000000000000000000011761444665775700314060ustar00rootroot00000000000000require '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 @machine.action_hook? end end state_machines-0.6.0/test/unit/machine/machine_with_action_defined_in_class_test.rb000066400000000000000000000017021444665775700310100ustar00rootroot00000000000000require '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 @object.respond_to?(:state_event) end def test_should_define_an_event_attribute_writer assert @object.respond_to?(: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 @machine.action_hook? end end state_machines-0.6.0/test/unit/machine/machine_with_action_defined_in_included_module_test.rb000066400000000000000000000024301444665775700330360ustar00rootroot00000000000000require '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 @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.6.0/test/unit/machine/machine_with_action_defined_in_prepended_module_test.rb000066400000000000000000000024361444665775700332230ustar00rootroot00000000000000require '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 @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.6.0/test/unit/machine/machine_with_action_defined_in_superclass_test.rb000066400000000000000000000021341444665775700320670ustar00rootroot00000000000000require '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 @object.respond_to?(:state_event) end def test_should_define_an_event_attribute_writer assert @object.respond_to?(: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 @machine.action_hook? end end state_machines-0.6.0/test/unit/machine/machine_with_action_undefined_test.rb000066400000000000000000000015301444665775700274770ustar00rootroot00000000000000require '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 @object.respond_to?(:state_event) end def test_should_define_an_event_attribute_writer assert @object.respond_to?(: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 @object.respond_to?(:save) end def test_should_not_mark_action_hook_as_defined refute @machine.action_hook? end end state_machines-0.6.0/test/unit/machine/machine_with_cached_state_test.rb000066400000000000000000000007471444665775700266210ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_class_helpers_test.rb000066400000000000000000000114421444665775700270330ustar00rootroot00000000000000require '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_equal [], @klass.states end def test_should_not_redefine_existing_protected_methods class << @klass protected def states [] end end @machine.define_helper(:class, :states) {} assert_equal [], @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_equal [], @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_equal true, 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_equal [], @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_equal [], @klass.states end end state_machines-0.6.0/test/unit/machine/machine_with_conflicting_helpers_after_definition_test.rb000066400000000000000000000120601444665775700336130ustar00rootroot00000000000000require '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 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 @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_equal true, @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_equal [], @klass.with_state assert_equal [], @klass.with_states assert_equal [], @klass.without_state assert_equal [], @klass.without_states assert_equal 'parked', @klass.human_state_name(:parked) assert_equal 'ignite', @klass.human_state_event_name(:ignite) assert_equal nil, @object.state @object.state = 'idling' assert_equal 'idling', @object.state assert_equal nil, @object.status assert_equal false, @object.state?(:parked) assert_equal :idling, @object.state_name assert_equal 'idling', @object.human_state_name assert_equal [], @object.state_events assert_equal [], @object.state_transitions assert_equal [], @object.state_paths assert_equal false, @object.fire_state_event(:ignite) end def test_should_not_output_warning assert_equal '', $stderr.string end def teardown $stderr = @original_stderr StateMachines::Integrations.reset end end state_machines-0.6.0/test/unit/machine/machine_with_conflicting_helpers_before_definition_test.rb000066400000000000000000000101251444665775700337540ustar00rootroot00000000000000require 'test_helper' class MachineWithConflictingHelpersBeforeDefinitionTest < StateMachinesTest module Custom include StateMachines::Integrations::Base def create_with_scope(_name) lambda { |_klass, _values| [] } end def create_without_scope(_name) lambda { |_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 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 @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_equal true, @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 def teardown $stderr = @original_stderr end end state_machines-0.6.0/test/unit/machine/machine_with_custom_action_test.rb000066400000000000000000000003761444665775700270570ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_custom_attribute_test.rb000066400000000000000000000050061444665775700276000ustar00rootroot00000000000000require '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 test_should_define_a_reader_attribute_for_the_attribute assert @object.respond_to?(:state_id) end def test_should_define_a_writer_attribute_for_the_attribute assert @object.respond_to?(:state_id=) end def test_should_define_a_predicate_for_the_attribute assert @object.respond_to?(:state?) end def test_should_define_a_name_reader_for_the_attribute assert @object.respond_to?(:state_name) end def test_should_define_a_human_name_reader_for_the_attribute assert @object.respond_to?(:state_name) end def test_should_define_an_event_reader_for_the_attribute assert @object.respond_to?(:state_events) end def test_should_define_a_transition_reader_for_the_attribute assert @object.respond_to?(:state_transitions) end def test_should_define_a_path_reader_for_the_attribute assert @object.respond_to?(:state_paths) end def test_should_define_an_event_runner_for_the_attribute assert @object.respond_to?(:fire_state_event) end def test_should_define_a_human_attribute_name_reader assert @klass.respond_to?(:human_state_name) end def test_should_define_a_human_event_name_reader assert @klass.respond_to?(:human_state_event_name) end def test_should_define_singular_with_scope assert @klass.respond_to?(:with_state) end def test_should_define_singular_without_scope assert @klass.respond_to?(:without_state) end def test_should_define_plural_with_scope assert @klass.respond_to?(:with_states) end def test_should_define_plural_without_scope assert @klass.respond_to?(:without_states) end def test_should_define_state_machines_reader expected = { state: @machine } assert_equal expected, @klass.state_machines end def teardown StateMachines::Integrations.reset end end state_machines-0.6.0/test/unit/machine/machine_with_custom_initialize_test.rb000066400000000000000000000011321444665775700277320ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_custom_integration_test.rb000066400000000000000000000045431444665775700301250ustar00rootroot00000000000000require '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 test_should_be_extended_by_the_integration_if_explicit machine = StateMachines::Machine.new(@klass, integration: :custom) assert((class << machine; ancestors; end).include?(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) assert(!(class << machine; ancestors; end).include?(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) assert(!(class << machine; ancestors; end).include?(MachineWithCustomIntegrationTest::Custom)) end def test_should_be_extended_by_the_integration_if_implicit_and_available_and_matches machine = StateMachines::Machine.new(@klass) assert((class << machine; ancestors; end).include?(MachineWithCustomIntegrationTest::Custom)) end def test_should_not_be_extended_by_the_integration_if_nil machine = StateMachines::Machine.new(@klass, integration: nil) assert(!(class << machine; ancestors; end).include?(MachineWithCustomIntegrationTest::Custom)) end def test_should_not_be_extended_by_the_integration_if_false machine = StateMachines::Machine.new(@klass, integration: false) assert(!(class << machine; ancestors; end).include?(MachineWithCustomIntegrationTest::Custom)) 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 end state_machines-0.6.0/test/unit/machine/machine_with_custom_invalidation_test.rb000066400000000000000000000017631444665775700302640ustar00rootroot00000000000000require '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 test_generate_custom_message assert_equal 'cannot park', @machine.generate_message(:invalid_transition, [[:event, :park]]) end def test_use_custom_message @machine.invalidate(@object, :state, :invalid_transition, [[:event, 'park']]) assert_equal 'cannot park', @object.error end def teardown StateMachines::Integrations.reset end end state_machines-0.6.0/test/unit/machine/machine_with_custom_name_test.rb000066400000000000000000000030241444665775700265130ustar00rootroot00000000000000require '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 @object.respond_to?(:status) end def test_should_define_a_writer_attribute_for_the_attribute assert @object.respond_to?(:status=) end def test_should_define_a_predicate_for_the_attribute assert @object.respond_to?(:status?) end def test_should_define_a_name_reader_for_the_attribute assert @object.respond_to?(:status_name) end def test_should_define_an_event_reader_for_the_attribute assert @object.respond_to?(:status_events) end def test_should_define_a_transition_reader_for_the_attribute assert @object.respond_to?(:status_transitions) end def test_should_define_an_event_runner_for_the_attribute assert @object.respond_to?(:fire_status_event) end def test_should_define_a_human_attribute_name_reader_for_the_attribute assert @klass.respond_to?(:human_status_name) end def test_should_define_a_human_event_name_reader_for_the_attribute assert @klass.respond_to?(:human_status_event_name) end end state_machines-0.6.0/test/unit/machine/machine_with_custom_plural_test.rb000066400000000000000000000033011444665775700270700ustar00rootroot00000000000000require '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 lambda {} end def create_without_scope(name) MachineWithCustomPluralTest::Custom.without_scopes << name lambda {} end end MachineWithCustomPluralTest.const_set('Custom', @integration) StateMachines::Integrations.register(MachineWithCustomPluralTest::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 def teardown StateMachines::Integrations.reset MachineWithCustomPluralTest.send(:remove_const, 'Custom') end end state_machines-0.6.0/test/unit/machine/machine_with_dynamic_initial_state_test.rb000066400000000000000000000034371444665775700305460ustar00rootroot00000000000000require 'test_helper' class MachineWithDynamicInitialStateTest < StateMachinesTest def setup @klass = Class.new do attr_accessor :initial_state end @machine = StateMachines::Machine.new(@klass, initial: lambda { |object| object.initial_state || :default }) @machine.state :parked, :idling, :default @object = @klass.new end def test_should_have_dynamic_initial_state assert @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: lambda { |_object| :parked }) machine.state :parked assert_nil klass.new.state_on_init end def test_should_not_be_included_in_known_states assert_equal [:parked, :idling, :default], @machine.states.map { |state| state.name } end end state_machines-0.6.0/test/unit/machine/machine_with_event_matchers_test.rb000066400000000000000000000025171444665775700272160ustar00rootroot00000000000000require '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_equal [], @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 exception = assert_raises(ArgumentError) { @machine.event(StateMachines::BlacklistMatcher.new([:park]), human_name: 'Park') } assert_equal 'Cannot configure events when using matchers (using {:human_name=>"Park"})', 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_equal [], contexts_run @machine.event :ignite assert_equal [:ignite], contexts_run @machine.event :shift_up, :shift_down assert_equal [:ignite, :shift_up, :shift_down], contexts_run end end state_machines-0.6.0/test/unit/machine/machine_with_events_test.rb000066400000000000000000000027601444665775700255130ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_events_with_custom_human_names_test.rb000066400000000000000000000007101444665775700325040ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_events_with_transitions_test.rb000066400000000000000000000020131444665775700311720ustar00rootroot00000000000000require '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 [: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 [: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 [:parked, :idling, :stalled, :first_gear], @machine.states.map { |state| state.name } end end state_machines-0.6.0/test/unit/machine/machine_with_existing_event_test.rb000066400000000000000000000006501444665775700272360ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_existing_machines_on_owner_class_test.rb000066400000000000000000000011511444665775700327740ustar00rootroot00000000000000require '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.rb000066400000000000000000000036671444665775700372410ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machinerequire '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.rb000066400000000000000000000022531444665775700377410ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machinerequire '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.6.0/test/unit/machine/machine_with_existing_state_test.rb000066400000000000000000000012271444665775700272360ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_failure_callbacks_test.rb000066400000000000000000000033701444665775700276330ustar00rootroot00000000000000require '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: lambda {} } 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 lambda { |object| object.callbacks << 'failure' } @event.fire(@object) assert_equal %w(failure), @object.callbacks end def test_should_allow_multiple_callbacks @machine.after_failure lambda { |object| object.callbacks << 'failure1' }, lambda { |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 lambda { |object| object.callbacks << 'failure_ignite1' }, lambda { |object| object.callbacks << 'failure_ignite2' }, on: :ignite @machine.after_failure lambda { |object| object.callbacks << 'failure_park1' }, lambda { |object| object.callbacks << 'failure_park2' }, on: :park @event.fire(@object) assert_equal %w(failure_ignite1 failure_ignite2), @object.callbacks end end state_machines-0.6.0/test/unit/machine/machine_with_helpers_test.rb000066400000000000000000000005101444665775700256400ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_initial_state_with_value_and_owner_default.rb000066400000000000000000000011211444665775700337560ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_initialize_and_super_test.rb000066400000000000000000000005521444665775700304050ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_initialize_arguments_and_block_test.rb000066400000000000000000000012551444665775700324270ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_initialize_without_super_test.rb000066400000000000000000000005271444665775700313500ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_instance_helpers_test.rb000066400000000000000000000115361444665775700275360ustar00rootroot00000000000000require '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_equal true, @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_equal true, @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_equal true, @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_equal true, 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 } assert_equal false, @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 assert_equal false, @object.park end end state_machines-0.6.0/test/unit/machine/machine_with_integration_test.rb000066400000000000000000000034151444665775700265300ustar00rootroot00000000000000require '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 lambda {} end def create_without_scope(name) (@without_scopes ||= []) << name lambda {} 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 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 assert_equal false, @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_equal true, 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 def teardown StateMachines::Integrations.reset end end state_machines-0.6.0/test/unit/machine/machine_with_multiple_events_test.rb000066400000000000000000000014721444665775700274250ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_namespace_test.rb000066400000000000000000000021731444665775700261410ustar00rootroot00000000000000require '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 [:alarm_active?, :alarm_off?].each do |name| assert @object.respond_to?(name) end end def test_should_namespace_event_checks [:can_enable_alarm?, :can_disable_alarm?].each do |name| assert @object.respond_to?(name) end end def test_should_namespace_event_transition_readers [:enable_alarm_transition, :disable_alarm_transition].each do |name| assert @object.respond_to?(name) end end def test_should_namespace_events [:enable_alarm, :disable_alarm].each do |name| assert @object.respond_to?(name) end end def test_should_namespace_bang_events [:enable_alarm!, :disable_alarm!].each do |name| assert @object.respond_to?(name) end end end state_machines-0.6.0/test/unit/machine/machine_with_nil_action_test.rb000066400000000000000000000012141444665775700263170ustar00rootroot00000000000000require 'test_helper' class MachineWithNilActionTest < StateMachinesTest module Custom include StateMachines::Integrations::Base @defaults = {action: :save} end def setup StateMachines::Integrations.register(MachineWithNilActionTest::Custom) 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 def teardown StateMachines::Integrations.reset end end state_machines-0.6.0/test/unit/machine/machine_with_other_states.rb000066400000000000000000000010201444665775700256400ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_owner_subclass_test.rb000066400000000000000000000007431444665775700272370ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_paths_test.rb000066400000000000000000000013541444665775700253240ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_private_action_test.rb000066400000000000000000000021471444665775700272150ustar00rootroot00000000000000require '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 @object.respond_to?(:state_event) end def test_should_define_an_event_attribute_writer assert @object.respond_to?(: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 @machine.action_hook? end end state_machines-0.6.0/test/unit/machine/machine_with_state_matchers_test.rb000066400000000000000000000025521444665775700272140ustar00rootroot00000000000000require '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_equal [], @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 exception = assert_raises(ArgumentError) { @machine.state(StateMachines::BlacklistMatcher.new([:parked]), human_name: 'Parked') } assert_equal 'Cannot configure states when using matchers (using {:human_name=>"Parked"})', 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_equal [], contexts_run @machine.event :idling assert_equal [:idling], contexts_run @machine.event :first_gear, :second_gear assert_equal [:idling, :first_gear, :second_gear], contexts_run end end state_machines-0.6.0/test/unit/machine/machine_with_state_with_matchers_test.rb000066400000000000000000000006541444665775700302500ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_states_test.rb000066400000000000000000000033371444665775700255130ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_states_with_behaviors_test.rb000066400000000000000000000011061444665775700306000ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_states_with_custom_human_names_test.rb000066400000000000000000000007071444665775700325110ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_states_with_custom_values_test.rb000066400000000000000000000007121444665775700315110ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_with_states_with_runtime_dependencies_test.rb000066400000000000000000000007351444665775700330160ustar00rootroot00000000000000require '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: -> { fail ArgumentError } end def test_should_not_evaluate_if_not_initial_state @machine.state :parked, value: -> { fail ArgumentError } @klass.new end end state_machines-0.6.0/test/unit/machine/machine_with_static_initial_state_test.rb000066400000000000000000000023211444665775700304000ustar00rootroot00000000000000require '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 @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_equal 'parked', object.state end def test_should_set_initial_on_state_object assert @machine.state(:parked).initial end def test_should_set_initial_state_on_created_object assert_equal 'parked', @klass.new.state 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_equal [:parked], @machine.states.keys end end machine_with_superclass_conflicting_helpers_after_definition_test.rb000066400000000000000000000013541444665775700360040ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machinerequire '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 test_should_call_superclass_attribute_predicate_without_arguments assert @object.state? end def test_should_define_attribute_predicate_with_arguments refute @object.state?(:parked) end def teardown $stderr = @original_stderr end end state_machines-0.6.0/test/unit/machine/machine_with_transition_callbacks_test.rb000066400000000000000000000153401444665775700303760ustar00rootroot00000000000000require '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 lambda { |object| object.callbacks << 'before' } @machine.after_transition lambda { |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 lambda { |object| object.callbacks << 'before1' }, lambda { |object| object.callbacks << 'before2' } @machine.after_transition lambda { |object| object.callbacks << 'after1' }, lambda { |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 lambda { |object| object.callbacks << 'before_parked1' }, lambda { |object| object.callbacks << 'before_parked2' }, from: :parked @machine.before_transition lambda { |object| object.callbacks << 'before_idling1' }, lambda { |object| object.callbacks << 'before_idling2' }, from: :idling @machine.after_transition lambda { |object| object.callbacks << 'after_parked1' }, lambda { |object| object.callbacks << 'after_parked2' }, from: :parked @machine.after_transition lambda { |object| object.callbacks << 'after_idling1' }, lambda { |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: lambda { |object| object.callbacks << :parked } @machine.before_transition from: :idling, do: lambda { |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: lambda { |object| object.callbacks << :parked } @machine.before_transition except_from: :idling, do: lambda { |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: lambda { |object| object.callbacks << :parked } @machine.before_transition to: :idling, do: lambda { |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: lambda { |object| object.callbacks << :parked } @machine.before_transition except_to: :idling, do: lambda { |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: lambda { |object| object.callbacks << :park } @machine.before_transition on: :ignite, do: lambda { |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: lambda { |object| object.callbacks << :park } @machine.before_transition except_on: :ignite, do: lambda { |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: lambda { |object| object.callbacks << :parked } @machine.before_transition idling: :parked, do: lambda { |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: lambda {} @machine.after_transition first_gear: :second_gear, do: lambda {} @machine.around_transition third_gear: :fourth_gear, do: lambda {} assert_equal [: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: lambda {} @machine.after_transition first_gear: :second_gear, do: lambda {} @machine.after_transition parked: :idling, do: lambda {} @machine.around_transition parked: :idling, do: lambda {} assert_equal [:parked, :idling, :first_gear, :second_gear], @machine.states.map { |state| state.name } end def test_should_define_predicates_for_each_state [:parked?, :idling?].each { |predicate| assert @object.respond_to?(predicate) } end end state_machines-0.6.0/test/unit/machine/machine_with_transitions_test.rb000066400000000000000000000060201444665775700265550ustar00rootroot00000000000000require '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', 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 [: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: [:ignite, :shift_up]) assert_equal 2, branches.length assert_equal [: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 options, parked: :idling, on: :ignite end end state_machines-0.6.0/test/unit/machine/machine_without_initialization_test.rb000066400000000000000000000014771444665775700277720ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_without_initialize_test.rb000066400000000000000000000004361444665775700270760ustar00rootroot00000000000000require '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.6.0/test/unit/machine/machine_without_integration_test.rb000066400000000000000000000012451444665775700272570ustar00rootroot00000000000000require '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.6.0/test/unit/machine_collection/000077500000000000000000000000001444665775700223125ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machine_collection/machine_collection_by_default_test.rb000066400000000000000000000003461444665775700317160ustar00rootroot00000000000000require 'test_helper' class MachineCollectionByDefaultTest < StateMachinesTest def setup @machines = StateMachines::MachineCollection.new end def test_should_not_have_any_machines assert @machines.empty? end end state_machines-0.6.0/test/unit/machine_collection/machine_collection_fire_test.rb000066400000000000000000000047621444665775700305330ustar00rootroot00000000000000require '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 state_machines-0.6.0/test/unit/machine_collection/machine_collection_fire_with_transactions_test.rb000066400000000000000000000024071444665775700343500ustar00rootroot00000000000000require '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 @object.errors.empty? end def test_should_invalidate_if_no_transition_exists @object.state = 'idling' @object.state_event = 'ignite' @machines.transitions(@object, :save) refute @object.errors.empty? end def test_should_not_invalidate_if_transition_exists @object.state_event = 'ignite' @machines.transitions(@object, :save) assert @object.errors.empty? end end state_machines-0.6.0/test/unit/machine_collection/machine_collection_fire_with_validations_test.rb000066400000000000000000000042441444665775700341560ustar00rootroot00000000000000require '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 test_should_not_invalidate_if_transitions_exist assert @machines.fire_events(@object, :ignite, :disable_alarm) assert_equal [], @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 def teardown StateMachines::Integrations.reset end end state_machines-0.6.0/test/unit/machine_collection/machine_collection_state_initialization_test.rb000066400000000000000000000065611444665775700340340ustar00rootroot00000000000000require '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.rb000066400000000000000000000011701444665775700356600ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machine_collectionrequire '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 @transitions.empty? end def test_should_perform assert_equal true, @transitions.perform end end machine_collection_transitions_with_custom_options_test.rb000066400000000000000000000010541444665775700362730ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machine_collectionrequire '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.rb000066400000000000000000000014751444665775700367030ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machine_collectionrequire '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.rb000066400000000000000000000013511444665775700376460ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machine_collectionrequire '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_equal true, @transitions.perform end end machine_collection_transitions_with_invalid_events_test.rb000066400000000000000000000012061444665775700362170ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machine_collectionrequire '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 @transitions.empty? end def test_should_not_perform assert_equal false, @transitions.perform end end machine_collection_transitions_with_same_actions_test.rb000066400000000000000000000015711444665775700356570ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machine_collectionrequire '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_equal true, @transitions.perform end end machine_collection_transitions_with_transition_test.rb000066400000000000000000000012131444665775700353750ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machine_collectionrequire '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_equal true, @transitions.perform end end machine_collection_transitions_without_events_test.rb000066400000000000000000000011671444665775700352470ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machine_collectionrequire '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 @transitions.empty? end def test_should_perform assert_equal true, @transitions.perform end end machine_collection_transitions_without_transition_test.rb000066400000000000000000000012431444665775700361300ustar00rootroot00000000000000state_machines-0.6.0/test/unit/machine_collectionrequire '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 @transitions.empty? end def test_should_not_perform assert_equal false, @transitions.perform end end state_machines-0.6.0/test/unit/matcher/000077500000000000000000000000001444665775700201165ustar00rootroot00000000000000state_machines-0.6.0/test/unit/matcher/all_matcher_test.rb000066400000000000000000000013771444665775700237650ustar00rootroot00000000000000require 'test_helper' class AllMatcherTest < StateMachinesTest def setup @matcher = StateMachines::AllMatcher.instance end def test_should_have_no_values assert_equal [], @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 [:parked, :idling], @matcher.filter([:parked, :idling]) end def test_should_generate_blacklist_matcher_after_subtraction matcher = @matcher - [:parked, :idling] assert_instance_of StateMachines::BlacklistMatcher, matcher assert_equal [:parked, :idling], matcher.values end def test_should_have_a_description assert_equal 'all', @matcher.description end end state_machines-0.6.0/test/unit/matcher/blacklist_matcher_test.rb000066400000000000000000000014131444665775700251540ustar00rootroot00000000000000require 'test_helper' class BlacklistMatcherTest < StateMachinesTest def setup @matcher = StateMachines::BlacklistMatcher.new([:parked, :idling]) end def test_should_have_values assert_equal [:parked, :idling], @matcher.values end def test_should_filter_known_values assert_equal [:first_gear], @matcher.filter([: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.6.0/test/unit/matcher/loopback_matcher_test.rb000066400000000000000000000011761444665775700250040ustar00rootroot00000000000000require 'test_helper' class LoopbackMatcherTest < StateMachinesTest def setup @matcher = StateMachines::LoopbackMatcher.instance end def test_should_have_no_values assert_equal [], @matcher.values end def test_should_filter_all_values assert_equal [], @matcher.filter([: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.6.0/test/unit/matcher/matcher_by_default_test.rb000066400000000000000000000004671444665775700253320ustar00rootroot00000000000000require 'test_helper' class MatcherByDefaultTest < StateMachinesTest def setup @matcher = StateMachines::Matcher.new end def test_should_have_no_values assert_equal [], @matcher.values end def test_should_filter_all_values assert_equal [], @matcher.filter([:parked, :idling]) end end state_machines-0.6.0/test/unit/matcher/matcher_with_multiple_values_test.rb000066400000000000000000000005601444665775700274530ustar00rootroot00000000000000require 'test_helper' class MatcherWithMultipleValuesTest < StateMachinesTest def setup @matcher = StateMachines::Matcher.new([:parked, :idling]) end def test_should_have_values assert_equal [:parked, :idling], @matcher.values end def test_should_filter_unknown_values assert_equal [:parked], @matcher.filter([:parked, :first_gear]) end end state_machines-0.6.0/test/unit/matcher/matcher_with_value_test.rb000066400000000000000000000004771444665775700253640ustar00rootroot00000000000000require '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.6.0/test/unit/matcher/whitelist_matcher_test.rb000066400000000000000000000014061444665775700252220ustar00rootroot00000000000000require 'test_helper' class WhitelistMatcherTest < StateMachinesTest def setup @matcher = StateMachines::WhitelistMatcher.new([:parked, :idling]) end def test_should_have_values assert_equal [:parked, :idling], @matcher.values end def test_should_filter_unknown_values assert_equal [:parked, :idling], @matcher.filter([: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.6.0/test/unit/matcher_helpers/000077500000000000000000000000001444665775700216405ustar00rootroot00000000000000state_machines-0.6.0/test/unit/matcher_helpers/matcher_helpers_all_test.rb000066400000000000000000000004061444665775700272210ustar00rootroot00000000000000require '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.6.0/test/unit/matcher_helpers/matcher_helpers_any_test.rb000066400000000000000000000004061444665775700272400ustar00rootroot00000000000000require '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.6.0/test/unit/matcher_helpers/matcher_helpers_same_test.rb000066400000000000000000000004201444665775700273720ustar00rootroot00000000000000require '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.6.0/test/unit/node_collection/000077500000000000000000000000001444665775700216335ustar00rootroot00000000000000state_machines-0.6.0/test/unit/node_collection/node_collection_after_being_copied_test.rb000066400000000000000000000023661444665775700322360ustar00rootroot00000000000000require '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_equal [], @contexts_run end def test_should_not_modify_contexts @collection << Node.new(:first_gear) assert_equal [], @contexts_run end def test_should_copy_contexts @copied_collection << Node.new(:parked) refute @contexts_run.empty? end end state_machines-0.6.0/test/unit/node_collection/node_collection_after_update_test.rb000066400000000000000000000016141444665775700311040ustar00rootroot00000000000000require 'test_helper' require 'files/node' class NodeCollectionAfterUpdateTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine, index: [: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.6.0/test/unit/node_collection/node_collection_by_default_test.rb000066400000000000000000000010301444665775700305470ustar00rootroot00000000000000require '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.6.0/test/unit/node_collection/node_collection_test.rb000066400000000000000000000016251444665775700263630ustar00rootroot00000000000000require '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.6.0/test/unit/node_collection/node_collection_with_indices_test.rb000066400000000000000000000024751444665775700311200ustar00rootroot00000000000000require 'test_helper' require 'files/node' class NodeCollectionWithIndicesTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine, index: [: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.6.0/test/unit/node_collection/node_collection_with_matcher_contexts_test.rb000066400000000000000000000014311444665775700330430ustar00rootroot00000000000000require '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_equal [], contexts_run @collection << Node.new(:idling) assert_equal [:blacklist], contexts_run end end state_machines-0.6.0/test/unit/node_collection/node_collection_with_nodes_test.rb000066400000000000000000000024631444665775700306070ustar00rootroot00000000000000require '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.6.0/test/unit/node_collection/node_collection_with_numeric_index_test.rb000066400000000000000000000011101444665775700323140ustar00rootroot00000000000000require 'test_helper' require 'files/node' class NodeCollectionWithNumericIndexTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine, index: [: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.6.0/test/unit/node_collection/node_collection_with_postdefined_contexts_test.rb000066400000000000000000000011721444665775700337260ustar00rootroot00000000000000require '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_equal [], contexts_run end end state_machines-0.6.0/test/unit/node_collection/node_collection_with_predefined_contexts_test.rb000066400000000000000000000012751444665775700335330ustar00rootroot00000000000000require '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 [:parked, :second_parked], @contexts_run end def test_should_not_run_contexts_if_not_matched @collection << Node.new(:idling) assert_equal [], @contexts_run end end state_machines-0.6.0/test/unit/node_collection/node_collection_with_string_index_test.rb000066400000000000000000000007721444665775700321750ustar00rootroot00000000000000require 'test_helper' require 'files/node' class NodeCollectionWithStringIndexTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine, index: [: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.6.0/test/unit/node_collection/node_collection_with_symbol_index_test.rb000066400000000000000000000007731444665775700321750ustar00rootroot00000000000000require 'test_helper' require 'files/node' class NodeCollectionWithSymbolIndexTest < StateMachinesTest def setup machine = StateMachines::Machine.new(Class.new) @collection = StateMachines::NodeCollection.new(machine, index: [: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.6.0/test/unit/node_collection/node_collection_without_indices_test.rb000066400000000000000000000016241444665775700316430ustar00rootroot00000000000000require '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.6.0/test/unit/path/000077500000000000000000000000001444665775700174275ustar00rootroot00000000000000state_machines-0.6.0/test/unit/path/path_by_default_test.rb000066400000000000000000000020771444665775700241530ustar00rootroot00000000000000require '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_equal [], @path end def test_should_not_have_a_from_name assert_nil @path.from_name end def test_should_have_no_from_states assert_equal [], @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_equal [], @path.to_states end def test_should_have_no_events assert_equal [], @path.events end def test_should_not_be_able_to_walk_anywhere walked = false @path.walk { walked = true } assert_equal false, walked end def test_should_not_be_complete assert_equal false, @path.complete? end end state_machines-0.6.0/test/unit/path/path_test.rb000066400000000000000000000006761444665775700217600ustar00rootroot00000000000000require '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 endstate_machines-0.6.0/test/unit/path/path_with_available_transitions_after_reaching_target_test.rb000066400000000000000000000023141444665775700340260ustar00rootroot00000000000000require '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 [:idling, :first_gear] => :parked end @object = @klass.new @object.state = 'parked' @path = StateMachines::Path.new(@object, @machine, target: :parked) @path.concat([ @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_equal true, @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.6.0/test/unit/path/path_with_available_transitions_test.rb000066400000000000000000000027601444665775700274440ustar00rootroot00000000000000require '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.concat([ @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) end def test_should_not_be_complete refute @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.6.0/test/unit/path/path_with_deep_target_reached_test.rb000066400000000000000000000027601444665775700270250ustar00rootroot00000000000000require '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 [:idling, :first_gear] => :parked end @object = @klass.new @object.state = 'parked' @path = StateMachines::Path.new(@object, @machine, target: :parked) @path.concat([ @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_equal true, @path.complete? end def test_should_not_be_able_to_walk walked = false @path.walk { walked = true } assert_equal false, 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 } assert_equal false, walked end end state_machines-0.6.0/test/unit/path/path_with_deep_target_test.rb000066400000000000000000000025041444665775700253460ustar00rootroot00000000000000require '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 [:idling, :first_gear] => :parked end @object = @klass.new @object.state = 'parked' @path = StateMachines::Path.new(@object, @machine, target: :parked) @path.concat([ @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 assert_equal false, @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.6.0/test/unit/path/path_with_duplicates_test.rb000066400000000000000000000020731444665775700252210ustar00rootroot00000000000000require '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.concat([ @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 [:parked, :idling], @path.from_states end def test_should_not_include_duplicates_in_to_states assert_equal [:idling, :parked], @path.to_states end def test_should_not_include_duplicates_in_events assert_equal [:ignite, :park], @path.events end end state_machines-0.6.0/test/unit/path/path_with_encountered_transitions_test.rb000066400000000000000000000016731444665775700300410ustar00rootroot00000000000000require '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.concat([ @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_equal true, @path.complete? end def test_should_not_be_able_to_walk walked = false @path.walk { walked = true } assert_equal false, walked end end state_machines-0.6.0/test/unit/path/path_with_guarded_transitions_test.rb000066400000000000000000000023471444665775700271400ustar00rootroot00000000000000require '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: lambda { 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.concat([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) paths = [] path.walk { |next_path| paths << next_path } assert_equal [], paths end def test_should_not_walk_transitions_if_guard_disabled path = StateMachines::Path.new(@object, @machine, guard: false) path.concat([ 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.6.0/test/unit/path/path_with_reached_target_test.rb000066400000000000000000000016671444665775700260350ustar00rootroot00000000000000require '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.concat([ @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_equal true, @path.complete? end def test_should_not_be_able_to_walk walked = false @path.walk { walked = true } assert_equal false, walked end end state_machines-0.6.0/test/unit/path/path_with_transitions_test.rb000066400000000000000000000026251444665775700254440ustar00rootroot00000000000000require '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.concat([ @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 [: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 [:idling, :first_gear], @path.to_states end def test_should_have_events assert_equal [:ignite, :shift_up], @path.events end def test_should_not_be_able_to_walk_anywhere walked = false @path.walk { walked = true } assert_equal false, walked end def test_should_be_complete assert_equal true, @path.complete? end end state_machines-0.6.0/test/unit/path/path_with_unreached_target_test.rb000066400000000000000000000014061444665775700263670ustar00rootroot00000000000000require '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.concat([ @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) end def test_should_not_be_complete assert_equal false, @path.complete? end def test_should_not_be_able_to_walk walked = false @path.walk { walked = true } assert_equal false, walked end end state_machines-0.6.0/test/unit/path/path_without_transitions_test.rb000066400000000000000000000011431444665775700261660ustar00rootroot00000000000000require '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.concat([ @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 } assert_equal false, walked end end state_machines-0.6.0/test/unit/path_collection/000077500000000000000000000000001444665775700216425ustar00rootroot00000000000000state_machines-0.6.0/test/unit/path_collection/path_collection_by_default_test.rb000066400000000000000000000016631444665775700306010ustar00rootroot00000000000000require '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_equal [], @paths.from_states end def test_should_have_no_to_states assert_equal [], @paths.to_states end def test_should_have_no_events assert_equal [], @paths.events end def test_should_have_no_paths assert @paths.empty? end end state_machines-0.6.0/test/unit/path_collection/path_collection_test.rb000066400000000000000000000017251444665775700264020ustar00rootroot00000000000000require '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.6.0/test/unit/path_collection/path_collection_with_deep_paths_test.rb000066400000000000000000000027161444665775700316320ustar00rootroot00000000000000require '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.6.0/test/unit/path_collection/path_collection_with_duplicate_nodes_test.rb000066400000000000000000000015411444665775700326530ustar00rootroot00000000000000require '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 [:parked, :idling, :first_gear], @paths.from_states end def test_should_not_include_duplicates_in_to_states assert_equal [:idling, :first_gear], @paths.to_states end def test_should_not_include_duplicates_in_events assert_equal [:shift_up, :park], @paths.events end end state_machines-0.6.0/test/unit/path_collection/path_collection_with_from_state_test.rb000066400000000000000000000012661444665775700316600ustar00rootroot00000000000000require '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.6.0/test/unit/path_collection/path_collection_with_paths_test.rb000066400000000000000000000022421444665775700306270ustar00rootroot00000000000000require '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 [:parked, :idling], @paths.from_states end def test_should_have_to_states assert_equal [:idling, :first_gear], @paths.to_states end def test_should_have_no_events assert_equal [:ignite, :shift_up], @paths.events end end state_machines-0.6.0/test/unit/path_collection/path_collection_with_to_state_test.rb000066400000000000000000000015261444665775700313360ustar00rootroot00000000000000require '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.6.0/test/unit/path_collection/path_with_guarded_paths_test.rb000066400000000000000000000013631444665775700301120ustar00rootroot00000000000000require '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: lambda { false } end @object = @klass.new @object.state = 'parked' end def test_should_not_enumerate_paths_if_guard_enabled assert_equal [], 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.6.0/test/unit/state/000077500000000000000000000000001444665775700176135ustar00rootroot00000000000000state_machines-0.6.0/test/unit/state/state_after_being_copied_test.rb000066400000000000000000000010021444665775700261600ustar00rootroot00000000000000require '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.6.0/test/unit/state/state_by_default_test.rb000066400000000000000000000016221444665775700245160ustar00rootroot00000000000000require '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.6.0/test/unit/state/state_final_test.rb000066400000000000000000000011221444665775700234640ustar00rootroot00000000000000require '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 @state.final? end def test_should_be_final_with_input_transitions @machine.event :park do transition idling: :parked end assert @state.final? end def test_should_be_final_with_loopback @machine.event :ignite do transition parked: same end assert @state.final? end end state_machines-0.6.0/test/unit/state/state_initial_test.rb000066400000000000000000000005011444665775700240240ustar00rootroot00000000000000require '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 @state.initial? end end state_machines-0.6.0/test/unit/state/state_not_final_test.rb000066400000000000000000000013411444665775700243470ustar00rootroot00000000000000require '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 @state.final? end def test_should_not_be_final_with_outgoing_all_transitions @machine.event :ignite do transition all => :idling end refute @state.final? end def test_should_not_be_final_with_outgoing_blacklist_transitions @machine.event :ignite do transition all - :first_gear => :idling end refute @state.final? end end state_machines-0.6.0/test/unit/state/state_not_initial_test.rb000066400000000000000000000005111444665775700247050ustar00rootroot00000000000000require '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 @state.initial? end end state_machines-0.6.0/test/unit/state/state_test.rb000066400000000000000000000024331444665775700223210ustar00rootroot00000000000000require '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 = lambda {} @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.6.0/test/unit/state/state_with_cached_lambda_value_test.rb000066400000000000000000000014001444665775700273300ustar00rootroot00000000000000require '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.6.0/test/unit/state/state_with_conflicting_helpers_after_definition_test.rb000066400000000000000000000013421444665775700330440ustar00rootroot00000000000000require 'test_helper' class StateWithConflictingHelpersAfterDefinitionTest < StateMachinesTest def setup @original_stderr, $stderr = $stderr, StringIO.new @klass = Class.new do def parked? 0 end end @machine = StateMachines::Machine.new(@klass) @machine.state :parked @object = @klass.new end def test_should_not_override_state_predicate assert_equal 0, @object.parked? end def test_should_still_allow_super_chaining @klass.class_eval do def parked? super end end assert_equal false, @object.parked? end def test_should_not_output_warning assert_equal '', $stderr.string end def teardown $stderr = @original_stderr end end state_machines-0.6.0/test/unit/state/state_with_conflicting_helpers_before_definition_test.rb000066400000000000000000000013741444665775700332120ustar00rootroot00000000000000require '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 test_should_not_override_state_predicate assert_equal 0, @object.parked? end def test_should_output_warning assert_equal "Instance method \"parked?\" is already defined in #{@superclass}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string end def teardown $stderr = @original_stderr end end state_machines-0.6.0/test/unit/state/state_with_conflicting_machine_name_test.rb000066400000000000000000000012021444665775700304100ustar00rootroot00000000000000require '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 test_should_output_warning_if_name_conflicts StateMachines::State.new(@state_machine, :state) assert_equal "Instance method \"state?\" is already defined in #{@klass} :state instance helpers, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string end def teardown $stderr = @original_stderr end end state_machines-0.6.0/test/unit/state/state_with_conflicting_machine_test.rb000066400000000000000000000024111444665775700274130ustar00rootroot00000000000000require '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 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 def teardown $stderr = @original_stderr end end state_machines-0.6.0/test/unit/state/state_with_context_test.rb000066400000000000000000000032231444665775700251160ustar00rootroot00000000000000require '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_equal true, @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.6.0/test/unit/state/state_with_dynamic_human_name_test.rb000066400000000000000000000013431444665775700272470ustar00rootroot00000000000000require '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: lambda { |_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.6.0/test/unit/state/state_with_existing_context_method_test.rb000066400000000000000000000010431444665775700303660ustar00rootroot00000000000000require '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.6.0/test/unit/state/state_with_human_name_test.rb000066400000000000000000000005471444665775700255500ustar00rootroot00000000000000require '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.6.0/test/unit/state/state_with_integer_value_test.rb000066400000000000000000000014441444665775700262660ustar00rootroot00000000000000require '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 object.respond_to?(:parked?) end end state_machines-0.6.0/test/unit/state/state_with_invalid_method_call_test.rb000066400000000000000000000007671444665775700274250ustar00rootroot00000000000000require '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.6.0/test/unit/state/state_with_lambda_value_test.rb000066400000000000000000000016241444665775700260510ustar00rootroot00000000000000require 'test_helper' class StateWithLambdaValueTest < StateMachinesTest def setup @klass = Class.new @args = nil @machine = StateMachines::Machine.new(@klass) @value = ->(*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_equal [], @args end def test_should_define_predicate object = @klass.new assert object.respond_to?(:parked?) end def test_should_match_evaluated_value assert @state.matches?(:parked) end end state_machines-0.6.0/test/unit/state/state_with_matcher_test.rb000066400000000000000000000007061444665775700250600ustar00rootroot00000000000000require '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: lambda { |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.6.0/test/unit/state/state_with_multiple_contexts_test.rb000066400000000000000000000031531444665775700272160ustar00rootroot00000000000000require '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.6.0/test/unit/state/state_with_name_test.rb000066400000000000000000000020431444665775700243510ustar00rootroot00000000000000require '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 @klass.new.respond_to?(:parked?) end end state_machines-0.6.0/test/unit/state/state_with_namespace_test.rb000066400000000000000000000010541444665775700253660ustar00rootroot00000000000000require '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 @object.respond_to?(:alarm_active?) end end state_machines-0.6.0/test/unit/state/state_with_nil_value_test.rb000066400000000000000000000015061444665775700254120ustar00rootroot00000000000000require '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 object.respond_to?(:parked?) end end state_machines-0.6.0/test/unit/state/state_with_redefined_context_method_test.rb000066400000000000000000000020731444665775700304650ustar00rootroot00000000000000require '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.6.0/test/unit/state/state_with_symbolic_value_test.rb000066400000000000000000000014731444665775700264540ustar00rootroot00000000000000require '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 object.respond_to?(:parked?) end end state_with_valid_inherited_method_call_for_current_state_test.rb000066400000000000000000000016271444665775700346560ustar00rootroot00000000000000state_machines-0.6.0/test/unit/staterequire '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: lambda { fail }) 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.6.0/test/unit/state/state_with_valid_method_call_for_current_state_test.rb000066400000000000000000000016221444665775700326750ustar00rootroot00000000000000require '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: lambda { fail }) end def test_should_pass_arguments_through assert_equal 1, @state.call(@object, :speed, 1, method_missing: lambda {}) 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: lambda {}) { 2 } end end state_machines-0.6.0/test/unit/state/state_with_valid_method_call_for_different_state_test.rb000066400000000000000000000025601444665775700331630ustar00rootroot00000000000000require '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: lambda { 1 }) end def test_should_raise_invalid_context_on_no_method_error exception = assert_raises(StateMachines::InvalidContext) do @state.call(@object, :speed, method_missing: lambda { fail 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: lambda { fail 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: lambda { fail NoMethodError.new('Invalid', :rpm, []) }) end end end state_machines-0.6.0/test/unit/state/state_without_cached_lambda_value_test.rb000066400000000000000000000012571444665775700300720ustar00rootroot00000000000000require 'test_helper' class StateWithoutCachedLambdaValueTest < 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) end def test_should_not_be_caching refute @state.cache end def test_should_evaluate_value_each_time value = @state.value refute_same value, @state.value 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.6.0/test/unit/state/state_without_name_test.rb000066400000000000000000000016021444665775700251010ustar00rootroot00000000000000require '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 object.nil? refute object.respond_to?('?') 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.6.0/test/unit/state_collection/000077500000000000000000000000001444665775700220265ustar00rootroot00000000000000state_machines-0.6.0/test/unit/state_collection/state_collection_by_default_test.rb000066400000000000000000000007161444665775700311470ustar00rootroot00000000000000require '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_equal [], @states.by_priority end end state_machines-0.6.0/test/unit/state_collection/state_collection_string_test.rb000066400000000000000000000016331444665775700303360ustar00rootroot00000000000000require '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.6.0/test/unit/state_collection/state_collection_test.rb000066400000000000000000000041441444665775700267500ustar00rootroot00000000000000require '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.6.0/test/unit/state_collection/state_collection_with_custom_state_values_test.rb000066400000000000000000000013461444665775700341550ustar00rootroot00000000000000require '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.6.0/test/unit/state_collection/state_collection_with_event_transitions_test.rb000066400000000000000000000021031444665775700336320ustar00rootroot00000000000000require '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: lambda {} assert_equal [@idling, @parked], @states.by_priority end end state_machines-0.6.0/test/unit/state_collection/state_collection_with_initial_state_test.rb000066400000000000000000000021021444665775700327040ustar00rootroot00000000000000require '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: lambda {} assert_equal [@parked, @idling], @states.by_priority end end state_machines-0.6.0/test/unit/state_collection/state_collection_with_namespace_test.rb000066400000000000000000000010701444665775700320120ustar00rootroot00000000000000require '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.6.0/test/unit/state_collection/state_collection_with_state_behaviors_test.rb000066400000000000000000000020771444665775700332500ustar00rootroot00000000000000require '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: lambda {} assert_equal [@idling, @parked], @states.by_priority end end state_machines-0.6.0/test/unit/state_collection/state_collection_with_state_matchers_test.rb000066400000000000000000000013761444665775700330750ustar00rootroot00000000000000require '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: lambda { |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.6.0/test/unit/state_collection/state_collection_with_transition_callbacks_test.rb000066400000000000000000000021071444665775700342510ustar00rootroot00000000000000require '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: lambda {} 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.6.0/test/unit/state_context/000077500000000000000000000000001444665775700213575ustar00rootroot00000000000000state_machines-0.6.0/test/unit/state_context/state_context_proxy_test.rb000066400000000000000000000012721444665775700270720ustar00rootroot00000000000000require '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 = lambda {} 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.rb000066400000000000000000000024031444665775700351650ustar00rootroot00000000000000state_machines-0.6.0/test/unit/state_contextrequire '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: lambda { @if_condition_result }, unless: lambda { @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.6.0/test/unit/state_context/state_context_proxy_with_if_condition_test.rb000066400000000000000000000030321444665775700326450ustar00rootroot00000000000000require '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: lambda { @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_machines-0.6.0/test/unit/state_context/state_context_proxy_with_multiple_if_conditions_test.rb000066400000000000000000000017311444665775700347470ustar00rootroot00000000000000require '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: [lambda { @first_condition_result }, lambda { @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.rb000066400000000000000000000017371444665775700356110ustar00rootroot00000000000000state_machines-0.6.0/test/unit/state_contextrequire '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 }, lambda { @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.6.0/test/unit/state_context/state_context_proxy_with_unless_condition_test.rb000066400000000000000000000030521444665775700335620ustar00rootroot00000000000000require '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: lambda { @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.6.0/test/unit/state_context/state_context_proxy_without_conditions_test.rb000066400000000000000000000013501444665775700331030ustar00rootroot00000000000000require '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.6.0/test/unit/state_context/state_context_test.rb000066400000000000000000000011261444665775700256270ustar00rootroot00000000000000require '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.6.0/test/unit/state_context/state_context_transition_test.rb000066400000000000000000000075641444665775700301150ustar00rootroot00000000000000require '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 [: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: [:ignite, :shift_up]) assert_equal [:ignite, :shift_up], @machine.events.map { |event| event.name } end end state_machines-0.6.0/test/unit/state_context/state_context_with_matching_transition_test.rb000066400000000000000000000013441444665775700330100ustar00rootroot00000000000000require '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.6.0/test/unit/state_machine/000077500000000000000000000000001444665775700212775ustar00rootroot00000000000000state_machines-0.6.0/test/unit/state_machine/state_machine_by_default_test.rb000066400000000000000000000003651444665775700276710ustar00rootroot00000000000000require '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.6.0/test/unit/state_machine/state_machine_test.rb000066400000000000000000000006251444665775700254720ustar00rootroot00000000000000require 'test_helper' class StateMachineTest < StateMachinesTest def setup @klass = Class.new end def test_should_allow_state_machines_on_any_class assert @klass.respond_to?(: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.6.0/test/unit/transition/000077500000000000000000000000001444665775700206655ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition/transition_after_being_performed_test.rb000066400000000000000000000020531444665775700310330ustar00rootroot00000000000000require '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_equal [], @transition.args end def test_should_have_a_result assert_equal 1, @transition.result end def test_should_be_successful assert_equal true, @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.6.0/test/unit/transition/transition_after_being_persisted_test.rb000066400000000000000000000021731444665775700310550ustar00rootroot00000000000000require '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.6.0/test/unit/transition/transition_after_being_rolled_back_test.rb000066400000000000000000000015261444665775700313150ustar00rootroot00000000000000require '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.6.0/test/unit/transition/transition_equality_test.rb000066400000000000000000000034071444665775700263640ustar00rootroot00000000000000require '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.6.0/test/unit/transition/transition_loopback_test.rb000066400000000000000000000006641444665775700263230ustar00rootroot00000000000000require '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 @transition.loopback? end end state_machines-0.6.0/test/unit/transition/transition_test.rb000066400000000000000000000045401444665775700244460ustar00rootroot00000000000000require '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 assert_equal false, @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_equal [], @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.6.0/test/unit/transition/transition_transient_test.rb000066400000000000000000000007461444665775700265410ustar00rootroot00000000000000require '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 @transition.transient? end end state_machines-0.6.0/test/unit/transition/transition_with_action_test.rb000066400000000000000000000011221444665775700270270ustar00rootroot00000000000000require '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.6.0/test/unit/transition/transition_with_after_callbacks_skipped_test.rb000066400000000000000000000110441444665775700323750ustar00rootroot00000000000000require '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_equal true, @transition.run_callbacks(after: false) assert @run end def test_should_not_run_after_callbacks @run = false @machine.after_transition { @run = true } assert_equal true, @transition.run_callbacks(after: false) refute @run end if StateMachines::Transition.pause_supported? def test_should_run_around_callbacks_before_yield @machine.around_transition { |block| @run = true; block.call } assert_equal true, @transition.run_callbacks(after: false) assert @run end def test_should_not_run_around_callbacks_after_yield @run = false @machine.around_transition { |block| block.call; @run = true } assert_equal true, @transition.run_callbacks(after: false) refute @run end def test_should_continue_around_transition_execution_on_second_call @callbacks = [] @machine.around_transition { |block| @callbacks << :before_around_1; block.call; @callbacks << :after_around_1 } @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 } @machine.after_transition { @callbacks << :after } assert_equal true, @transition.run_callbacks(after: false) assert_equal [:before_around_1, :before_around_2], @callbacks assert_equal true, @transition.run_callbacks assert_equal [: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 { |block| @callbacks << :before_around_1; block.call; @callbacks << :after_around_1 } @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2; throw :halt } @machine.after_transition { @callbacks << :after } assert_equal true, @transition.run_callbacks(after: false) assert_equal [:before_around_1, :before_around_2], @callbacks assert_equal true, @transition.run_callbacks assert_equal [: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 { |block| block.call; @count += 1 } @machine.after_transition { @count += 1 } @transition.run_callbacks(after: false) 2.times do assert_equal true, @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 { |block| block.call; @count += 1; throw :halt } @machine.after_transition { @count += 1 } @transition.run_callbacks(after: false) 2.times do assert_equal true, @transition.run_callbacks assert_equal 1, @count end end def test_should_have_access_to_result_after_continued @machine.around_transition { |block| @around_before_result = @transition.result; block.call; @around_after_result = @transition.result } @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 { |block| block.call; fail ArgumentError } @transition.run_callbacks(after: false) assert_raises(ArgumentError) { @transition.run_callbacks } end else def test_should_raise_exception_on_second_call @callbacks = [] @machine.around_transition { |block| @callbacks << :before_around_1; block.call; @callbacks << :after_around_1 } @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 } @machine.after_transition { @callbacks << :after } assert_raises(ArgumentError) { @transition.run_callbacks(after: false) } end end end state_machines-0.6.0/test/unit/transition/transition_with_after_callbacks_test.rb000066400000000000000000000050661444665775700306650ustar00rootroot00000000000000require '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_equal true, result assert_equal true, @run end def test_should_only_run_those_that_match_transition_context @count = 0 callback = lambda { @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_equal true, result end def test_should_not_catch_exceptions @machine.after_transition { fail 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 { @count += 1; throw :halt } @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.6.0/test/unit/transition/transition_with_around_callbacks_test.rb000066400000000000000000000111671444665775700310530ustar00rootroot00000000000000require '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 { |_object, _transition, block| @run_before = true; block.call; @run_after = true } result = @transition.run_callbacks assert_equal true, result assert_equal true, @run_before assert_equal true, @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 { |*args| block = args.pop; @args = args; block.call } @transition.run_callbacks assert_equal [@object, @transition], @args end def test_should_run_block_between_callback @callbacks = [] @machine.around_transition { |block| @callbacks << :before; block.call; @callbacks << :after } @transition.run_callbacks { @callbacks << :within; { success: true } } assert_equal [:before, :within, :after], @callbacks end def test_should_have_access_to_result_after_yield @machine.around_transition { |block| @before_result = @transition.result; block.call; @after_result = @transition.result } @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 assert_equal false, result end def test_should_catch_after_yield_halts @machine.around_transition { |block| block.call; throw :halt } result = @transition.run_callbacks assert_equal true, result end def test_should_not_catch_before_yield @machine.around_transition { fail ArgumentError } assert_raises(ArgumentError) { @transition.run_callbacks } end def test_should_not_catch_after_yield @machine.around_transition { |block| block.call; fail ArgumentError } assert_raises(ArgumentError) { @transition.run_callbacks } end def test_should_fail_if_not_yielded @machine.around_transition {} result = @transition.run_callbacks assert_equal false, result end def test_should_not_be_able_to_run_twice @before_count = 0 @after_count = 0 @machine.around_transition { |block| @before_count += 1; block.call; @after_count += 1 } @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 { |block| @before_count += 1; block.call; @after_count += 1 } @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 { |block| @before_run = true; block.call; @after_run = true } assert_equal true, @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 { |block| @before_run = true; block.call; @after_run = true } assert_equal true, @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 { |block| @before_run = true; block.call; @after_run = true } assert_equal true, @transition.run_callbacks { { success: false } } assert @before_run refute @after_run end def test_should_succeed_if_block_success_is_false @machine.around_transition { |block| @before_run = true; block.call; @after_run = true } assert_equal true, @transition.run_callbacks { { success: true } } assert @before_run assert @after_run end end state_machines-0.6.0/test/unit/transition/transition_with_before_callbacks_skipped_test.rb000066400000000000000000000013761444665775700325450ustar00rootroot00000000000000require '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 } assert_equal false, @transition.run_callbacks(before: false) refute @run end def test_should_run_failure_callbacks @machine.after_failure { @run = true } assert_equal false, @transition.run_callbacks(before: false) assert @run end end state_machines-0.6.0/test/unit/transition/transition_with_before_callbacks_test.rb000066400000000000000000000057161444665775700310300ustar00rootroot00000000000000require '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_equal true, result assert_equal true, @run end def test_should_only_run_those_that_match_transition_context @count = 0 callback = lambda { @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 assert_equal false, result end def test_should_not_catch_exceptions @machine.before_transition { fail 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 { @count += 1; throw :halt } @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_equal true, @transition.run_callbacks { { result: false } } assert @run end def test_should_succeed_if_block_result_is_true @machine.before_transition { @run = true } assert_equal true, @transition.run_callbacks { { result: true } } assert @run end def test_should_succeed_if_block_success_is_false @machine.before_transition { @run = true } assert_equal true, @transition.run_callbacks { { success: false } } assert @run end def test_should_succeed_if_block_success_is_true @machine.before_transition { @run = true } assert_equal true, @transition.run_callbacks { { success: true } } assert @run end end state_machines-0.6.0/test/unit/transition/transition_with_custom_machine_attribute_test.rb000066400000000000000000000012441444665775700326400ustar00rootroot00000000000000require '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.6.0/test/unit/transition/transition_with_different_states_test.rb000066400000000000000000000007201444665775700311060ustar00rootroot00000000000000require '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 @transition.loopback? end end state_machines-0.6.0/test/unit/transition/transition_with_dynamic_to_value_test.rb000066400000000000000000000007721444665775700311060ustar00rootroot00000000000000require 'test_helper' class TransitionWithDynamicToValueTest < StateMachinesTest def setup @klass = Class.new @machine = StateMachines::Machine.new(@klass) @machine.state :parked @machine.state :idling, value: lambda { 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.6.0/test/unit/transition/transition_with_failure_callbacks_test.rb000066400000000000000000000046051444665775700312110ustar00rootroot00000000000000require '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 = lambda { @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_equal true, result end def test_should_not_catch_exceptions @machine.after_failure { fail 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 { @count += 1; throw :halt } @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.6.0/test/unit/transition/transition_with_invalid_nodes_test.rb000066400000000000000000000017171444665775700304020ustar00rootroot00000000000000require '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.6.0/test/unit/transition/transition_with_mixed_callbacks_test.rb000066400000000000000000000113211444665775700306610ustar00rootroot00000000000000require '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 { |block| @callbacks << :around; block.call } @machine.before_transition { @callbacks << :before_2 } assert_equal true, @transition.run_callbacks assert_equal [: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 { |block| block.call; @callbacks << :after_2 } @machine.after_transition { @callbacks << :after_3 } assert_equal true, @transition.run_callbacks assert_equal [: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 { |block| block.call; @around_result = @transition.result } @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 { |block| @callbacks << :before_around_1; block.call; @callbacks << :after_around_1 } @machine.before_transition { @callbacks << :before_2; throw :halt } @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 } @machine.after_transition { @callbacks << :after } assert_equal false, @transition.run_callbacks assert_equal [: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 { |_block| @callbacks << :before_around_1; throw :halt } @machine.before_transition { @callbacks << :before_2; throw :halt } @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 } @machine.after_transition { @callbacks << :after } assert_equal false, @transition.run_callbacks assert_equal [: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 { @callbacks << :before_2; throw :halt } @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 } @machine.after_transition { @callbacks << :after } assert_equal false, @transition.run_callbacks assert_equal [: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 { |block| @callbacks << :before_around_1; block.call; @callbacks << :after_around_1; throw :halt } @machine.before_transition { @callbacks << :before_2 } @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 } @machine.after_transition { @callbacks << :after } assert_equal true, @transition.run_callbacks assert_equal [: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 { |block| @callbacks << :before_around_1; block.call; @callbacks << :after_around_1 } @machine.before_transition { @callbacks << :before_2 } @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 } @machine.after_transition { @callbacks << :after_1; throw :halt } @machine.after_transition { @callbacks << :after_2 } assert_equal true, @transition.run_callbacks assert_equal [:before_1, :before_around_1, :before_2, :before_around_2, :after_around_2, :after_around_1, :after_1], @callbacks end end state_machines-0.6.0/test/unit/transition/transition_with_multiple_after_callbacks_test.rb000066400000000000000000000021261444665775700325720ustar00rootroot00000000000000require '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 { @callbacks << 1; throw :halt } @machine.after_transition { @callbacks << 2 } assert_equal true, @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_equal true, @transition.run_callbacks end end state_machines-0.6.0/test/unit/transition/transition_with_multiple_around_callbacks_test.rb000066400000000000000000000102321444665775700327560ustar00rootroot00000000000000require '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 { |block| @callbacks << 1; block.call } @machine.around_transition { |block| @callbacks << 2; block.call } @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 { |block| block.call; @callbacks << 1 } @machine.around_transition { |block| block.call; @callbacks << 2 } @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 }) { |block| block.call; @callbacks << 2 } @machine.around_transition(lambda { |block| block.call; @callbacks << 3 }) { |block| block.call; @callbacks << 4 } @transition.run_callbacks assert_equal [4, 3, 2, 1], @callbacks end def test_should_run_block_between_callback @callbacks = [] @machine.around_transition { |block| @callbacks << :before_1; block.call; @callbacks << :after_1 } @machine.around_transition { |block| @callbacks << :before_2; block.call; @callbacks << :after_2 } @transition.run_callbacks { @callbacks << :within; { success: true } } assert_equal [:before_1, :before_2, :within, :after_2, :after_1], @callbacks end def test_should_have_access_to_result_after_yield @machine.around_transition { |block| @before_result_1 = @transition.result; block.call; @after_result_1 = @transition.result } @machine.around_transition { |block| @before_result_2 = @transition.result; block.call; @after_result_2 = @transition.result } @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 } assert_equal false, @transition.run_callbacks end def test_should_not_continue_around_callbacks_if_before_yield_halted @callbacks = [] @machine.around_transition { @callbacks << 1; throw :halt } @machine.around_transition { |block| @callbacks << 2; block.call; @callbacks << 3 } assert_equal false, @transition.run_callbacks assert_equal [1], @callbacks end def test_should_not_continue_around_callbacks_if_later_before_yield_halted @callbacks = [] @machine.around_transition { |block| block.call; @callbacks << 1 } @machine.around_transition { throw :halt } @transition.run_callbacks assert_equal [], @callbacks end def test_should_not_run_further_callbacks_if_after_yield_halted @callbacks = [] @machine.around_transition { |block| block.call; @callbacks << 1 } @machine.around_transition { |block| block.call; throw :halt } assert_equal true, @transition.run_callbacks assert_equal [], @callbacks end def test_should_fail_if_any_fail_to_yield @callbacks = [] @machine.around_transition { @callbacks << 1 } @machine.around_transition { |block| @callbacks << 2; block.call; @callbacks << 3 } assert_equal false, @transition.run_callbacks assert_equal [1], @callbacks end end state_machines-0.6.0/test/unit/transition/transition_with_multiple_before_callbacks_test.rb000066400000000000000000000021371444665775700327350ustar00rootroot00000000000000require '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 { @callbacks << 1; throw :halt } @machine.before_transition { @callbacks << 2 } assert_equal false, @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 } assert_equal false, @transition.run_callbacks end end state_machines-0.6.0/test/unit/transition/transition_with_multiple_failure_callbacks_test.rb000066400000000000000000000022131444665775700331150ustar00rootroot00000000000000require '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 { @callbacks << 1; throw :halt } @machine.after_failure { @callbacks << 2 } assert_equal true, @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_equal true, @transition.run_callbacks { { success: false } } end end state_machines-0.6.0/test/unit/transition/transition_with_namespace_test.rb000066400000000000000000000022501444665775700275110ustar00rootroot00000000000000require '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.6.0/test/unit/transition/transition_with_perform_arguments_test.rb000066400000000000000000000014331444665775700313160ustar00rootroot00000000000000require '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.6.0/test/unit/transition/transition_with_transactions_test.rb000066400000000000000000000017271444665775700302750ustar00rootroot00000000000000require '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.6.0/test/unit/transition/transition_without_callbacks_test.rb000066400000000000000000000015221444665775700302250ustar00rootroot00000000000000require '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_equal true, @transition.run_callbacks end def test_should_succeed_if_after_callbacks_skipped assert_equal true, @transition.run_callbacks(after: false) end def test_should_call_block_if_provided @transition.run_callbacks { @ran_block = true; {} } 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.6.0/test/unit/transition/transition_without_reading_state_test.rb000066400000000000000000000011031444665775700311120ustar00rootroot00000000000000require '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.6.0/test/unit/transition/transition_without_running_action_test.rb000066400000000000000000000020271444665775700313240ustar00rootroot00000000000000require '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_equal [], @transition.args end def test_should_not_have_a_result assert_nil @transition.result end def test_should_be_successful assert_equal true, @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.6.0/test/unit/transition_collection/000077500000000000000000000000001444665775700231005ustar00rootroot00000000000000attribute_transition_collection_by_default_test.rb000066400000000000000000000007431444665775700352770ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 @transitions.empty? end end attribute_transition_collection_marshalling_test.rb000066400000000000000000000035011444665775700354550ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 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 if StateMachines::Transition.pause_supported? def test_should_marshal_during_around_callbacks_before_yield @machine.around_transition { |object, _transition, block| Marshal.dump(object); block.call } transitions(after: false).perform { true } transitions.perform { true } end def test_should_marshal_during_around_callbacks_after_yield @machine.around_transition { |object, _transition, block| block.call; Marshal.dump(object) } transitions(after: false).perform { true } transitions.perform { true } end end def teardown self.class.send(:remove_const, 'Example') 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.rb000066400000000000000000000024631444665775700367030ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 { fail ArgumentError } rescue 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.rb000066400000000000000000000025241444665775700367740ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 assert_equal false, @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.rb000066400000000000000000000014571444665775700403450ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 { fail ArgumentError } @object = @klass.new @object.state_event = 'ignite' @transitions = StateMachines::AttributeTransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) begin ; @transitions.perform rescue 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.rb000066400000000000000000000015251444665775700401400ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 assert_equal false, @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.rb000066400000000000000000000014711444665775700430770ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 { fail ArgumentError } @object = @klass.new @object.state_event = 'ignite' @transitions = StateMachines::AttributeTransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) begin ; @transitions.perform rescue 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.rb000066400000000000000000000014761444665775700431040ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 { |block| block.call; fail ArgumentError } @object = @klass.new @object.state_event = 'ignite' @transitions = StateMachines::AttributeTransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) begin ; @transitions.perform rescue 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.rb000066400000000000000000000015371444665775700427010ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 { |block| block.call; 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_equal true, @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.rb000066400000000000000000000015401444665775700430340ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 assert_equal false, @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.rb000066400000000000000000000014361444665775700405030ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 { fail ArgumentError } @object = @klass.new @object.state_event = 'ignite' @transitions = StateMachines::AttributeTransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) begin ; @transitions.perform rescue 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.rb000066400000000000000000000014771444665775700403070ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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_equal true, @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.rb000066400000000000000000000047671444665775700361450ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 { |object, _transition, block| @around_state_event = object.state_event; block.call } @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 { |object, _transition, block| block.call; @around_state_event = object.state_event } @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 { |object, _transition, block| block.call; @around_state_event_transition = object.send(:state_event_transition) } @transitions.perform assert_nil @after_state_event_transition assert_nil @around_state_event_transition end end attribute_transition_collection_with_event_transitions_test.rb000066400000000000000000000025151444665775700377710ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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_equal true, @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.rb000066400000000000000000000024401444665775700355140ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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_equal true, @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.rb000066400000000000000000000026241444665775700410330ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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.6.0/test/unit/transition_collection/transition_collection_by_default_test.rb000066400000000000000000000007211444665775700332670ustar00rootroot00000000000000require '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 @transitions.empty? end end state_machines-0.6.0/test/unit/transition_collection/transition_collection_empty_with_block_test.rb000066400000000000000000000011561444665775700345170ustar00rootroot00000000000000require '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 { fail 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 assert_equal false, @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.rb000066400000000000000000000004171444665775700351670ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire 'test_helper' class TransitionCollectionEmptyWithoutBlockTest < StateMachinesTest def setup @transitions = StateMachines::TransitionCollection.new @result = @transitions.perform end def test_should_succeed assert_equal true, @result end end state_machines-0.6.0/test/unit/transition_collection/transition_collection_invalid_test.rb000066400000000000000000000007061444665775700326020ustar00rootroot00000000000000require 'test_helper' class TransitionCollectionInvalidTest < StateMachinesTest def setup @transitions = StateMachines::TransitionCollection.new([false]) end def test_should_be_empty assert @transitions.empty? end def test_should_not_succeed assert_equal false, @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.6.0/test/unit/transition_collection/transition_collection_partial_invalid_test.rb000066400000000000000000000032771444665775700343240ustar00rootroot00000000000000require '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 { |block| @callbacks << :around_before; block.call; @callbacks << :around_after } 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 assert_equal false, @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 @callbacks.include?(:before) end def test_should_not_persist_states assert_equal 'parked', @object.state end def test_should_not_run_after_callbacks refute @callbacks.include?(:after) end def test_should_not_run_around_callbacks_before_yield refute @callbacks.include?(:around_before) end def test_should_not_run_around_callbacks_after_yield refute @callbacks.include?(:around_after) end end state_machines-0.6.0/test/unit/transition_collection/transition_collection_test.rb000066400000000000000000000020301444665775700310640ustar00rootroot00000000000000require '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', 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.6.0/test/unit/transition_collection/transition_collection_valid_test.rb000066400000000000000000000026641444665775700322600ustar00rootroot00000000000000require '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_equal true, @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 state_machines-0.6.0/test/unit/transition_collection/transition_collection_with_action_error_test.rb000066400000000000000000000031121444665775700346670ustar00rootroot00000000000000require 'test_helper' class TransitionCollectionWithActionErrorTest < StateMachinesTest def setup @klass = Class.new do def save fail 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 { |block| @around_before_count += 1; block.call; @around_after_count += 1 } @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.rb000066400000000000000000000027461444665775700347170ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 { |block| @around_before_count += 1; block.call; @around_after_count += 1 } @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 assert_equal false, @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.rb000066400000000000000000000006641444665775700364240ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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_equal true, @result end def test_should_not_run_action refute @object.saved end end transition_collection_with_action_hook_and_skipped_action_test.rb000066400000000000000000000007061444665775700403230ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire 'test_helper' require_relative 'transition_collection_with_action_hook_base_test.rb' class TransitionCollectionWithActionHookAndSkippedActionTest < TransitionCollectionWithActionHookBaseTest def setup super @result = StateMachines::TransitionCollection.new([@transition], actions: false).perform end def test_should_succeed assert_equal true, @result end def test_should_not_run_action refute @object.saved end end transition_collection_with_action_hook_and_skipped_after_callbacks_test.rb000066400000000000000000000017071444665775700421500ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire 'test_helper' require_relative 'transition_collection_with_action_hook_base_test.rb' class TransitionCollectionWithActionHookAndSkippedAfterCallbacksTest < TransitionCollectionWithActionHookBaseTest def setup super @result = StateMachines::TransitionCollection.new([@transition], after: false).perform end def test_should_succeed assert_equal true, @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.rb000066400000000000000000000014651444665775700354220ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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.rb000066400000000000000000000011551444665775700356350ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire 'test_helper' require_relative 'transition_collection_with_action_hook_base_test.rb' class TransitionCollectionWithActionHookErrorTest < TransitionCollectionWithActionHookBaseTest def setup super @superclass.class_eval do remove_method :save def save fail ArgumentError end end begin ; StateMachines::TransitionCollection.new([@transition]).perform rescue 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.rb000066400000000000000000000006671444665775700361410ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire 'test_helper' require_relative 'transition_collection_with_action_hook_base_test.rb' class TransitionCollectionWithActionHookInvalidTest < TransitionCollectionWithActionHookBaseTest def setup super @result = StateMachines::TransitionCollection.new([@transition, nil]).perform end def test_should_not_succeed assert_equal false, @result end def test_should_not_run_action refute @object.saved end end transition_collection_with_action_hook_multiple_test.rb000066400000000000000000000046211444665775700363400ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire 'test_helper' require_relative 'transition_collection_with_action_hook_base_test.rb' 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 @state_transition.transient? assert @status_transition.transient? end end state_machines-0.6.0/test/unit/transition_collection/transition_collection_with_action_hook_test.rb000066400000000000000000000021331444665775700345000ustar00rootroot00000000000000require 'test_helper' require_relative 'transition_collection_with_action_hook_base_test.rb' class TransitionCollectionWithActionHookTest < TransitionCollectionWithActionHookBaseTest def setup super @result = StateMachines::TransitionCollection.new([@transition]).perform end def test_should_succeed assert_equal true, @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 @transition.transient? end end transition_collection_with_action_hook_with_different_actions_test.rb000066400000000000000000000024021444665775700412210ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire 'test_helper' require_relative 'transition_collection_with_action_hook_base_test.rb' 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_equal true, @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.rb000066400000000000000000000022261444665775700376560ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire 'test_helper' require_relative 'transition_collection_with_action_hook_base_test.rb' 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_equal true, @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.rb000066400000000000000000000022541444665775700360550ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 { @after_count += 1; throw :halt } @machine.after_transition { @after_count += 1 } @machine.around_transition { |block| @before_count += 1; block.call; @after_count += 1 } @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ]) @result = @transitions.perform end def test_should_succeed assert_equal true, @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.rb000066400000000000000000000023751444665775700362220ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 { @before_count += 1; throw :halt } @machine.before_transition { @before_count += 1 } @machine.after_transition { @after_count += 1 } @machine.around_transition { |block| @before_count += 1; block.call; @after_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 assert_equal false, @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.6.0/test/unit/transition_collection/transition_collection_with_block_test.rb000066400000000000000000000024101444665775700332730ustar00rootroot00000000000000require '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.6.0/test/unit/transition_collection/transition_collection_with_callbacks_test.rb000066400000000000000000000107171444665775700341310ustar00rootroot00000000000000require '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 { |block| @before_callbacks << :state_around; block.call; @after_callbacks << :state_around } @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 { |block| @before_callbacks << :status_around; block.call; @after_callbacks << :status_around } @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 [: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 } assert_equal false, @transitions.perform assert_equal [:state_before, :state_around], @before_callbacks end def test_should_halt_if_before_callback_halted_for_second_transition @status.before_transition { throw :halt } assert_equal false, @transitions.perform assert_equal [: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 } assert_equal false, @transitions.perform assert_equal [: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 } assert_equal false, @transitions.perform assert_equal [:state_before, :state_around, :status_before, :status_around], @before_callbacks end def test_should_run_after_callbacks_in_reverse_order @transitions.perform assert_equal [: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_equal true, @transitions.perform assert_equal [: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 { |block| block.call; throw :halt } assert_equal true, @transitions.perform assert_equal [: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 { |object, _transition, block| @around_state = object.state; block.call } @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 { |object, _transition, block| block.call; @around_saved = object.saved } @transitions.perform assert @after_saved assert @around_saved end end transition_collection_with_different_actions_test.rb000066400000000000000000000113561444665775700356210ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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_equal true, @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 [: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 assert_equal false, @transitions.perform assert_equal [: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 assert_equal false, @transitions.perform assert_equal [: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 fail ArgumentError end end begin ; @transitions.perform rescue 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 fail ArgumentError end end begin ; @transitions.perform rescue 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 { |block| block.call; @callbacks << :state_around } @status.after_transition { @callbacks << :status_after } @status.around_transition { |block| block.call; @callbacks << :status_around } @transitions.perform assert_equal [], @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 { |block| block.call; @callbacks << :state_around } @status.after_transition { @callbacks << :status_after } @status.around_transition { |block| block.call; @callbacks << :status_around } @transitions.perform assert_equal [], @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 [: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 [:status_after, :state_after], @callbacks end end transition_collection_with_duplicate_actions_test.rb000066400000000000000000000024401444665775700356170ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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.rb000066400000000000000000000021511444665775700350020ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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_equal true, @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.rb000066400000000000000000000021571444665775700347600ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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_equal true, @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_equal true, @state_transition.result assert_nil @status_transition.result end end transition_collection_with_skipped_actions_and_block_test.rb000066400000000000000000000015341444665775700373030ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 { @ran_block = true; 1 } 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.rb000066400000000000000000000043051444665775700353060ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 { |block| @callbacks << :state_around_before; block.call; @callbacks << :state_around_after } @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 { |block| @callbacks << :status_around_before; block.call; @callbacks << :status_around_after } @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_equal true, @transitions.skip_actions end def test_should_succeed assert_equal true, @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 [: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.rb000066400000000000000000000036061444665775700431420ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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 { |block| @callbacks << :around_before; block.call; @callbacks << :around_after } @object = @klass.new @transitions = StateMachines::TransitionCollection.new([ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling) ], after: false) end def test_should_raise_exception skip('test not supported in this Ruby Engine') if StateMachines::Transition.pause_supported? assert_raises(ArgumentError) { @transitions.perform } end def test_should_succeed skip('test not supported in this Ruby Engine') unless StateMachines::Transition.pause_supported? @transitions.perform assert_equal true, @transitions.perform end def test_should_not_run_around_callbacks_after_yield skip('test not supported in this Ruby Engine') unless StateMachines::Transition.pause_supported? @transitions.perform refute @callbacks.include?(:around_after) end def test_should_run_around_callbacks_after_yield_on_subsequent_perform skip('test not supported in this Ruby Engine') unless StateMachines::Transition.pause_supported? @transitions.perform StateMachines::TransitionCollection.new([@transition]).perform assert @callbacks.include?(:around_after) end def test_should_not_rerun_around_callbacks_before_yield_on_subsequent_perform skip('test not supported in this Ruby Engine') unless StateMachines::Transition.pause_supported? @transitions.perform @callbacks = [] StateMachines::TransitionCollection.new([@transition]).perform refute @callbacks.include?(:around_before) end end transition_collection_with_skipped_after_callbacks_test.rb000066400000000000000000000016141444665775700367460ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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_equal true, @result end def test_should_not_run_after_callbacks refute @callbacks.include?(:after) end def test_should_run_after_callbacks_on_subsequent_perform StateMachines::TransitionCollection.new([@transition]).perform assert @callbacks.include?(:after) end end state_machines-0.6.0/test/unit/transition_collection/transition_collection_with_transactions_test.rb000066400000000000000000000034331444665775700347170ustar00rootroot00000000000000require '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.rb000066400000000000000000000013421444665775700353650ustar00rootroot00000000000000state_machines-0.6.0/test/unit/transition_collectionrequire '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