statemachine 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGES ADDED
@@ -0,0 +1,12 @@
1
+ = StateMachine Changelog
2
+
3
+ == Version 0.0.0
4
+
5
+ The first release. Most finite state machine features are implemented
6
+ * states
7
+ * transitions
8
+ * transition actions
9
+ * super states
10
+ * entry actions
11
+ * exit actions
12
+ * history state
data/Rakefile ADDED
@@ -0,0 +1,170 @@
1
+ $:.unshift('lib')
2
+ require 'rubygems'
3
+ require 'rake/gempackagetask'
4
+ require 'rake/contrib/rubyforgepublisher'
5
+ require 'rake/clean'
6
+ require 'rake/rdoctask'
7
+ require 'spec/rake/spectask'
8
+
9
+ # Some of the tasks are in separate files since they are also part of the website documentation
10
+ "load File.dirname(__FILE__) + '/tasks/examples.rake'
11
+ load File.dirname(__FILE__) + '/tasks/examples_specdoc.rake'
12
+ load File.dirname(__FILE__) + '/tasks/examples_with_rcov.rake'
13
+ load File.dirname(__FILE__) + '/tasks/failing_examples_with_html.rake'
14
+ load File.dirname(__FILE__) + '/tasks/verify_rcov.rake'"
15
+
16
+ PKG_NAME = "statemachine"
17
+ PKG_VERSION = "0.0.0"
18
+ PKG_TAG = "0_0_0"
19
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
20
+ PKG_FILES = FileList[
21
+ '[A-Z]*',
22
+ 'lib/**/*.rb',
23
+ 'spec/**/*.rb'
24
+ # 'examples/**/*',
25
+ ]
26
+
27
+ task :default => :spec
28
+
29
+ desc "Run all specs"
30
+ Spec::Rake::SpecTask.new do |t|
31
+ t.spec_files = FileList['spec/**/*_spec.rb']
32
+ t.spec_opts = ['--diff','--color']
33
+ # t.rcov = true
34
+ # t.rcov_dir = 'doc/output/coverage'
35
+ # t.rcov_opts = ['--exclude', 'spec\/spec,bin\/spec']"
36
+ end
37
+
38
+ #desc 'Generate HTML documentation for website'
39
+ #task :webgen do
40
+ # Dir.chdir 'doc' do
41
+ # output = nil
42
+ # IO.popen('webgen 2>&1') do |io|
43
+ # output = io.read
44
+ # end
45
+ # raise "ERROR while running webgen: #{output}" if output =~ /ERROR/n || $? != 0
46
+ # end
47
+ #end
48
+
49
+ #desc 'Generate RDoc'
50
+ #rd = Rake::RDocTask.new do |rdoc|
51
+ # rdoc.rdoc_dir = 'doc/output/rdoc'
52
+ # rdoc.options << '--title' << 'RSpec' << '--line-numbers' << '--inline-source' << '--main' << 'README'
53
+ # rdoc.rdoc_files.include('README', 'CHANGES', 'EXAMPLES.rd', 'lib/**/*.rb')
54
+ #end
55
+ #task :rdoc => :examples_specdoc # We generate EXAMPLES.rd
56
+
57
+ spec = Gem::Specification.new do |s|
58
+ s.name = PKG_NAME
59
+ s.version = PKG_VERSION
60
+ s.summary = Spec::VERSION::DESCRIPTION
61
+ s.description = <<-EOF
62
+ StateMachine is a ruby library for building Finite State Machines (FSM), also known as Finite State Automata (FSA).
63
+ EOF
64
+
65
+ s.files = PKG_FILES.to_a
66
+ s.require_path = 'lib'
67
+
68
+ # s.has_rdoc = true
69
+ # s.rdoc_options = rd.options
70
+ # s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$|^EXAMPLES.rd$/ }.to_a
71
+
72
+ s.test_files = Dir.glob('spec/*_spec.rb')
73
+ s.require_path = 'lib'
74
+ s.autorequire = 'statemachine'
75
+ # s.bindir = "bin"
76
+ # s.executables = ["spec"]
77
+ # s.default_executable = "spec"
78
+ s.author = ["Micah Martin"]
79
+ s.email = "statemachine-devel@rubyforge.org"
80
+ s.homepage = "http://statemachine.rubyforge.org"
81
+ s.rubyforge_project = "statemachine"
82
+ end
83
+
84
+ Rake::GemPackageTask.new(spec) do |pkg|
85
+ pkg.need_zip = true
86
+ pkg.need_tar = true
87
+ end
88
+
89
+ def egrep(pattern)
90
+ Dir['**/*.rb'].each do |fn|
91
+ count = 0
92
+ open(fn) do |f|
93
+ while line = f.gets
94
+ count += 1
95
+ if line =~ pattern
96
+ puts "#{fn}:#{count}:#{line}"
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ desc "Look for TODO and FIXME tags in the code"
104
+ task :todo do
105
+ egrep /(FIXME|TODO|TBD)/
106
+ end
107
+
108
+ task :clobber do
109
+ rm_rf 'doc/output'
110
+ end
111
+
112
+ task :release => [:clobber, :verify_committed, :verify_user, :verify_password, :spec, :publish_packages, :tag, :publish_website, :publish_news]
113
+
114
+ desc "Verifies that there is no uncommitted code"
115
+ task :verify_committed do
116
+ IO.popen('svn stat') do |io|
117
+ io.each_line do |line|
118
+ raise "\n!!! Do a svn commit first !!!\n\n" if line =~ /^\s*M\s*/
119
+ end
120
+ end
121
+ end
122
+
123
+ desc "Creates a tag in svn"
124
+ task :tag do
125
+ puts "Creating tag in SVN"
126
+ `svn cp svn+ssh://#{ENV['RUBYFORGE_USER']}@rubyforge.org/var/svn/statemachine/trunk svn+ssh://#{ENV['RUBYFORGE_USER']}@rubyforge.org/var/svn/statemachine/tags/#{PKG_VERSION} -m "Tag release #{PKG_TAG}"`
127
+ puts "Done!"
128
+ end
129
+
130
+
131
+ #desc "Build the website, but do not publish it"
132
+ #task :website => [:clobber, :verify_rcov, :webgen, :failing_examples_with_html, :spec, :examples_specdoc, :rdoc]
133
+
134
+ #desc "Upload Website to RubyForge"
135
+ #task :publish_website => [:verify_user, :website] do
136
+ # publisher = Rake::SshDirPublisher.new(
137
+ # "rspec-website@rubyforge.org",
138
+ # "/var/www/gforge-projects/#{PKG_NAME}",
139
+ # "doc/output"
140
+ # )
141
+
142
+ # publisher.upload
143
+ #end
144
+
145
+ task :verify_user do
146
+ raise "RUBYFORGE_USER environment variable not set!" unless ENV['RUBYFORGE_USER']
147
+ end
148
+
149
+ task :verify_password do
150
+ raise "RUBYFORGE_PASSWORD environment variable not set!" unless ENV['RUBYFORGE_PASSWORD']
151
+ end
152
+
153
+ desc "Publish gem+tgz+zip on RubyForge. You must make sure lib/version.rb is aligned with the CHANGELOG file"
154
+ task :publish_packages => [:verify_user, :verify_password, :package] do
155
+ require 'meta_project'
156
+ require 'rake/contrib/xforge'
157
+ release_files = FileList[
158
+ "pkg/#{PKG_FILE_NAME}.gem",
159
+ "pkg/#{PKG_FILE_NAME}.tgz",
160
+ "pkg/#{PKG_FILE_NAME}.zip"
161
+ ]
162
+
163
+ Rake::XForge::Release.new(MetaProject::Project::XForge::RubyForge.new(PKG_NAME)) do |xf|
164
+ # Never hardcode user name and password in the Rakefile!
165
+ xf.user_name = ENV['RUBYFORGE_USER']
166
+ xf.password = ENV['RUBYFORGE_PASSWORD']
167
+ xf.files = release_files.to_a
168
+ xf.release_name = "StateMachine #{PKG_VERSION}"
169
+ end
170
+ end
@@ -0,0 +1,54 @@
1
+ module StateMachine
2
+
3
+ module ProcCalling
4
+
5
+ private
6
+
7
+ def call_proc(proc, args, message)
8
+ arity = proc.arity
9
+ if should_call_with(arity, 0, args, message)
10
+ proc.call
11
+ elsif should_call_with(arity, 1, args, message)
12
+ proc.call args[0]
13
+ elsif should_call_with(arity, 2, args, message)
14
+ proc.call args[0], args[1]
15
+ elsif should_call_with(arity, 3, args, message)
16
+ proc.call args[0], args[1], args[2]
17
+ elsif should_call_with(arity, 4, args, message)
18
+ proc.call args[0], args[1], args[2], args[3]
19
+ elsif should_call_with(arity, 5, args, message)
20
+ proc.call args[0], args[1], args[2], args[3], args[4]
21
+ elsif should_call_with(arity, 6, args, message)
22
+ proc.call args[0], args[1], args[2], args[3], args[4], args[5]
23
+ elsif should_call_with(arity, 7, args, message)
24
+ proc.call args[0], args[1], args[2], args[3], args[4], args[5], args[6]
25
+ elsif should_call_with(arity, 8, args, message)
26
+ proc.call args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]
27
+ elsif arity < 0 and args and args.length > 8
28
+ proc.call args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]
29
+ else
30
+ raise StateMachineException.new("Too many arguments(#{args.length}). (#{message})")
31
+ end
32
+ end
33
+
34
+ def should_call_with(arity, n, args, message)
35
+ actual = args ? args.length : 0
36
+ if arity == n
37
+ return enough_args?(actual, arity, arity, message)
38
+ elsif arity < 0
39
+ required_args = (arity * -1) - 1
40
+ return (actual == n and enough_args?(actual, required_args, arity, message))
41
+ end
42
+ end
43
+
44
+ def enough_args?(actual, required, arity, message)
45
+ if actual >= required
46
+ return true
47
+ else
48
+ raise StateMachineException.new("Insufficient parameters. (#{message})")
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ end
data/lib/state.rb ADDED
@@ -0,0 +1,69 @@
1
+ require File.dirname(__FILE__) + '/proc_calling'
2
+
3
+ module StateMachine
4
+
5
+ class State
6
+
7
+ include ProcCalling
8
+
9
+ attr_reader :id, :statemachine, :entry_action, :exit_action
10
+ attr_accessor :superstate
11
+
12
+ def initialize(id, state_machine)
13
+ @id = id
14
+ @statemachine = state_machine
15
+ @transitions = {}
16
+ end
17
+
18
+ def add(transition)
19
+ @transitions[transition.event] = transition
20
+ end
21
+
22
+ def transitions
23
+ return @superstate ? @transitions.merge(@superstate.transitions) : @transitions
24
+ end
25
+
26
+ def local_transitions
27
+ return @transitions
28
+ end
29
+
30
+ def [] (event)
31
+ return transitions[event]
32
+ end
33
+
34
+ def on_entry action
35
+ @entry_action = action
36
+ end
37
+
38
+ def on_exit action
39
+ @exit_action = action
40
+ end
41
+
42
+ def exit(args)
43
+ @statemachine.trace("\texiting #{self}")
44
+ call_proc(@exit_action, args, "exit action for #{self}") if @exit_action
45
+ @superstate.existing(self) if @superstate
46
+ end
47
+
48
+ def enter(args)
49
+ @statemachine.trace("\tentering #{self}")
50
+ call_proc(@entry_action, args, "entry action for #{self}") if @entry_action
51
+ end
52
+
53
+ def is_superstate?
54
+ return false
55
+ end
56
+
57
+ def to_s
58
+ return "'#{id}' state"
59
+ end
60
+
61
+ def add_substates(*substate_ids)
62
+ raise StateMachineException.new("At least one parameter is required for add_substates.") if substate_ids.length == 0
63
+ replacement = Superstate.new(self, @transitions, substate_ids)
64
+ @statemachine.replace_state(@id, replacement)
65
+ end
66
+
67
+ end
68
+
69
+ end
@@ -0,0 +1,103 @@
1
+ require File.dirname(__FILE__) + '/state'
2
+ require File.dirname(__FILE__) + '/super_state'
3
+ require File.dirname(__FILE__) + '/transition'
4
+ require File.dirname(__FILE__) + '/proc_calling'
5
+
6
+ module StateMachine
7
+
8
+ class StateMachineException < Exception
9
+ end
10
+
11
+ class MissingTransitionException < StateMachineException
12
+ end
13
+
14
+ class StateMachine
15
+
16
+ include ProcCalling
17
+
18
+ attr_reader :states, :state, :running
19
+ attr_accessor :start_state, :tracer
20
+
21
+ def initialize
22
+ @states = {}
23
+ @start_state = nil
24
+ @state = nil
25
+ @running = false
26
+ end
27
+
28
+ def add(origin_id, event, destination_id, action = nil)
29
+ origin = acquire_state(origin_id)
30
+ @start_state = origin if @start_state == nil
31
+ destination = acquire_state(destination_id)
32
+ origin.add(Transition.new(origin, destination, event, action))
33
+ end
34
+
35
+ def run
36
+ @state = @start_state
37
+ @running = true
38
+ end
39
+ alias :reset :run
40
+
41
+ def [] (state_id)
42
+ return @states[state_id]
43
+ end
44
+
45
+ def state= value
46
+ if @states[value]
47
+ @state = @states[value]
48
+ elsif value and @states[value.to_sym]
49
+ @state = @states[value.to_sym]
50
+ end
51
+ end
52
+
53
+ def process_event(event, *args)
54
+ trace "Event: #{event}"
55
+ if @state
56
+ transition = @state.transitions[event]
57
+ if transition
58
+ @state = transition.invoke(@state, args)
59
+ else
60
+ raise MissingTransitionException.new("#{@state} does not respond to the '#{event}' event.")
61
+ end
62
+ @running = @state != nil
63
+ else
64
+ raise StateMachineException.new("The state machine isn't in any state. Did you forget to call run?")
65
+ end
66
+ end
67
+
68
+ def method_missing(message, *args)
69
+ if @state and @state[message]
70
+ method = self.method(:process_event)
71
+ params = [message.to_sym].concat(args)
72
+ call_proc(method, params, "method_missing")
73
+ else
74
+ super(message, args)
75
+ end
76
+ end
77
+
78
+ def acquire_state(state_id)
79
+ return nil if state_id == nil
80
+ state = @states[state_id]
81
+ if not state
82
+ state = State.new(state_id, self)
83
+ @states[state_id] = state
84
+ end
85
+ return state
86
+ end
87
+
88
+ def replace_state(state_id, replacement_state)
89
+ @states[state_id] = replacement_state
90
+ @states.values.each do |state|
91
+ state.local_transitions.values.each do |transition|
92
+ transition.destination = replacement_state if transition.destination.id == state_id
93
+ end
94
+ end
95
+ end
96
+
97
+ def trace(message)
98
+ @tracer.puts message if @tracer
99
+ end
100
+
101
+ end
102
+
103
+ end
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/state_machine'
@@ -0,0 +1,68 @@
1
+ module StateMachine
2
+
3
+ class Superstate < State
4
+
5
+ attr_writer :start_state
6
+
7
+ def initialize(state, transitions, substate_ids)
8
+ @id = state.id
9
+ @statemachine = state.statemachine
10
+ @transitions = transitions
11
+ @entry_action = state.entry_action
12
+ @exit_action = state.exit_action
13
+ @superstate = state.superstate
14
+ do_substate_adding(substate_ids)
15
+ end
16
+
17
+ def is_superstate?
18
+ return true
19
+ end
20
+
21
+ def start_state
22
+ if @use_history and @history_state
23
+ return @history_state
24
+ else
25
+ return @start_state
26
+ end
27
+ end
28
+
29
+ def existing(substate)
30
+ @history_state = substate
31
+ end
32
+
33
+ def add_substates(*substate_ids)
34
+ do_substate_adding(substate_ids)
35
+ end
36
+
37
+ def use_history
38
+ @use_history = true;
39
+ end
40
+
41
+ def to_s
42
+ return "'#{id}' superstate"
43
+ end
44
+
45
+ private
46
+
47
+ def do_substate_adding(substate_ids)
48
+ substate_ids.each do |substate_id|
49
+ substate = @statemachine.acquire_state(substate_id)
50
+ @start_state = substate if not @start_state
51
+ substate.superstate = self
52
+ check_for_substate_recursion
53
+ end
54
+ end
55
+
56
+ def check_for_substate_recursion
57
+ tmp_state = @superstate
58
+ while tmp_state
59
+ if tmp_state == self
60
+ raise StateMachineException.new("Cyclic substates not allowed. (#{id})")
61
+ end
62
+ tmp_state = tmp_state.superstate
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ end
data/lib/transition.rb ADDED
@@ -0,0 +1,71 @@
1
+ require File.dirname(__FILE__) + '/proc_calling'
2
+
3
+ module StateMachine
4
+
5
+ class Transition
6
+
7
+ include ProcCalling
8
+
9
+ attr_reader :origin, :event, :action
10
+ attr_accessor :destination
11
+
12
+ def initialize(origin, destination, event, action)
13
+ @origin = origin
14
+ @destination = destination
15
+ @event = event
16
+ @action = action
17
+ end
18
+
19
+ def invoke(origin, args)
20
+ exits, entries = exits_and_entries(origin)
21
+ exits.each { |exited_state| exited_state.exit(args) }
22
+
23
+ call_proc(@action, args, "transition action from #{origin} invoked by '#{event}' event") if @action
24
+
25
+ entries.each { |entered_state| entered_state.enter(args) }
26
+
27
+ terminal_state = @destination
28
+ while terminal_state and terminal_state.is_superstate?
29
+ terminal_state = terminal_state.start_state
30
+ terminal_state.enter(args)
31
+ end
32
+
33
+ return terminal_state
34
+ end
35
+
36
+ def exits_and_entries(origin)
37
+ exits = []
38
+ entries = exits_and_entries_helper(exits, origin)
39
+
40
+ return exits, entries.reverse
41
+ end
42
+
43
+ def to_s
44
+ return "#{origin.id} ---#{event}---> #{destination.id} : #{action}"
45
+ end
46
+
47
+ private
48
+
49
+ def exits_and_entries_helper(exits, exit_state)
50
+ entries = entries_to_destination(exit_state)
51
+ return entries if entries
52
+ return [] if exit_state == nil
53
+
54
+ exits << exit_state
55
+ exits_and_entries_helper(exits, exit_state.superstate)
56
+ end
57
+
58
+ def entries_to_destination(exit_state)
59
+ entries = []
60
+ state = @destination
61
+ while state
62
+ entries << state
63
+ return entries if exit_state == state.superstate
64
+ state = state.superstate
65
+ end
66
+ return nil
67
+ end
68
+
69
+ end
70
+
71
+ end
@@ -0,0 +1,114 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ context "State Machine Odds And Ends" do
4
+ include SwitchStateMachine
5
+
6
+ setup do
7
+ create_switch
8
+ @sm.run
9
+ end
10
+
11
+ specify "action with one parameter" do
12
+ @sm.add(:off, :set, :on, Proc.new { |value| @status = value } )
13
+ @sm.set "blue"
14
+ @status.should_equal "blue"
15
+ @sm.state.id.should_be :on
16
+ end
17
+
18
+ specify "action with two parameters" do
19
+ @sm.add(:off, :set, :on, Proc.new { |a, b| @status = [a, b].join(",") } )
20
+ @sm.set "blue", "green"
21
+ @status.should_equal "blue,green"
22
+ @sm.state.id.should_be :on
23
+ end
24
+
25
+ specify "action with three parameters" do
26
+ @sm.add(:off, :set, :on, Proc.new { |a, b, c| @status = [a, b, c].join(",") } )
27
+ @sm.set "blue", "green", "red"
28
+ @status.should_equal "blue,green,red"
29
+ @sm.state.id.should_be :on
30
+ end
31
+
32
+ specify "action with four parameters" do
33
+ @sm.add(:off, :set, :on, Proc.new { |a, b, c, d| @status = [a, b, c, d].join(",") } )
34
+ @sm.set "blue", "green", "red", "orange"
35
+ @status.should_equal "blue,green,red,orange"
36
+ @sm.state.id.should_be :on
37
+ end
38
+
39
+ specify "action with five parameters" do
40
+ @sm.add(:off, :set, :on, Proc.new { |a, b, c, d, e| @status = [a, b, c, d, e].join(",") } )
41
+ @sm.set "blue", "green", "red", "orange", "yellow"
42
+ @status.should_equal "blue,green,red,orange,yellow"
43
+ @sm.state.id.should_be :on
44
+ end
45
+
46
+ specify "action with six parameters" do
47
+ @sm.add(:off, :set, :on, Proc.new { |a, b, c, d, e, f| @status = [a, b, c, d, e, f].join(",") } )
48
+ @sm.set "blue", "green", "red", "orange", "yellow", "indigo"
49
+ @status.should_equal "blue,green,red,orange,yellow,indigo"
50
+ @sm.state.id.should_be :on
51
+ end
52
+
53
+ specify "action with seven parameters" do
54
+ @sm.add(:off, :set, :on, Proc.new { |a, b, c, d, e, f, g| @status = [a, b, c, d, e, f, g].join(",") } )
55
+ @sm.set "blue", "green", "red", "orange", "yellow", "indigo", "violet"
56
+ @status.should_equal "blue,green,red,orange,yellow,indigo,violet"
57
+ @sm.state.id.should_be :on
58
+ end
59
+
60
+ specify "action with eight parameters" do
61
+ @sm.add(:off, :set, :on, Proc.new { |a, b, c, d, e, f, g, h| @status = [a, b, c, d, e, f, g, h].join(",") } )
62
+ @sm.set "blue", "green", "red", "orange", "yellow", "indigo", "violet", "ultra-violet"
63
+ @status.should_equal "blue,green,red,orange,yellow,indigo,violet,ultra-violet"
64
+ @sm.state.id.should_be :on
65
+ end
66
+
67
+ specify "To many parameters" do
68
+ @sm.add(:off, :set, :on, Proc.new { |a, b, c, d, e, f, g, h, i| @status = [a, b, c, d, e, f, g, h, i].join(",") } )
69
+ begin
70
+ @sm.process_event(:set, "blue", "green", "red", "orange", "yellow", "indigo", "violet", "ultra-violet", "Yikes!")
71
+ rescue StateMachine::StateMachineException => e
72
+ e.message.should_equal "Too many arguments(9). (transition action from 'off' state invoked by 'set' event)"
73
+ end
74
+ end
75
+
76
+ specify "calling process_event with parameters" do
77
+ @sm.add(:off, :set, :on, Proc.new { |a, b, c| @status = [a, b, c].join(",") } )
78
+ @sm.process_event(:set, "blue", "green", "red")
79
+ @status.should_equal "blue,green,red"
80
+ @sm.state.id.should_be :on
81
+ end
82
+
83
+ specify "Insufficient params" do
84
+ @sm.add(:off, :set, :on, Proc.new { |a, b, c| @status = [a, b, c].join(",") } )
85
+ begin
86
+ @sm.set "blue", "green"
87
+ rescue StateMachine::StateMachineException => e
88
+ e.message.should_equal "Insufficient parameters. (transition action from 'off' state invoked by 'set' event)"
89
+ end
90
+ end
91
+
92
+ specify "infinate args" do
93
+ @sm.add(:off, :set, :on, Proc.new { |*a| @status = a.join(",") } )
94
+ @sm.set(1, 2, 3)
95
+ @status.should_equal "1,2,3"
96
+
97
+ @sm.state = :off
98
+ @sm.set(1, 2, 3, 4, 5, 6)
99
+ @status.should_equal "1,2,3,4,5,6"
100
+ end
101
+
102
+ specify "Insufficient params when params are infinate" do
103
+ @sm.add(:off, :set, :on, Proc.new { |a, *b| @status = a.to_s + ":" + b.join(",") } )
104
+ @sm.set(1, 2, 3)
105
+ @status.should_equal "1:2,3"
106
+
107
+ @sm.state = :off
108
+ begin
109
+ @sm.set
110
+ rescue StateMachine::StateMachineException => e
111
+ e.message.should_equal "Insufficient parameters. (transition action from 'off' state invoked by 'set' event)"
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,49 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ context "State Machine Entry and Exit Actions" do
4
+
5
+ setup do
6
+ @log = []
7
+ @sm = StateMachine::StateMachine.new
8
+ @sm.add(:off, :toggle, :on, Proc.new { @log << "on" } )
9
+ @sm.add(:on, :toggle, :off, Proc.new { @log << "off" } )
10
+ @sm.run
11
+ end
12
+
13
+ specify "entry action" do
14
+ @sm[:on].on_entry Proc.new { @log << "entered_on" }
15
+
16
+ @sm.toggle
17
+
18
+ @log.join(",").should_equal "on,entered_on"
19
+ end
20
+
21
+ specify "exit action" do
22
+ @sm[:off].on_exit Proc.new { @log << "exited_off" }
23
+
24
+ @sm.toggle
25
+
26
+ @log.join(",").should_equal "exited_off,on"
27
+ end
28
+
29
+ specify "exit and entry" do
30
+ @sm[:off].on_exit Proc.new { @log << "exited_off" }
31
+ @sm[:on].on_entry Proc.new { @log << "entered_on" }
32
+
33
+ @sm.toggle
34
+
35
+ @log.join(",").should_equal "exited_off,on,entered_on"
36
+ end
37
+
38
+ specify "entry and exit actions may be parameterized" do
39
+ @sm[:off].on_exit Proc.new { |a| @log << "exited_off(#{a})" }
40
+ @sm[:on].on_entry Proc.new { |a, b| @log << "entered_on(#{a},#{b})" }
41
+
42
+ @sm.toggle "one", "two"
43
+
44
+ @log.join(",").should_equal "exited_off(one),on,entered_on(one,two)"
45
+ end
46
+
47
+
48
+
49
+ end
@@ -0,0 +1,27 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ context "State Machine Odds And Ends" do
4
+ include SwitchStateMachine
5
+
6
+ setup do
7
+ create_switch
8
+ @sm.run
9
+ end
10
+
11
+ specify "method missing delegates to super in case of no event" do
12
+ lambda { @sm.blah }.should_raise NoMethodError
13
+ end
14
+
15
+ specify "set state with string" do
16
+ @sm.state.id.should_be :off
17
+ @sm.state = "on"
18
+ @sm.state.id.should_be :on
19
+ end
20
+
21
+ specify "set state with symbol" do
22
+ @sm.state.id.should_be :off
23
+ @sm.state = :on
24
+ @sm.state.id.should_be :on
25
+ end
26
+
27
+ end
@@ -0,0 +1,69 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ context "simple cases:" do
4
+ setup do
5
+ @sm = StateMachine::StateMachine.new
6
+ @count = 0
7
+ @proc = Proc.new {@count = @count + 1}
8
+ end
9
+
10
+ specify "one transition has states" do
11
+ @sm.add(:on, :flip, :off, @proc)
12
+
13
+ @sm.states.length.should_be 2
14
+ @sm[:on].should_not_be nil
15
+ @sm[:off].should_not_be nil
16
+ end
17
+
18
+ specify "one trasition create connects states with transition" do
19
+ @sm.add(:on, :flip, :off, @proc)
20
+ origin = @sm[:on]
21
+ destination = @sm[:off]
22
+
23
+ origin.transitions.length.should_be 1
24
+ destination.transitions.length.should_be 0
25
+ transition = origin[:flip]
26
+ check_transition(transition, :on, :off, :flip, @proc)
27
+ end
28
+
29
+ specify "end state" do
30
+ @sm.add(:start, :blah, nil, @proc)
31
+
32
+ @sm.run
33
+ @sm.running.should.be true
34
+ @sm.process_event(:blah)
35
+ @sm.state.should.be nil
36
+ @sm.running.should.be false;
37
+ end
38
+
39
+ specify "reset" do
40
+ @sm.add(:start, :blah, nil, @proc)
41
+ @sm.run
42
+ @sm.process_event(:blah)
43
+
44
+ @sm.reset
45
+
46
+ @sm.state.should.be @sm[:start]
47
+ @sm.running.should.be true
48
+ end
49
+
50
+ specify "exception when state machine is not running" do
51
+ @sm.add(:on, :flip, :off)
52
+
53
+ begin
54
+ @sm.process_event(:flip)
55
+ rescue StateMachine::StateMachineException => e
56
+ e.message.should_equal "The state machine isn't in any state. Did you forget to call run?"
57
+ end
58
+ end
59
+
60
+ specify "no proc in transition" do
61
+ @sm.add(:on, :flip, :off)
62
+ @sm.run
63
+
64
+ @sm.flip
65
+ end
66
+
67
+
68
+
69
+ end
@@ -0,0 +1,80 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ context "Turn Stile" do
4
+ include TurnstileStateMachine
5
+
6
+ setup do
7
+ create_turnstile
8
+
9
+ @out_out_order = false
10
+ @sm.add(:operative, :maintain, :maintenance, Proc.new { @out_of_order = true } )
11
+ @sm.add(:maintenance, :operate, :operative, Proc.new { @out_of_order = false } )
12
+ @sm[:operative].add_substates(:locked, :unlocked)
13
+
14
+ @sm.run
15
+ end
16
+
17
+ specify "substates respond to superstate transitions" do
18
+ @sm.process_event(:maintain)
19
+ @sm.state.id.should_be :maintenance
20
+ @locked.should_be true
21
+ @out_of_order.should_be true
22
+ end
23
+
24
+ specify "after transitions, substates respond to superstate transitions" do
25
+ @sm.coin
26
+ @sm.maintain
27
+ @sm.state.id.should_be :maintenance
28
+ @locked.should_be false
29
+ @out_of_order.should_be true
30
+ end
31
+
32
+ specify "transitions back to superstate go to history state" do
33
+ @sm[:operative].use_history
34
+ @sm.maintain
35
+ @sm.operate
36
+ @sm.state.id.should_be :locked
37
+ @out_of_order.should_be false
38
+
39
+ @sm.coin
40
+ @sm.maintain
41
+ @sm.operate
42
+ @sm.state.id.should_be :unlocked
43
+ end
44
+
45
+ specify "missing substates are added" do
46
+ @sm[:operative].add_substates(:blah)
47
+ @sm[:blah].should_not_be nil
48
+ @sm[:blah].superstate.id.should_be :operative
49
+ end
50
+
51
+ specify "recursive superstates not allowed" do
52
+ begin
53
+ @sm[:operative].add_substates(:operative)
54
+ self.should_fail_with_message("exception expected")
55
+ rescue StateMachine::StateMachineException => e
56
+ e.message.should_equal "Cyclic substates not allowed. (operative)"
57
+ end
58
+ end
59
+
60
+ specify "recursive superstates (2 levels) not allowed" do
61
+ begin
62
+ @sm[:operative].add_substates(:blah)
63
+ @sm[:blah].add_substates(:operative)
64
+ self.should_fail_with_message("exception expected")
65
+ rescue StateMachine::StateMachineException => e
66
+ e.message.should_equal "Cyclic substates not allowed. (blah)"
67
+ end
68
+ end
69
+
70
+ specify "exception when add_substates called without args" do
71
+ begin
72
+ @sm[:locked].add_substates()
73
+ self.should_fail_with_message("exception expected")
74
+ rescue StateMachine::StateMachineException => e
75
+ e.message.should_equal "At least one parameter is required for add_substates."
76
+ end
77
+ end
78
+
79
+
80
+ end
@@ -0,0 +1,78 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ context "Turn Stile" do
4
+ include TurnstileStateMachine
5
+
6
+ setup do
7
+ create_turnstile
8
+ @sm.run
9
+ end
10
+
11
+ specify "connections" do
12
+ @sm.states.length.should_be 2
13
+ locked_state = @sm[:locked]
14
+ unlocked_state = @sm[:unlocked]
15
+
16
+ locked_state.transitions.length.should_be 2
17
+ unlocked_state.transitions.length.should_be 2
18
+
19
+ check_transition(locked_state[:coin], :locked, :unlocked, :coin, @unlock)
20
+ check_transition(locked_state[:pass], :locked, :locked, :pass, @alarm)
21
+ check_transition(unlocked_state[:pass], :unlocked, :locked, :pass, @lock)
22
+ check_transition(unlocked_state[:coin], :unlocked, :locked, :coin, @thankyou)
23
+ end
24
+
25
+ specify "start state" do
26
+ @sm.run
27
+ @sm.start_state.should.be @sm[:locked]
28
+ @sm.state.should.be @sm[:locked]
29
+ end
30
+
31
+ specify "bad event" do
32
+ begin
33
+ @sm.process_event(:blah)
34
+ self.should.fail_with_message("Exception expected")
35
+ rescue Exception => e
36
+ e.class.should.be StateMachine::MissingTransitionException
37
+ e.to_s.should_equal "'locked' state does not respond to the 'blah' event."
38
+ end
39
+ end
40
+
41
+ specify "locked state with a coin" do
42
+ @sm.process_event(:coin)
43
+
44
+ @sm.state.should.be @sm[:unlocked]
45
+ @locked.should.be false
46
+ end
47
+
48
+ specify "locked state with pass event" do
49
+ @sm.process_event(:pass)
50
+
51
+ @sm.state.should.be @sm[:locked]
52
+ @locked.should.be true
53
+ @alarm.should.be true
54
+ end
55
+
56
+ specify "unlocked state with coin" do
57
+ @sm.process_event(:coin)
58
+ @sm.process_event(:coin)
59
+
60
+ @sm.state.should.be @sm[:locked]
61
+ @thankyou_status.should.be true
62
+ end
63
+
64
+ specify "unlocked state with pass event" do
65
+ @sm.process_event(:coin)
66
+ @sm.process_event(:pass)
67
+
68
+ @sm.state.should.be @sm[:locked]
69
+ @locked.should.be true
70
+ end
71
+
72
+ specify "events invoked via method_missing" do
73
+ @sm.coin
74
+ @sm.state.should.be @sm[:unlocked]
75
+ @sm.pass
76
+ @sm.state.should.be @sm[:locked]
77
+ end
78
+ end
@@ -0,0 +1,40 @@
1
+ require File.dirname(__FILE__) + '/../lib/state_machine'
2
+
3
+ def check_transition(transition, origin_id, destination_id, event, action)
4
+ transition.should_not_be nil
5
+ transition.event.should_be event
6
+ transition.action.should_be action
7
+ transition.origin.id.should_be origin_id
8
+ transition.destination.id.should_be destination_id
9
+ end
10
+
11
+ module SwitchStateMachine
12
+
13
+ def create_switch
14
+ @status = "off"
15
+ @sm = StateMachine::StateMachine.new
16
+ @sm.add(:off, :toggle, :on, Proc.new { @status = "on" } )
17
+ @sm.add(:on, :toggle, :off, Proc.new { @status = "off" } )
18
+ end
19
+
20
+ end
21
+
22
+ module TurnstileStateMachine
23
+
24
+ def create_turnstile
25
+ @locked = true
26
+ @alarm_status = false
27
+ @thankyou_status = false
28
+ @lock = Proc.new { @locked = true }
29
+ @unlock = Proc.new { @locked = false }
30
+ @alarm = Proc.new { @alarm_status = true }
31
+ @thankyou = Proc.new { @thankyou_status = true }
32
+
33
+ @sm = StateMachine::StateMachine.new
34
+ @sm.add(:locked, :coin, :unlocked, @unlock)
35
+ @sm.add(:unlocked, :pass, :locked, @lock)
36
+ @sm.add(:locked, :pass, :locked, @alarm)
37
+ @sm.add(:unlocked, :coin, :locked, @thankyou)
38
+ end
39
+
40
+ end
@@ -0,0 +1,97 @@
1
+ require File.dirname(__FILE__) + '/../lib/state_machine'
2
+
3
+ context "Transition Calculating Exits and Entries" do
4
+
5
+ setup do
6
+ @a = StateMachine::State.new("a", nil)
7
+ @b = StateMachine::State.new("b", nil)
8
+ @c = StateMachine::State.new("c", nil)
9
+ @d = StateMachine::State.new("d", nil)
10
+ @e = StateMachine::State.new("e", nil)
11
+ end
12
+
13
+ specify "to nil" do
14
+ transition = StateMachine::Transition.new(@a, nil, nil, nil)
15
+ exits, entries = transition.exits_and_entries(@a)
16
+ exits.to_s.should_equal [@a].to_s
17
+ entries.to_s.should_equal [].to_s
18
+ entries.length.should_be 0
19
+ end
20
+
21
+ specify "to itself" do
22
+ transition = StateMachine::Transition.new(@a, @a, nil, nil)
23
+ exits, entries = transition.exits_and_entries(@a)
24
+ exits.to_s.should_equal [@a].to_s
25
+ entries.to_s.should_equal [@a].to_s
26
+ end
27
+
28
+ specify "to friend" do
29
+ transition = StateMachine::Transition.new(@a, @b, nil, nil)
30
+ exits, entries = transition.exits_and_entries(@a)
31
+ exits.to_s.should_equal [@a].to_s
32
+ entries.to_s.should_equal [@b].to_s
33
+ end
34
+
35
+ specify "to parent" do
36
+ @a.superstate = @b
37
+ transition = StateMachine::Transition.new(@a, @b, nil, nil)
38
+ exits, entries = transition.exits_and_entries(@a)
39
+ exits.to_s.should_equal [@a, @b].to_s
40
+ entries.to_s.should_equal [@b].to_s
41
+ end
42
+
43
+ specify "to uncle" do
44
+ @a.superstate = @b
45
+ transition = StateMachine::Transition.new(@a, @c, nil, nil)
46
+ exits, entries = transition.exits_and_entries(@a)
47
+ exits.to_s.should_equal [@a, @b].to_s
48
+ entries.to_s.should_equal [@c].to_s
49
+ end
50
+
51
+ specify "to cousin" do
52
+ @a.superstate = @b
53
+ @c.superstate = @d
54
+ transition = StateMachine::Transition.new(@a, @c, nil, nil)
55
+ exits, entries = transition.exits_and_entries(@a)
56
+ exits.to_s.should_equal [@a, @b].to_s
57
+ entries.to_s.should_equal [@d, @c].to_s
58
+ end
59
+
60
+ specify "to nephew" do
61
+ @a.superstate = @b
62
+ transition = StateMachine::Transition.new(@c, @a, nil, nil)
63
+ exits, entries = transition.exits_and_entries(@c)
64
+ exits.to_s.should_equal [@c].to_s
65
+ entries.to_s.should_equal [@b,@a].to_s
66
+ end
67
+
68
+ specify "to sister" do
69
+ @a.superstate = @c
70
+ @b.superstate = @c
71
+ transition = StateMachine::Transition.new(@a, @b, nil, nil)
72
+ exits, entries = transition.exits_and_entries(@a)
73
+ exits.to_s.should_equal [@a].to_s
74
+ entries.to_s.should_equal [@b].to_s
75
+ end
76
+
77
+ specify "to second cousin" do
78
+ @a.superstate = @b
79
+ @b.superstate = @c
80
+ @d.superstate = @e
81
+ @e.superstate = @c
82
+ transition = StateMachine::Transition.new(@a, @d, nil, nil)
83
+ exits, entries = transition.exits_and_entries(@a)
84
+ exits.to_s.should_equal [@a, @b].to_s
85
+ entries.to_s.should_equal [@e, @d].to_s
86
+ end
87
+
88
+ specify "to grandparent" do
89
+ @a.superstate = @b
90
+ @b.superstate = @c
91
+ transition = StateMachine::Transition.new(@a, @c, nil, nil)
92
+ exits, entries = transition.exits_and_entries(@a)
93
+ exits.to_s.should_equal [@a, @b, @c].to_s
94
+ entries.to_s.should_equal [@c].to_s
95
+ end
96
+
97
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: statemachine
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.0.0
7
+ date: 2006-10-25 00:00:00 -05:00
8
+ summary: RSpec-0.6.4 - BDD for Ruby http://rspec.rubyforge.org/
9
+ require_paths:
10
+ - lib
11
+ email: statemachine-devel@rubyforge.org
12
+ homepage: http://statemachine.rubyforge.org
13
+ rubyforge_project: statemachine
14
+ description: StateMachine is a ruby library for building Finite State Machines (FSM), also known as Finite State Automata (FSA).
15
+ autorequire: statemachine
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: false
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - - Micah Martin
30
+ files:
31
+ - CHANGES
32
+ - Rakefile
33
+ - lib/proc_calling.rb
34
+ - lib/state.rb
35
+ - lib/state_machine.rb
36
+ - lib/statemachine.rb
37
+ - lib/super_state.rb
38
+ - lib/transition.rb
39
+ - spec/sm_action_parameterization_spec.rb
40
+ - spec/sm_entry_exit_actions_spec.rb
41
+ - spec/sm_odds_n_ends_spec.rb
42
+ - spec/sm_simple_spec.rb
43
+ - spec/sm_super_state_spec.rb
44
+ - spec/sm_turnstile_spec.rb
45
+ - spec/spec_helper.rb
46
+ - spec/transition_spec.rb
47
+ test_files:
48
+ - spec/sm_action_parameterization_spec.rb
49
+ - spec/sm_entry_exit_actions_spec.rb
50
+ - spec/sm_odds_n_ends_spec.rb
51
+ - spec/sm_simple_spec.rb
52
+ - spec/sm_super_state_spec.rb
53
+ - spec/sm_turnstile_spec.rb
54
+ - spec/transition_spec.rb
55
+ rdoc_options: []
56
+
57
+ extra_rdoc_files: []
58
+
59
+ executables: []
60
+
61
+ extensions: []
62
+
63
+ requirements: []
64
+
65
+ dependencies: []
66
+