MINT-statemachine 1.2.3 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- @transitions[transition.event] = transition
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.merge(@superstate.transitions) : @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 = @transitions[event]
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
- return transition
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
- return transition
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
- # if (@statemachine.is_parallel)
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
- @statemachine.activation.call(self.id,@statemachine.abstract_states,@statemachine.states_id) if @statemachine.activation
65
- # end
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
- return @root.startstate_id
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.abstract_states
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
- belongs, parallel = belongs_to_parallel(@state.id)
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
- cond = true
147
- if transition.cond!=true
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
- if cond
151
- transition.invoke(@state, self, args)
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
- super(message, args)
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
- return parallel.In(id)
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
- return id.to_s[-2..-1] == "_H"
303
+ id.to_s[-2..-1] == "_H"
269
304
  end
270
305
 
271
306
  def base_id(history_id)
272
- return history_id.to_s[0...-2].to_sym
307
+ history_id.to_s[0...-2].to_sym
273
308
  end
274
309
 
275
310
  def load_history(superstate)
@@ -47,7 +47,11 @@ module Statemachine
47
47
  return [@id] if not @superstate or @superstate.is_parallel
48
48
  ([@id] + @superstate.abstract_states).uniq
49
49
  end
50
-
50
+
51
+ def spontaneous_transition
52
+ startstate.spontaneous_transition
53
+ end
54
+
51
55
  end
52
56
 
53
57
  end
@@ -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
- if @action # changed this if statement to return if action fails
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
- entries.each { |entered_state| entered_state.activate(terminal_state.id) if entered_state.is_parallel }
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
- # return [], [] if origin == destination
83
+ # return [], [] if origin == destination
43
84
  exits = []
44
85
  entries = exits_and_entries_helper(exits, origin, destination)
45
86
  return exits, entries.reverse
@@ -2,8 +2,8 @@ module Statemachine
2
2
  module VERSION #:nodoc:
3
3
  unless defined? MAJOR
4
4
  MAJOR = 1
5
- MINOR = 2
6
- TINY = 3
5
+ MINOR = 3
6
+ TINY = 0
7
7
 
8
8
  STRING = [MAJOR, MINOR, TINY].join('.')
9
9
  TAG = "REL_" + [MAJOR, MINOR, TINY].join('_')
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require 'spec_helper'
2
2
  #require 'statemachine'
3
3
  #require 'lib/statemachine/action_invokation.rb'
4
4
  require "noodle"
data/spec/builder_spec.rb CHANGED
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require 'spec_helper'
2
2
 
3
3
  describe "Builder" do
4
4
 
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require 'spec_helper'
2
2
 
3
3
  describe "Default Transition" do
4
4
 
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/../../spec_helper'
1
+ require "./"+File.dirname(__FILE__) + '/../../spec_helper'
2
2
  require 'statemachine/generate/dot_graph/dot_graph_statemachine'
3
3
 
4
4
  describe Statemachine::Statemachine, "(Turn Stile)" do
data/spec/history_spec.rb CHANGED
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require 'spec_helper'
2
2
 
3
3
  describe "History States" do
4
4
 
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require 'spec_helper'
2
2
 
3
3
  describe "State Machine Odds And Ends" do
4
4
  include SwitchStatemachine
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
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 :state
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
- @state = []
16
+ @new_states = []
17
17
  @abstract_states = []
18
18
  @atomic_states =[]
19
19
 
20
20
  end
21
- def activate(state,abstract_states, atomic_states)
21
+ def activate(new_states,abstract_states, atomic_states)
22
22
  @called << true
23
- @state << state
23
+ @new_states<< new_states
24
24
  @abstract_states << abstract_states
25
25
  @atomic_states << atomic_states
26
- puts "activate #{@state.last} #{@abstract_states.last} #{@atomic_states.last}"
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
- @callback.called.length.should == 0
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.state.last.should == :on
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.state.last.should == :off
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 == 2
58
- @callback.state.last.should == :on
59
- @callback.abstract_states.last.should.eql? [:operative, :root, :onoff]
60
- @callback.atomic_states.last.should == [:locked, :on]
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.state.last.should == :off
63
- @callback.abstract_states.last.should.eql? [:onoff,:operative,:root]
64
- @callback.atomic_states.last.should.eql? [:off,:locked]
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.state.last.should == :off
74
- @callback.state.first.should == :on
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.state.last.should == :me
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.state.should.eql? [:unlocked,:on]
112
- @callback.called.length.should == 2
120
+ @callback.new_states.last.should == [:p, :operative, :onoff, :unlocked, :on]
121
+ @callback.called.length.should == 1
113
122
  end
114
123
 
115
124