statemachine 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+