hifsm 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/.travis.yml +7 -0
- data/README.md +60 -28
- data/hifsm.gemspec +3 -0
- data/lib/hifsm/adapters/active_record_adapter.rb +34 -0
- data/lib/hifsm/fsm.rb +46 -4
- data/lib/hifsm/machine.rb +6 -7
- data/lib/hifsm/state.rb +4 -2
- data/lib/hifsm/version.rb +1 -1
- data/lib/hifsm.rb +11 -0
- data/test/monster.rb +31 -27
- data/test/setup_tests.rb +4 -0
- data/test/test_activerecord_adapter.rb +80 -0
- data/test/test_basic_fsm.rb +7 -0
- data/test/test_dynamic_initial_state.rb +51 -0
- data/test/test_event_guard.rb +20 -12
- data/test/test_ifless_factorial.rb +8 -11
- data/test/test_two_machines.rb +41 -0
- metadata +52 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2dd540fa33580b8e82c96d82a1746aa5016ab3c6
|
4
|
+
data.tar.gz: ab0492f94873e14a2f75d1e3fd32ae764ed652c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 258cd2e2f5a18942a0ee40a96779246ba0b99adc4659834460ac482f96882cef9c2d5ca4f7e49301df4cb759ea12f4969a8f2aeb00554af5f092664c419b089e
|
7
|
+
data.tar.gz: 56cf2f726e53af3fdfa98067030448cbcd86a9306937e30d3ab75ab0c35bfc24246371df1fb1b7e202c7ccf54a443762bb1e126582d2462f36c5139438a244f7
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# Hierarchical Finite State Machine in Ruby
|
2
2
|
|
3
|
+
[](https://travis-ci.org/stiff/hifsm)
|
4
|
+
[](https://coveralls.io/r/stiff/hifsm?branch=master)
|
5
|
+
|
3
6
|
This library was created from the desire to have nested states inspired by [rFSM](https://github.com/kmarkus/rFSM).
|
4
7
|
|
5
8
|
It can be used in plain old ruby objects, but works well with `ActiveRecord`s too.
|
@@ -39,7 +42,7 @@ Here is how to use it to model a monster in a Quake-like game. It covers most Hi
|
|
39
42
|
require 'hifsm'
|
40
43
|
|
41
44
|
class Monster
|
42
|
-
|
45
|
+
include Hifsm.fsm_module {
|
43
46
|
state :idle, :initial => true
|
44
47
|
state :attacking do
|
45
48
|
state :acquiring_target, :initial => true do
|
@@ -89,22 +92,20 @@ class Monster
|
|
89
92
|
self.target = nil
|
90
93
|
end
|
91
94
|
end
|
92
|
-
|
95
|
+
}
|
93
96
|
|
94
97
|
attr_accessor :target, :low_hp, :debug
|
95
|
-
attr_reader :state
|
96
98
|
|
97
99
|
def initialize
|
98
100
|
@debug = false
|
99
101
|
@home = 'home'
|
100
|
-
@state = @@fsm.instantiate(self) # or @@fsm.instantiate(self, 'attacking.pursuing')
|
101
102
|
@tick = 1
|
102
103
|
@low_hp = false
|
103
104
|
end
|
104
105
|
|
105
106
|
def act!
|
106
|
-
debug && puts("Acting @#{
|
107
|
-
|
107
|
+
debug && puts("Acting @#{state}")
|
108
|
+
state_machine.act! @tick
|
108
109
|
@tick = @tick + 1
|
109
110
|
end
|
110
111
|
|
@@ -132,35 +133,43 @@ class Monster
|
|
132
133
|
end
|
133
134
|
|
134
135
|
ogre = Monster.new
|
135
|
-
ogre.debug = true
|
136
|
-
ogre.act! #
|
137
|
-
ogre.sight 'player' #
|
138
|
-
ogre.act! #
|
139
|
-
#
|
140
|
-
#
|
136
|
+
ogre.debug = true ### Console output:
|
137
|
+
ogre.act! # Acting @idle
|
138
|
+
ogre.sight 'player' # Setting target to player
|
139
|
+
ogre.act! # Acting @attacking.acquiring_target
|
140
|
+
# 2: Attack! <- parent state act! first
|
141
|
+
# planning...
|
142
|
+
# AARGHH!
|
141
143
|
# ogre.acquire -> Hifsm::MissingTransition, already acquired in act!
|
142
|
-
ogre.act! #
|
143
|
-
#
|
144
|
-
|
145
|
-
ogre.
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
ogre.
|
150
|
-
|
144
|
+
ogre.act! # Acting @attacking.pursuing
|
145
|
+
# 3: Attack!
|
146
|
+
# step step player
|
147
|
+
ogre.enemy_dead # Woohoo!
|
148
|
+
ogre.act! # Acting @coming_back
|
149
|
+
# step step home
|
150
|
+
|
151
|
+
ogre.sight 'player2' # Setting target to player2
|
152
|
+
ogre.acquire # AARGHH!
|
153
|
+
ogre.act! # Acting @attacking.pursuing
|
154
|
+
# 5: Attack!
|
155
|
+
# step step player2
|
151
156
|
ogre.reached
|
152
|
-
puts ogre.state #
|
153
|
-
ogre.act! #
|
154
|
-
|
155
|
-
|
156
|
-
ogre.act!
|
157
|
-
|
157
|
+
puts ogre.state # attacking.fighting
|
158
|
+
ogre.act! # Acting @attacking.fighting
|
159
|
+
# 6: Attack!
|
160
|
+
# ~~> player2
|
161
|
+
5.times { ogre.act! } # ...
|
162
|
+
ogre.enemy_dead # Woohoo!
|
163
|
+
ogre.act! # Acting @coming_back
|
164
|
+
# step step home
|
158
165
|
ogre.low_hp = true
|
159
166
|
ogre.sight 'player3'
|
160
|
-
ogre.act! #
|
167
|
+
ogre.act! # Acting @runaway
|
161
168
|
|
162
169
|
```
|
163
170
|
|
171
|
+
Note the use of `{..}` construct instead of `do..end` in `include`. `do..end` is treated as block for include itself, instead of `fsm_module`.
|
172
|
+
|
164
173
|
## Guards
|
165
174
|
|
166
175
|
Events are tried in order they were defined, if guard callback returns `false` then event is skipped.
|
@@ -181,6 +190,29 @@ If any of `before...` callbacks returns `false` then no further processing is do
|
|
181
190
|
|
182
191
|
On `act!` just calls action block if it was given.
|
183
192
|
|
193
|
+
## ActiveRecord integration
|
194
|
+
|
195
|
+
Add column to your database which would hold the state, and then:
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
class Order < ActiveRecord::Base
|
199
|
+
hifsm do
|
200
|
+
state :draft, :initial => true
|
201
|
+
state :processing do
|
202
|
+
state :packaging, :initial => true
|
203
|
+
state :delivering
|
204
|
+
end
|
205
|
+
state :done
|
206
|
+
state :cancelled
|
207
|
+
|
208
|
+
event :start_processing, :from => :draft, :to => :processing
|
209
|
+
event :cancel, :to => :cancelled
|
210
|
+
end
|
211
|
+
end
|
212
|
+
Order.new # draft
|
213
|
+
|
214
|
+
```
|
215
|
+
|
184
216
|
## Testing
|
185
217
|
|
186
218
|
Only 'public' API is unit-tested, internal implementation may be freely changed, so don't rely on it.
|
data/hifsm.gemspec
CHANGED
@@ -21,4 +21,7 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.add_development_dependency "bundler", "~> 1.3"
|
22
22
|
spec.add_development_dependency "rake"
|
23
23
|
spec.add_development_dependency "minitest"
|
24
|
+
spec.add_development_dependency "coveralls"
|
25
|
+
spec.add_development_dependency "activerecord"
|
26
|
+
spec.add_development_dependency "sqlite3"
|
24
27
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Hifsm
|
2
|
+
module Adapters
|
3
|
+
module ActiveRecordAdapter
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
extend ClassMethods
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def hifsm(column, &block)
|
12
|
+
include Hifsm.fsm_module(column, &block)
|
13
|
+
before_save "hifsm_write_#{column}_attribute"
|
14
|
+
|
15
|
+
define_method "#{column}=" do |value|
|
16
|
+
raise 'not (sure will be) implemented'
|
17
|
+
end
|
18
|
+
|
19
|
+
define_method "initial_#{column}" do
|
20
|
+
read_attribute(column)
|
21
|
+
end
|
22
|
+
|
23
|
+
define_method "hifsm_write_#{column}_attribute" do
|
24
|
+
write_attribute(column, send(column))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
ActiveRecord::Base.class_eval do
|
33
|
+
include Hifsm::Adapters::ActiveRecordAdapter
|
34
|
+
end
|
data/lib/hifsm/fsm.rb
CHANGED
@@ -1,17 +1,26 @@
|
|
1
1
|
module Hifsm
|
2
|
+
|
3
|
+
# This class holds immutable state machine definition
|
2
4
|
class FSM
|
3
|
-
attr_reader :states, :transitions
|
5
|
+
attr_reader :name, :states, :transitions
|
4
6
|
|
5
|
-
def initialize(parent = nil, &block)
|
7
|
+
def initialize(name = :state, parent = nil, &block)
|
8
|
+
@name = name
|
6
9
|
@parent = parent
|
7
10
|
@states = {}
|
8
11
|
@initial_state = nil
|
9
12
|
|
10
13
|
instance_eval(&block) if block
|
14
|
+
|
15
|
+
@fsm_module = fsm_module = initialize_module
|
16
|
+
@machine_class = Class.new(Hifsm::Machine) do
|
17
|
+
include fsm_module
|
18
|
+
define_method("#{name}_machine") { self }
|
19
|
+
end
|
11
20
|
end
|
12
21
|
|
13
22
|
def instantiate(target = nil, initial_state = nil)
|
14
|
-
|
23
|
+
@machine_class.new(self, target, initial_state)
|
15
24
|
end
|
16
25
|
|
17
26
|
def all_events
|
@@ -44,15 +53,48 @@ module Hifsm
|
|
44
53
|
end
|
45
54
|
|
46
55
|
def state(name, options = {}, &block)
|
47
|
-
st = @states[name.to_s] = Hifsm::State.new(name, @parent)
|
56
|
+
st = @states[name.to_s] = Hifsm::State.new(self, name, @parent)
|
48
57
|
@initial_state = st if options[:initial]
|
49
58
|
st.instance_eval(&block) if block
|
50
59
|
end
|
51
60
|
|
61
|
+
def to_module
|
62
|
+
@fsm_module
|
63
|
+
end
|
64
|
+
|
52
65
|
private
|
53
66
|
# like in ActiveSupport
|
54
67
|
def array_wrap(anything)
|
55
68
|
anything.is_a?(Array) ? anything : [anything].compact
|
56
69
|
end
|
70
|
+
|
71
|
+
def initialize_module
|
72
|
+
fsm = self # capture self
|
73
|
+
machine_var = "@#{name}_machine"
|
74
|
+
machine_name = "#{name}_machine"
|
75
|
+
|
76
|
+
Module.new.module_exec do
|
77
|
+
|
78
|
+
# <state>_machine returns machine instance
|
79
|
+
define_method(machine_name) do
|
80
|
+
if instance_variable_defined?(machine_var)
|
81
|
+
instance_variable_get(machine_var)
|
82
|
+
else
|
83
|
+
machine = fsm.instantiate(self)
|
84
|
+
instance_variable_set(machine_var, machine)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# <state> returns string representation of the current state
|
89
|
+
define_method(fsm.name) { send(machine_name).to_s }
|
90
|
+
|
91
|
+
# <event> fires event
|
92
|
+
fsm.all_events.each do |event_name, event|
|
93
|
+
define_method(event_name) {|*args| send(machine_name).fire(event_name, *args) }
|
94
|
+
end
|
95
|
+
|
96
|
+
self # return module
|
97
|
+
end
|
98
|
+
end
|
57
99
|
end
|
58
100
|
end
|
data/lib/hifsm/machine.rb
CHANGED
@@ -1,17 +1,16 @@
|
|
1
1
|
module Hifsm
|
2
|
+
# This is just a storage of current state
|
2
3
|
class Machine
|
3
4
|
def initialize(fsm, target, initial_state = nil)
|
4
5
|
@target = target || self
|
5
6
|
@fsm = fsm
|
6
7
|
|
7
|
-
|
8
|
+
initial_state_method_name = "initial_#{fsm.name}"
|
9
|
+
initial_state ||= target.send(initial_state_method_name) if target.respond_to?(initial_state_method_name)
|
10
|
+
initial_state &&= fsm.get_state!(initial_state)
|
11
|
+
initial_state ||= fsm.initial_state!
|
8
12
|
|
9
|
-
|
10
|
-
fsm.all_events.each do |event_name, event|
|
11
|
-
@target.singleton_class.instance_exec do
|
12
|
-
define_method(event_name) {|*args| mach.fire(event_name, *args) }
|
13
|
-
end
|
14
|
-
end
|
13
|
+
@state = initial_state.enter!
|
15
14
|
end
|
16
15
|
|
17
16
|
def act!(*args)
|
data/lib/hifsm/state.rb
CHANGED
@@ -2,7 +2,8 @@ module Hifsm
|
|
2
2
|
class State
|
3
3
|
CALLBACKS = [:before_enter, :before_exit, :after_enter, :after_exit].freeze
|
4
4
|
|
5
|
-
def initialize(name, parent = nil)
|
5
|
+
def initialize(fsm, name, parent = nil)
|
6
|
+
@fsm = fsm
|
6
7
|
@name = name
|
7
8
|
@parent = parent
|
8
9
|
@action = nil
|
@@ -92,7 +93,8 @@ module Hifsm
|
|
92
93
|
|
93
94
|
private
|
94
95
|
def sub_fsm!
|
95
|
-
|
96
|
+
# FIXME too much coupling
|
97
|
+
@sub_fsm ||= Hifsm::FSM.new(@fsm.name, self)
|
96
98
|
end
|
97
99
|
end
|
98
100
|
end
|
data/lib/hifsm/version.rb
CHANGED
data/lib/hifsm.rb
CHANGED
@@ -18,4 +18,15 @@ module Hifsm
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
+
class <<self
|
22
|
+
def fsm_module(name = :state, &block)
|
23
|
+
FSM::new(name, &block).to_module
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'active_record'
|
30
|
+
require 'hifsm/adapters/active_record_adapter'
|
31
|
+
rescue LoadError
|
21
32
|
end
|
data/test/monster.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'hifsm'
|
2
2
|
|
3
3
|
class Monster
|
4
|
-
|
4
|
+
include Hifsm.fsm_module {
|
5
5
|
state :idle, :initial => true
|
6
6
|
state :attacking do
|
7
7
|
state :acquiring_target, :initial => true do
|
@@ -51,22 +51,20 @@ class Monster
|
|
51
51
|
self.target = nil
|
52
52
|
end
|
53
53
|
end
|
54
|
-
|
54
|
+
}
|
55
55
|
|
56
56
|
attr_accessor :target, :low_hp, :debug
|
57
|
-
attr_reader :state
|
58
57
|
|
59
58
|
def initialize
|
60
59
|
@debug = false
|
61
60
|
@home = 'home'
|
62
|
-
@state = @@fsm.instantiate(self) # or @@fsm.instantiate(self, 'attacking.pursuing')
|
63
61
|
@tick = 1
|
64
62
|
@low_hp = false
|
65
63
|
end
|
66
64
|
|
67
65
|
def act!
|
68
|
-
debug && puts("Acting @#{
|
69
|
-
|
66
|
+
debug && puts("Acting @#{state}")
|
67
|
+
state_machine.act! @tick
|
70
68
|
@tick = @tick + 1
|
71
69
|
end
|
72
70
|
|
@@ -95,30 +93,36 @@ end
|
|
95
93
|
|
96
94
|
if $0 == __FILE__
|
97
95
|
ogre = Monster.new
|
98
|
-
ogre.debug = true
|
99
|
-
ogre.act! #
|
100
|
-
ogre.sight 'player' #
|
101
|
-
ogre.act! #
|
102
|
-
#
|
103
|
-
#
|
96
|
+
ogre.debug = true ### Console output:
|
97
|
+
ogre.act! # Acting @idle
|
98
|
+
ogre.sight 'player' # Setting target to player
|
99
|
+
ogre.act! # Acting @attacking.acquiring_target
|
100
|
+
# 2: Attack! <- parent state act! first
|
101
|
+
# planning...
|
102
|
+
# AARGHH!
|
104
103
|
# ogre.acquire -> Hifsm::MissingTransition, already acquired in act!
|
105
|
-
ogre.act! #
|
106
|
-
#
|
107
|
-
|
108
|
-
ogre.
|
104
|
+
ogre.act! # Acting @attacking.pursuing
|
105
|
+
# 3: Attack!
|
106
|
+
# step step player
|
107
|
+
ogre.enemy_dead # Woohoo!
|
108
|
+
ogre.act! # Acting @coming_back
|
109
|
+
# step step home
|
109
110
|
|
110
|
-
ogre.sight 'player2' #
|
111
|
-
ogre.acquire #
|
112
|
-
ogre.act! #
|
113
|
-
#
|
111
|
+
ogre.sight 'player2' # Setting target to player2
|
112
|
+
ogre.acquire # AARGHH!
|
113
|
+
ogre.act! # Acting @attacking.pursuing
|
114
|
+
# 5: Attack!
|
115
|
+
# step step player2
|
114
116
|
ogre.reached
|
115
|
-
puts ogre.state #
|
116
|
-
ogre.act! #
|
117
|
-
|
118
|
-
|
119
|
-
ogre.act!
|
120
|
-
|
117
|
+
puts ogre.state # attacking.fighting
|
118
|
+
ogre.act! # Acting @attacking.fighting
|
119
|
+
# 6: Attack!
|
120
|
+
# ~~> player2
|
121
|
+
5.times { ogre.act! } # ...
|
122
|
+
ogre.enemy_dead # Woohoo!
|
123
|
+
ogre.act! # Acting @coming_back
|
124
|
+
# step step home
|
121
125
|
ogre.low_hp = true
|
122
126
|
ogre.sight 'player3'
|
123
|
-
ogre.act! #
|
127
|
+
ogre.act! # Acting @runaway
|
124
128
|
end
|
data/test/setup_tests.rb
CHANGED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'setup_tests'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
class TestActiverecrodAdapter < Minitest::Test
|
5
|
+
class SodaMachine < ActiveRecord::Base
|
6
|
+
hifsm :state do
|
7
|
+
state :off, :initial => true
|
8
|
+
state :on do
|
9
|
+
state :idle, :initial => true
|
10
|
+
state :accepting_cash
|
11
|
+
state :cooking
|
12
|
+
state :ready
|
13
|
+
|
14
|
+
event :done, :from => :accepting_cash, :to => :cooking
|
15
|
+
event :done, :from => :cooking, :to => :ready
|
16
|
+
event :done, :from => :ready, :to => :idle do
|
17
|
+
after { self.counter += 1 }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
state :broken
|
21
|
+
|
22
|
+
event :toggle_power, :from => :off, :to => :on
|
23
|
+
event :toggle_power, :from => :on, :to => :off
|
24
|
+
event :break, :to => :broken
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def setup
|
29
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
30
|
+
ActiveRecord::Base.connection.create_table :soda_machines do |t|
|
31
|
+
t.column :address, :string
|
32
|
+
t.column :state, :string
|
33
|
+
t.column :counter, :integer, :null => false, :default => 0
|
34
|
+
end
|
35
|
+
|
36
|
+
insert_record 'South Park', 'on.idle'
|
37
|
+
insert_record 'Springfield', 'broken'
|
38
|
+
end
|
39
|
+
|
40
|
+
def teardown
|
41
|
+
ActiveRecord::Base.connection.disconnect!
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_hifsm_installed
|
45
|
+
@machine = SodaMachine.create
|
46
|
+
assert @machine.state_machine.is_a?(Hifsm::Machine), ".state should be Hifsm::Mahine"
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_new_machines_saved_in_initial_state
|
50
|
+
@machine = SodaMachine.create
|
51
|
+
assert_equal 'off', @machine.state_machine.state
|
52
|
+
assert_equal 'off', @machine.state
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_state_fetched_from_db
|
56
|
+
@machine = SodaMachine.where(:address => 'South Park').first
|
57
|
+
assert_equal 'on.idle', @machine.state.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_events_defined_on_record
|
61
|
+
@machine = SodaMachine.first
|
62
|
+
@machine.toggle_power.save
|
63
|
+
pass # assert_nothing_raised
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_state_saved_in_db
|
67
|
+
@machine = SodaMachine.where(:address => 'South Park').first
|
68
|
+
@machine.toggle_power.save
|
69
|
+
assert_equal 'off', SodaMachine.where(:id => @machine.id).pluck(:state).first
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
def insert_record(address, state)
|
74
|
+
insert_manager = Arel::InsertManager.new(SodaMachine)
|
75
|
+
insert_manager.insert([[SodaMachine.arel_table[:address], address], [SodaMachine.arel_table[:state], state]])
|
76
|
+
ActiveRecord::Base.connection.insert insert_manager
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
data/test/test_basic_fsm.rb
CHANGED
@@ -26,4 +26,11 @@ class TestBasicFSM < Minitest::Test
|
|
26
26
|
@machine.toggle
|
27
27
|
assert_equal 'off', @machine.state
|
28
28
|
end
|
29
|
+
|
30
|
+
def test_instantiating_maching_in_unknown_state_raises_error
|
31
|
+
assert_raises(Hifsm::MissingState) do
|
32
|
+
@fsm.instantiate(nil, 'on.no')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
29
36
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'setup_tests'
|
2
|
+
|
3
|
+
class TestDynamicInitialState < Minitest::Test
|
4
|
+
class Value < Struct.new(:value)
|
5
|
+
include Hifsm.fsm_module(:group) {
|
6
|
+
state :few do
|
7
|
+
state :very
|
8
|
+
state :almost
|
9
|
+
end
|
10
|
+
state :lots do
|
11
|
+
state :very
|
12
|
+
state :almost, :initial => true
|
13
|
+
end
|
14
|
+
state :throng
|
15
|
+
state :swarm
|
16
|
+
}
|
17
|
+
|
18
|
+
def initial_group
|
19
|
+
case value
|
20
|
+
when 1..5 then 'few.very'
|
21
|
+
when 5..10 then 'few.almost'
|
22
|
+
when 10..50 then :lots
|
23
|
+
when 50..150 then :throng
|
24
|
+
when 150..1000 then :swarm
|
25
|
+
else :unknown
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_initial_group_3
|
31
|
+
assert_equal 'few.very', Value.new(3).group
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_initial_group_7
|
35
|
+
assert_equal 'few.almost', Value.new(7).group
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_initial_group_30
|
39
|
+
assert_equal 'lots.almost', Value.new(30).group
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_initial_group_120
|
43
|
+
assert_equal 'throng', Value.new(120).group
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_unknown_intiial_group
|
47
|
+
assert_raises(Hifsm::MissingState) do
|
48
|
+
Value.new(-3).group
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/test/test_event_guard.rb
CHANGED
@@ -1,31 +1,39 @@
|
|
1
1
|
require 'setup_tests'
|
2
2
|
|
3
3
|
class TestEventGuard < Minitest::Test
|
4
|
-
Wall
|
5
|
-
|
6
|
-
def build_wall(thickness)
|
7
|
-
fsm = Hifsm::FSM.new do
|
4
|
+
class Wall < Struct.new(:stones)
|
5
|
+
include Hifsm.fsm_module {
|
8
6
|
state :constructed, :initial => true
|
9
7
|
state :broken
|
10
8
|
|
9
|
+
# guards can be inline proc, or symbols
|
11
10
|
event :break, :from => :constructed, :to => :broken, :guard => proc { stones < 5 }
|
11
|
+
|
12
|
+
# event parameters are passed to guards only if arity > 0
|
13
|
+
event :shoot, :from => :constructed, :to => :broken, :guard => :breakable?
|
14
|
+
}
|
15
|
+
def breakable?(hits)
|
16
|
+
hits * 5 > stones
|
12
17
|
end
|
13
|
-
|
14
|
-
|
15
|
-
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_can_break_thin_wall
|
21
|
+
wall = Wall.new(3)
|
22
|
+
wall.break
|
23
|
+
assert_equal 'broken', wall.state
|
16
24
|
end
|
17
25
|
|
18
26
|
def test_cant_break_wall_10_stones_thick
|
19
|
-
wall =
|
27
|
+
wall = Wall.new(10)
|
20
28
|
assert_raises(Hifsm::MissingTransition) do
|
21
29
|
wall.break
|
22
30
|
end
|
23
31
|
end
|
24
32
|
|
25
|
-
def
|
26
|
-
wall =
|
27
|
-
wall.
|
28
|
-
assert_equal 'broken',
|
33
|
+
def test_can_break_thick_wall_if_hit_3_times
|
34
|
+
wall = Wall.new(10)
|
35
|
+
wall.shoot 3
|
36
|
+
assert_equal 'broken', wall.state
|
29
37
|
end
|
30
38
|
|
31
39
|
end
|
@@ -1,30 +1,27 @@
|
|
1
1
|
require 'setup_tests'
|
2
2
|
|
3
3
|
class TestIflessFactorial < Minitest::Test
|
4
|
-
Value
|
5
|
-
|
6
|
-
def setup
|
7
|
-
@fsm = Hifsm::FSM.new do
|
4
|
+
class Value < Struct.new(:value)
|
5
|
+
include Hifsm.fsm_module {
|
8
6
|
state :idle, :initial => true
|
9
|
-
state :
|
7
|
+
state :computing
|
10
8
|
|
11
|
-
event :
|
9
|
+
event :compute, :to => :idle do
|
12
10
|
guard { |x| x == 0 }
|
13
11
|
after { |x| self.value = 1 }
|
14
12
|
end
|
15
|
-
event :
|
13
|
+
event :compute, :to => :computing do
|
16
14
|
after do |x|
|
17
|
-
|
15
|
+
compute(x - 1)
|
18
16
|
self.value *= x
|
19
17
|
end
|
20
18
|
end
|
21
|
-
|
19
|
+
}
|
22
20
|
end
|
23
21
|
|
24
22
|
def factorial(n)
|
25
23
|
val = Value.new
|
26
|
-
|
27
|
-
val.count(n).value
|
24
|
+
val.compute(n).value
|
28
25
|
end
|
29
26
|
|
30
27
|
def test_factorial_0
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'setup_tests'
|
2
|
+
|
3
|
+
class TestTwoMachines < Minitest::Test
|
4
|
+
class ColorPrinter
|
5
|
+
include Hifsm.fsm_module(:working_state) {
|
6
|
+
state :off, :initial => true
|
7
|
+
state :on do
|
8
|
+
action do
|
9
|
+
color_machine.to_s
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
event :toggle, :from => :off, :to => :on
|
14
|
+
event :toggle, :from => :on, :to => :off
|
15
|
+
}
|
16
|
+
include Hifsm.fsm_module(:color) {
|
17
|
+
state :red, :initial => true
|
18
|
+
state :green
|
19
|
+
state :blue
|
20
|
+
|
21
|
+
event :cycle_color!, :from => :red, :to => :green
|
22
|
+
event :cycle_color!, :from => :green, :to => :blue
|
23
|
+
event :cycle_color!, :from => :blue, :to => :red
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def setup
|
28
|
+
@color_printer = ColorPrinter.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_two_machines_defined
|
32
|
+
assert_equal 'off', @color_printer.working_state_machine.state
|
33
|
+
assert_equal 'red', @color_printer.color_machine.color
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_initial_state_is_off_and_red
|
37
|
+
assert_equal 'off', @color_printer.working_state
|
38
|
+
assert_equal 'red', @color_printer.color
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hifsm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Meremyanin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-09-
|
11
|
+
date: 2014-09-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,6 +52,48 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: coveralls
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activerecord
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
55
97
|
description: FSM with support for nested states and parameterised events
|
56
98
|
email:
|
57
99
|
- vladimir@meremyanin.com
|
@@ -60,12 +102,14 @@ extensions: []
|
|
60
102
|
extra_rdoc_files: []
|
61
103
|
files:
|
62
104
|
- ".gitignore"
|
105
|
+
- ".travis.yml"
|
63
106
|
- Gemfile
|
64
107
|
- LICENSE.txt
|
65
108
|
- README.md
|
66
109
|
- Rakefile
|
67
110
|
- hifsm.gemspec
|
68
111
|
- lib/hifsm.rb
|
112
|
+
- lib/hifsm/adapters/active_record_adapter.rb
|
69
113
|
- lib/hifsm/callbacks.rb
|
70
114
|
- lib/hifsm/event.rb
|
71
115
|
- lib/hifsm/fsm.rb
|
@@ -75,13 +119,16 @@ files:
|
|
75
119
|
- test/minitest_helper.rb
|
76
120
|
- test/monster.rb
|
77
121
|
- test/setup_tests.rb
|
122
|
+
- test/test_activerecord_adapter.rb
|
78
123
|
- test/test_any_state_event.rb
|
79
124
|
- test/test_basic_fsm.rb
|
125
|
+
- test/test_dynamic_initial_state.rb
|
80
126
|
- test/test_event_guard.rb
|
81
127
|
- test/test_hierarchical.rb
|
82
128
|
- test/test_ifless_factorial.rb
|
83
129
|
- test/test_many_states.rb
|
84
130
|
- test/test_monster.rb
|
131
|
+
- test/test_two_machines.rb
|
85
132
|
homepage: http://github.com/stiff/hifsm
|
86
133
|
licenses:
|
87
134
|
- MIT
|
@@ -110,10 +157,13 @@ test_files:
|
|
110
157
|
- test/minitest_helper.rb
|
111
158
|
- test/monster.rb
|
112
159
|
- test/setup_tests.rb
|
160
|
+
- test/test_activerecord_adapter.rb
|
113
161
|
- test/test_any_state_event.rb
|
114
162
|
- test/test_basic_fsm.rb
|
163
|
+
- test/test_dynamic_initial_state.rb
|
115
164
|
- test/test_event_guard.rb
|
116
165
|
- test/test_hierarchical.rb
|
117
166
|
- test/test_ifless_factorial.rb
|
118
167
|
- test/test_many_states.rb
|
119
168
|
- test/test_monster.rb
|
169
|
+
- test/test_two_machines.rb
|