MINT-statemachine 1.2.3 → 1.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.
- data/CHANGES +22 -0
- data/LICENSE +2 -2
- data/MINT-statemachine.gemspec +12 -12
- data/README.rdoc +1 -1
- data/lib/statemachine/action_invokation.rb +51 -4
- data/lib/statemachine/builder.rb +18 -1
- data/lib/statemachine/generate/dot_graph/dot_graph_statemachine.rb +2 -2
- data/lib/statemachine/generate/java/java_statemachine.rb +4 -4
- data/lib/statemachine/parallelstate.rb +97 -49
- data/lib/statemachine/state.rb +54 -13
- data/lib/statemachine/statemachine.rb +53 -18
- data/lib/statemachine/superstate.rb +5 -1
- data/lib/statemachine/transition.rb +45 -4
- data/lib/statemachine/version.rb +2 -2
- data/spec/action_invokation_spec.rb +1 -1
- data/spec/builder_spec.rb +1 -1
- data/spec/default_transition_spec.rb +1 -1
- data/spec/generate/dot_graph/dot_graph_stagemachine_spec.rb +1 -1
- data/spec/history_spec.rb +1 -1
- data/spec/sm_action_parameterization_spec.rb +1 -1
- data/spec/sm_activation_spec.rb +31 -22
- data/spec/sm_entry_exit_actions_spec.rb +1 -1
- data/spec/sm_odds_n_ends_spec.rb +1 -1
- data/spec/sm_parallel_state_spec.rb +48 -8
- data/spec/sm_simple_spec.rb +1 -1
- data/spec/sm_super_state_spec.rb +1 -1
- data/spec/sm_turnstile_spec.rb +5 -5
- data/spec/spec_helper.rb +7 -5
- data/spec/transition_spec.rb +103 -7
- metadata +58 -75
data/lib/statemachine/state.rb
CHANGED
@@ -10,23 +10,46 @@ module Statemachine
|
|
10
10
|
@id = id
|
11
11
|
@superstate = superstate
|
12
12
|
@statemachine = state_machine
|
13
|
-
@transitions =
|
13
|
+
@transitions = []
|
14
|
+
@spontaneous_transitions = []
|
15
|
+
end
|
16
|
+
|
17
|
+
# TODO hack to support SCXML parser parallel state creation
|
18
|
+
def modify_statemachine(state_machine)
|
19
|
+
@statemachine = state_machine
|
14
20
|
end
|
15
21
|
|
16
22
|
def add(transition)
|
17
|
-
|
23
|
+
if transition.event == nil
|
24
|
+
@spontaneous_transitions.push(transition)
|
25
|
+
else
|
26
|
+
@transitions.push(transition)
|
27
|
+
end
|
18
28
|
end
|
19
29
|
|
20
30
|
def transitions
|
21
|
-
return @superstate ? @transitions
|
31
|
+
return @superstate ? @transitions | @superstate.transitions : @transitions
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_transitions(event)
|
35
|
+
transitions = []
|
36
|
+
@transitions.each do |t|
|
37
|
+
if t.event == event
|
38
|
+
transitions << t
|
39
|
+
end
|
40
|
+
end
|
41
|
+
if transitions.empty?
|
42
|
+
return nil
|
43
|
+
end
|
44
|
+
transitions
|
22
45
|
end
|
23
46
|
|
24
47
|
def non_default_transition_for(event,check_superstates = true)
|
25
|
-
transition =
|
48
|
+
transition = get_transitions(event)
|
26
49
|
if check_superstates and @superstate
|
27
50
|
transition = @superstate.non_default_transition_for(event) if @superstate and @superstate.is_parallel == false and not transition
|
28
51
|
end
|
29
|
-
|
52
|
+
transition
|
30
53
|
end
|
31
54
|
|
32
55
|
def default_transition
|
@@ -35,10 +58,28 @@ module Statemachine
|
|
35
58
|
return nil
|
36
59
|
end
|
37
60
|
|
61
|
+
def spontaneous_transition
|
62
|
+
transition = []
|
63
|
+
@spontaneous_transitions.each do |s|
|
64
|
+
if s.cond == true
|
65
|
+
transition << [s,self]
|
66
|
+
else
|
67
|
+
if s.cond
|
68
|
+
transition << [s,self] if @statemachine.invoke_action(s.cond, [], "condition from #{@state} invoked by '#{nil}' event", nil, nil)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
if transition.empty?
|
74
|
+
return nil
|
75
|
+
end
|
76
|
+
return transition
|
77
|
+
end
|
78
|
+
|
38
79
|
def transition_for(event,check_superstate=true)
|
39
80
|
transition = non_default_transition_for(event,check_superstate)
|
40
81
|
transition = default_transition if not transition
|
41
|
-
|
82
|
+
transition
|
42
83
|
end
|
43
84
|
|
44
85
|
def exit(args)
|
@@ -58,11 +99,11 @@ module Statemachine
|
|
58
99
|
|
59
100
|
def activate
|
60
101
|
@statemachine.state = self
|
61
|
-
|
102
|
+
# if (@statemachine.is_parallel)
|
62
103
|
# @statemachine.activation.call(self.id,@statemachine.is_parallel.abstract_states,@statemachine.is_parallel.statemachine.states_id) if @statemachine.activation
|
63
104
|
#else
|
64
|
-
|
65
|
-
|
105
|
+
#@statemachine.activation.call(self.id,self.abstract_states,@statemachine.states_id) if @statemachine.activation # and not @statemachine.is_parallel
|
106
|
+
# end
|
66
107
|
end
|
67
108
|
|
68
109
|
def concrete?
|
@@ -83,13 +124,13 @@ module Statemachine
|
|
83
124
|
|
84
125
|
def has_superstate(id)
|
85
126
|
return false if not @superstate
|
86
|
-
return true if @superstate.id == id
|
127
|
+
return true if @superstate.id == id
|
87
128
|
return @superstate.has_superstate(id)
|
88
129
|
end
|
89
130
|
|
90
131
|
def abstract_states
|
91
|
-
return [] if not superstate
|
92
|
-
return @superstate.abstract_states if not @superstate.is_parallel
|
132
|
+
return [] if not @superstate
|
133
|
+
return @superstate.abstract_states #if not @superstate.is_parallel
|
93
134
|
[]
|
94
135
|
end
|
95
136
|
|
@@ -97,5 +138,5 @@ module Statemachine
|
|
97
138
|
false
|
98
139
|
end
|
99
140
|
end
|
100
|
-
|
141
|
+
|
101
142
|
end
|
@@ -37,6 +37,15 @@ module Statemachine
|
|
37
37
|
attr_accessor :messenger, :message_queue, :is_parallel #:nodoc:
|
38
38
|
attr_accessor :activation
|
39
39
|
|
40
|
+
# workaround that activation is not automatically set for parallel state machines
|
41
|
+
def activation
|
42
|
+
if is_parallel
|
43
|
+
is_parallel.statemachine.activation
|
44
|
+
else
|
45
|
+
@activation
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
40
49
|
# Should not be called directly. Instances of Statemachine::Statemachine are created
|
41
50
|
# through the Statemachine.build method.
|
42
51
|
def initialize(root = Superstate.new(:root, nil, self))
|
@@ -46,11 +55,11 @@ module Statemachine
|
|
46
55
|
|
47
56
|
# Returns the id of the startstate of the statemachine.
|
48
57
|
def startstate
|
49
|
-
|
58
|
+
@root.startstate_id
|
50
59
|
end
|
51
60
|
|
52
61
|
# Resets the statemachine back to its starting state.
|
53
|
-
def reset(startstate_id=nil)
|
62
|
+
def reset(startstate_id=nil, use_activation_callback = true)
|
54
63
|
|
55
64
|
if (startstate_id and @root.is_parallel) # called when enterin a parallel state or dierctly entering a child of a parallel state from outside the parallel state
|
56
65
|
@state = get_state(startstate_id)
|
@@ -62,9 +71,11 @@ module Statemachine
|
|
62
71
|
end
|
63
72
|
raise StatemachineException.new("The state machine doesn't know where to start. Try setting the startstate.") if @state == nil
|
64
73
|
@state.enter
|
74
|
+
@state.activate
|
65
75
|
@states.values.each { |state|
|
66
76
|
state.reset if not state.is_a? Parallelstate
|
67
77
|
}
|
78
|
+
activation.call([@state.id],abstract_states,states_id) if use_activation_callback and activation and not is_parallel
|
68
79
|
end
|
69
80
|
|
70
81
|
def context= c
|
@@ -94,7 +105,11 @@ module Statemachine
|
|
94
105
|
|
95
106
|
# returns an array with all currently active super states
|
96
107
|
def abstract_states
|
97
|
-
@state.
|
108
|
+
belongs, parallel = belongs_to_parallel(@state.id)
|
109
|
+
if belongs
|
110
|
+
return parallel.abstract_states
|
111
|
+
end
|
112
|
+
return @state.abstract_states
|
98
113
|
end
|
99
114
|
|
100
115
|
# You may change the state of the statemachine by using this method. The parameter should be
|
@@ -136,22 +151,32 @@ module Statemachine
|
|
136
151
|
event = event.to_sym
|
137
152
|
trace "Event: #{event}"
|
138
153
|
if @state
|
139
|
-
|
140
|
-
if belongs
|
141
|
-
r = parallel.process_event(event, *args)
|
142
|
-
return true if r
|
143
|
-
end
|
154
|
+
|
144
155
|
transition = @state.transition_for(event)
|
145
156
|
if transition
|
146
|
-
|
147
|
-
|
148
|
-
cond = @state.statemachine.invoke_action(transition.cond, [], "condition from #{@state} invoked by '#{event}' event", nil, nil)
|
157
|
+
if not transition.is_a? Array
|
158
|
+
transition = [transition]
|
149
159
|
end
|
150
|
-
|
151
|
-
|
160
|
+
transition.each do |t|
|
161
|
+
cond = true
|
162
|
+
if t.cond != true
|
163
|
+
cond = @state.statemachine.invoke_action(t.cond, [], "condition from #{@state} invoked by '#{event}' event", nil, nil)
|
164
|
+
end
|
165
|
+
if cond
|
166
|
+
t.invoke(@state, self, args)
|
167
|
+
end
|
152
168
|
end
|
153
169
|
else
|
170
|
+
|
171
|
+
belongs, parallel = belongs_to_parallel(@state.id)
|
172
|
+
if belongs
|
173
|
+
r = parallel.process_event(event, *args)
|
174
|
+
if r
|
175
|
+
return true
|
176
|
+
end
|
177
|
+
end
|
154
178
|
raise TransitionMissingException.new("#{@state} does not respond to the '#{event}' event.")
|
179
|
+
|
155
180
|
end
|
156
181
|
|
157
182
|
else
|
@@ -205,6 +230,9 @@ module Statemachine
|
|
205
230
|
elsif p = get_parallel and s = p.get_state(id)
|
206
231
|
return s
|
207
232
|
else
|
233
|
+
if @root.is_a? Parallelstate
|
234
|
+
return false
|
235
|
+
end
|
208
236
|
state = State.new(id, @root, self)
|
209
237
|
@states[id] = state
|
210
238
|
return state
|
@@ -233,6 +261,12 @@ module Statemachine
|
|
233
261
|
return false
|
234
262
|
end
|
235
263
|
|
264
|
+
def which_state_respond_to?(message)
|
265
|
+
#return super if super(message)
|
266
|
+
return @state if @state and @state.transition_for(message)
|
267
|
+
nil
|
268
|
+
end
|
269
|
+
|
236
270
|
def method_missing(message, *args) #:nodoc:
|
237
271
|
if @state and @state.transition_for(message)
|
238
272
|
process_event(message.to_sym, *args)
|
@@ -240,8 +274,9 @@ module Statemachine
|
|
240
274
|
# params = [message.to_sym].concat(args)
|
241
275
|
# method.call(*params)
|
242
276
|
else
|
243
|
-
begin
|
244
|
-
|
277
|
+
begin return super if super(message)
|
278
|
+
|
279
|
+
super(message, args)
|
245
280
|
rescue NoMethodError
|
246
281
|
process_event(message.to_sym, *args)
|
247
282
|
end
|
@@ -258,18 +293,18 @@ module Statemachine
|
|
258
293
|
# check if it is one of the running parallel states
|
259
294
|
belongs, parallel = belongs_to_parallel(@state.id)
|
260
295
|
if belongs
|
261
|
-
|
296
|
+
parallel.In(id)
|
262
297
|
end
|
263
298
|
end
|
264
299
|
|
265
300
|
private
|
266
301
|
|
267
302
|
def is_history_state_id?(id)
|
268
|
-
|
303
|
+
id.to_s[-2..-1] == "_H"
|
269
304
|
end
|
270
305
|
|
271
306
|
def base_id(history_id)
|
272
|
-
|
307
|
+
history_id.to_s[0...-2].to_sym
|
273
308
|
end
|
274
309
|
|
275
310
|
def load_history(superstate)
|
@@ -13,14 +13,21 @@ module Statemachine
|
|
13
13
|
@cond = cond
|
14
14
|
end
|
15
15
|
|
16
|
+
def is_self_transition?
|
17
|
+
@origin_id == @destination_id
|
18
|
+
end
|
19
|
+
|
16
20
|
def invoke(origin, statemachine, args)
|
21
|
+
old_abstract_states = statemachine.abstract_states
|
22
|
+
old_atomic_states = statemachine.states_id
|
23
|
+
|
17
24
|
destination = statemachine.get_state(@destination_id)
|
18
25
|
exits, entries = exits_and_entries(origin, destination)
|
19
26
|
exits.each { |exited_state| exited_state.exit(args) }
|
20
27
|
messenger = origin.statemachine.messenger
|
21
28
|
message_queue = origin.statemachine.message_queue
|
22
29
|
|
23
|
-
|
30
|
+
if @action # changed this if statement to return if action fails
|
24
31
|
if not origin.statemachine.invoke_action(@action, args, "transition action from #{origin} invoked by '#{@event}' event", messenger, message_queue)
|
25
32
|
raise StatemachineException.new("Transition to state #{destination.id} failed because action for event #{@event} return false.")
|
26
33
|
end
|
@@ -30,16 +37,50 @@ module Statemachine
|
|
30
37
|
|
31
38
|
terminal_state = entries.last
|
32
39
|
|
33
|
-
terminal_state.activate if terminal_state and not terminal_state.is_parallel
|
34
40
|
|
41
|
+
terminal_state.activate if terminal_state and not terminal_state.is_parallel
|
35
42
|
entries.each { |entered_state| entered_state.enter(args) }
|
36
|
-
|
43
|
+
#entries.each { |entered_state| entered_state.activate(terminal_state.id) if entered_state.is_parallel }
|
37
44
|
statemachine.state = terminal_state if statemachine.has_state(terminal_state.id) and statemachine.is_parallel
|
38
45
|
|
46
|
+
if is_self_transition? # handle special case of self transition
|
47
|
+
new_states = [@destination_id]
|
48
|
+
else
|
49
|
+
new_states = statemachine.states_id - (old_atomic_states & statemachine.states_id)
|
50
|
+
new_states = (statemachine.abstract_states - old_abstract_states) + new_states
|
51
|
+
end
|
52
|
+
|
53
|
+
if statemachine.activation
|
54
|
+
sm = statemachine
|
55
|
+
while (sm.is_parallel)
|
56
|
+
sm = sm.is_parallel.statemachine
|
57
|
+
end
|
58
|
+
sm.activation.call(new_states,sm.abstract_states,sm.states_id) if sm.activation # and not @statemachine.is_parallel
|
59
|
+
end
|
60
|
+
|
61
|
+
# Take any valid spontaneous transitions
|
62
|
+
transition = destination.spontaneous_transition
|
63
|
+
|
64
|
+
if transition
|
65
|
+
if destination.is_parallel
|
66
|
+
transition.each do |trans,statem|
|
67
|
+
trans.each do |t|
|
68
|
+
t[0].invoke(t[1], statem, args) if t[0].is_a? Transition
|
69
|
+
end
|
70
|
+
end
|
71
|
+
else
|
72
|
+
|
73
|
+
if transition.is_a? Array
|
74
|
+
transition.each do |t|
|
75
|
+
t[0].invoke(t[1], statemachine, args) if t[0].is_a? Transition
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
39
80
|
end
|
40
81
|
|
41
82
|
def exits_and_entries(origin, destination)
|
42
|
-
|
83
|
+
# return [], [] if origin == destination
|
43
84
|
exits = []
|
44
85
|
entries = exits_and_entries_helper(exits, origin, destination)
|
45
86
|
return exits, entries.reverse
|
data/lib/statemachine/version.rb
CHANGED
data/spec/builder_spec.rb
CHANGED
data/spec/history_spec.rb
CHANGED
data/spec/sm_activation_spec.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require 'spec_helper'
|
2
2
|
|
3
3
|
describe "State Activation Callback" do
|
4
4
|
include SwitchStatemachine
|
@@ -7,46 +7,54 @@ describe "State Activation Callback" do
|
|
7
7
|
before(:each) do
|
8
8
|
class ActivationCallback
|
9
9
|
attr_reader :called
|
10
|
-
attr_reader :
|
10
|
+
attr_reader :new_states
|
11
11
|
attr_reader :abstract_states
|
12
12
|
attr_reader :atomic_states
|
13
13
|
|
14
14
|
def initialize
|
15
15
|
@called = []
|
16
|
-
@
|
16
|
+
@new_states = []
|
17
17
|
@abstract_states = []
|
18
18
|
@atomic_states =[]
|
19
19
|
|
20
20
|
end
|
21
|
-
def activate(
|
21
|
+
def activate(new_states,abstract_states, atomic_states)
|
22
22
|
@called << true
|
23
|
-
@
|
23
|
+
@new_states<< new_states
|
24
24
|
@abstract_states << abstract_states
|
25
25
|
@atomic_states << atomic_states
|
26
|
-
puts "activate #{@
|
26
|
+
puts "activate #{@new_states.last} #{@abstract_states.last} #{@atomic_states.last}"
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
30
|
@callback = ActivationCallback.new
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
it "should fire on successful state change" do
|
34
34
|
create_switch
|
35
35
|
@sm.activation=@callback.method(:activate)
|
36
|
-
|
36
|
+
|
37
37
|
@sm.toggle
|
38
38
|
@callback.called.length.should == 1
|
39
39
|
end
|
40
40
|
|
41
|
+
it "should call activation callback after rest" do
|
42
|
+
create_switch
|
43
|
+
@sm.activation=@callback.method(:activate)
|
44
|
+
@callback.called.length.should == 0
|
45
|
+
@sm.reset
|
46
|
+
@callback.called.length.should == 1
|
47
|
+
end
|
48
|
+
|
41
49
|
it "should deliver new active state on state change" do
|
42
50
|
create_switch
|
43
51
|
@sm.activation=@callback.method(:activate)
|
44
52
|
@sm.toggle
|
45
|
-
@callback.
|
53
|
+
@callback.new_states.last.should == [:on]
|
46
54
|
@callback.atomic_states.last.should == [:on]
|
47
55
|
@callback.abstract_states.last.should == [:root]
|
48
56
|
@sm.toggle
|
49
|
-
@callback.
|
57
|
+
@callback.new_states.last.should == [:off]
|
50
58
|
end
|
51
59
|
|
52
60
|
it "should deliver new active state on state change of parallel state machine" do
|
@@ -54,24 +62,25 @@ describe "State Activation Callback" do
|
|
54
62
|
|
55
63
|
@sm.activation=@callback.method(:activate)
|
56
64
|
@sm.go
|
57
|
-
@callback.called.length.should ==
|
58
|
-
@callback.
|
59
|
-
@callback.abstract_states.last.should
|
60
|
-
@callback.atomic_states.last.should == [:locked
|
65
|
+
@callback.called.length.should == 1
|
66
|
+
@callback.new_states.last.should == [:p, :operative, :onoff, :locked, :on]
|
67
|
+
@callback.abstract_states.last.should == [:root,:p, :operative, :onoff]
|
68
|
+
@callback.atomic_states.last.should == [:locked,:on]
|
61
69
|
@sm.toggle
|
62
|
-
@callback.
|
63
|
-
@callback.abstract_states.last.should
|
64
|
-
@callback.atomic_states.last.should
|
70
|
+
@callback.new_states.last.should == [:off]
|
71
|
+
@callback.abstract_states.last.should == [:root, :p, :operative, :onoff]
|
72
|
+
@callback.atomic_states.last.should == [:locked,:off]
|
65
73
|
|
66
74
|
end
|
67
75
|
|
68
76
|
it "activation works for on_entry ticks as well" do
|
77
|
+
pending "throwing an event inside on entry is substituted by event-less transitions"
|
69
78
|
create_tick
|
70
79
|
@sm.activation=@callback.method(:activate)
|
71
80
|
@sm.toggle
|
72
81
|
@callback.called.length.should == 2
|
73
|
-
@callback.
|
74
|
-
@callback.
|
82
|
+
@callback.new_states.last.should == :off
|
83
|
+
@callback.new_states.first.should == :on
|
75
84
|
@callback.atomic_states.last.should == [:off]
|
76
85
|
@callback.atomic_states.first.should == [:on]
|
77
86
|
@callback.abstract_states.last.should == [:root]
|
@@ -82,7 +91,7 @@ describe "State Activation Callback" do
|
|
82
91
|
@sm.activation=@callback.method(:activate)
|
83
92
|
@sm.toggle
|
84
93
|
@callback.called.length.should == 1
|
85
|
-
@callback.
|
94
|
+
@callback.new_states.last.should == [:me]
|
86
95
|
@callback.atomic_states.last.should == [:me]
|
87
96
|
@callback.abstract_states.last.should == [:root]
|
88
97
|
end
|
@@ -108,8 +117,8 @@ describe "State Activation Callback" do
|
|
108
117
|
|
109
118
|
@sm.activation=@callback.method(:activate)
|
110
119
|
@sm.go
|
111
|
-
@callback.
|
112
|
-
@callback.called.length.should ==
|
120
|
+
@callback.new_states.last.should == [:p, :operative, :onoff, :unlocked, :on]
|
121
|
+
@callback.called.length.should == 1
|
113
122
|
end
|
114
123
|
|
115
124
|
|