pushdown 0.1.0.pre.20210714190141 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -0
- data/History.md +30 -1
- data/LICENSE.txt +27 -0
- data/README.md +42 -8
- data/lib/pushdown/automaton.rb +228 -0
- data/lib/pushdown/exceptions.rb +18 -0
- data/lib/pushdown/spec_helpers.rb +263 -0
- data/lib/pushdown/state.rb +174 -0
- data/lib/pushdown/transition/pop.rb +35 -0
- data/lib/pushdown/transition/push.rb +47 -0
- data/lib/pushdown/transition/replace.rb +49 -0
- data/lib/pushdown/transition/switch.rb +50 -0
- data/lib/pushdown/transition.rb +68 -0
- data/lib/pushdown.rb +7 -6
- data/spec/pushdown/automaton_spec.rb +152 -0
- data/spec/pushdown/spec_helpers_spec.rb +504 -0
- data/spec/pushdown/state_spec.rb +154 -0
- data/spec/pushdown/transition/pop_spec.rb +65 -0
- data/spec/pushdown/transition/push_spec.rb +62 -0
- data/spec/pushdown/transition/replace_spec.rb +72 -0
- data/spec/pushdown/transition/switch_spec.rb +71 -0
- data/spec/pushdown/transition_spec.rb +109 -0
- data/spec/spec_helper.rb +4 -0
- data.tar.gz.sig +0 -0
- metadata +93 -10
- metadata.gz.sig +0 -0
- data/.simplecov +0 -9
- data/Rakefile +0 -8
@@ -0,0 +1,174 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pluggability'
|
5
|
+
require 'loggability'
|
6
|
+
|
7
|
+
require 'pushdown' unless defined?( Pushdown )
|
8
|
+
|
9
|
+
|
10
|
+
# A componented state object in a Pushdown automaton
|
11
|
+
class Pushdown::State
|
12
|
+
extend Loggability
|
13
|
+
|
14
|
+
# Loggability API -- log to the pushdown logger
|
15
|
+
log_to :pushdown
|
16
|
+
|
17
|
+
# Don't allow instantation of the abstract class
|
18
|
+
private_class_method :new
|
19
|
+
|
20
|
+
##
|
21
|
+
# Allow introspection on declared transitions
|
22
|
+
singleton_class.attr_reader :transitions
|
23
|
+
|
24
|
+
|
25
|
+
### Inheritance callback -- allow subclasses to be instantiated, and add some
|
26
|
+
### class-instance data to them.
|
27
|
+
def self::inherited( subclass )
|
28
|
+
super
|
29
|
+
|
30
|
+
subclass.public_class_method( :new )
|
31
|
+
subclass.instance_variable_set( :@transitions, {} )
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
#
|
36
|
+
# Transition declarations
|
37
|
+
#
|
38
|
+
|
39
|
+
### Register a transition +type+ declaration method.
|
40
|
+
def self::register_transition( type )
|
41
|
+
type = type.to_sym
|
42
|
+
meth = lambda do |transition_name, *args|
|
43
|
+
self.transitions[ transition_name ] = [ type, *args ]
|
44
|
+
end
|
45
|
+
|
46
|
+
method_name = "transition_%s" % [ type ]
|
47
|
+
self.log.info "Setting up transition declaration method %p" % [ method_name ]
|
48
|
+
define_singleton_method( method_name, &meth )
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
### Return the transition's type as a lowercase Symbol, such as that specified
|
53
|
+
### in transition declarations.
|
54
|
+
def self::type_name
|
55
|
+
class_name = self.name or return :anonymous
|
56
|
+
return class_name.sub( /.*::/, '' ).downcase.to_sym
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
### Set up new States with an optional +data+ object.
|
61
|
+
def initialize( data=nil )
|
62
|
+
@data = data
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
######
|
67
|
+
public
|
68
|
+
######
|
69
|
+
|
70
|
+
##
|
71
|
+
# The state data object that was used to create the State (if any)
|
72
|
+
attr_reader :data
|
73
|
+
|
74
|
+
|
75
|
+
#
|
76
|
+
# Stack callbacks
|
77
|
+
#
|
78
|
+
|
79
|
+
### Stack callback -- called when the state is added to the stack.
|
80
|
+
def on_start
|
81
|
+
return nil # no-op
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
### Stack callback -- called when the state is removed from the stack.
|
86
|
+
def on_stop
|
87
|
+
return nil # no-op
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
### Stack callback -- called when another state is pushed over this one.
|
92
|
+
def on_pause
|
93
|
+
return nil # no-op
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
### Stack callback -- called when another state is popped off from in front of
|
98
|
+
### this one, making it the current state.
|
99
|
+
def on_resume
|
100
|
+
return nil # no-op
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
#
|
105
|
+
# Event callbacks
|
106
|
+
#
|
107
|
+
|
108
|
+
### Event callback -- called by the automaton when its #on_<stackname>_event method
|
109
|
+
### is called. This method can return a Transition or a Symbol which maps to one.
|
110
|
+
def on_event( event, *args )
|
111
|
+
return nil # no-op
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
#
|
116
|
+
# Interval callbacks
|
117
|
+
#
|
118
|
+
|
119
|
+
### State callback -- interval callback called when the state is the current
|
120
|
+
### one. This method can return a Transition or a Symbol which maps to one.
|
121
|
+
def update( *data )
|
122
|
+
return nil # no-op
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
### State callback -- interval callback called when the state is on the stack,
|
127
|
+
### even when the state is not the current one.
|
128
|
+
def shadow_update( *data )
|
129
|
+
return nil # no-op
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
#
|
134
|
+
# Introspection/information
|
135
|
+
#
|
136
|
+
|
137
|
+
### Return the transition's type as a lowercase Symbol, such as that specified
|
138
|
+
### in transition declarations.
|
139
|
+
def type_name
|
140
|
+
return self.class.type_name
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
### Return a description of the State as an engine phrase.
|
145
|
+
def description
|
146
|
+
return "%#x" % [ self.class.object_id ] unless self.class.name
|
147
|
+
return self.class.name.sub( /.*::/, '' ).
|
148
|
+
gsub( /([A-Z])([A-Z])/ ) { "#$1 #$2" }.
|
149
|
+
gsub( /([a-z])([A-Z])/ ) { "#$1 #$2" }.downcase
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
### Create a new instance of Pushdown::Transition named +transition_name+ that
|
154
|
+
### has been declared using one of the Transition Declaration methods.
|
155
|
+
def transition( transition_name, automaton, stack_name )
|
156
|
+
self.log.debug "Looking up the %p transition for %p via %p" %
|
157
|
+
[ transition_name, self, automaton ]
|
158
|
+
|
159
|
+
transition_type, state_class_name = self.class.transitions[ transition_name ]
|
160
|
+
raise "no such transition %p for %p" % [ transition_name, self.class ] unless transition_type
|
161
|
+
|
162
|
+
if state_class_name
|
163
|
+
state_class = automaton.class.pushdown_state_class( stack_name, state_class_name )
|
164
|
+
state_data = self.data
|
165
|
+
|
166
|
+
return Pushdown::Transition.
|
167
|
+
create( transition_type, transition_name, state_class, state_data )
|
168
|
+
else
|
169
|
+
return Pushdown::Transition.create( transition_type, transition_name )
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
end # class Pushdown::State
|
174
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pushdown/transition' unless defined?( Pushdown::Transition )
|
5
|
+
require 'pushdown/exceptions'
|
6
|
+
|
7
|
+
|
8
|
+
# A push transition -- add an instance of a given State to the top of the state
|
9
|
+
# stack.
|
10
|
+
class Pushdown::Transition::Pop < Pushdown::Transition
|
11
|
+
|
12
|
+
|
13
|
+
######
|
14
|
+
public
|
15
|
+
######
|
16
|
+
|
17
|
+
##
|
18
|
+
# Return the state that was popped
|
19
|
+
attr_reader :popped_state
|
20
|
+
|
21
|
+
|
22
|
+
### Apply the transition to the given +stack+.
|
23
|
+
def apply( stack )
|
24
|
+
raise Pushdown::TransitionError, "can't pop from an empty stack" if stack.empty?
|
25
|
+
raise Pushdown::TransitionError, "can't pop the only state on the stack" if stack.length == 1
|
26
|
+
|
27
|
+
self.log.debug "popping a state"
|
28
|
+
@popped_state = stack.pop
|
29
|
+
@popped_state.on_stop
|
30
|
+
stack.last.on_resume
|
31
|
+
|
32
|
+
return stack
|
33
|
+
end
|
34
|
+
|
35
|
+
end # class Pushdown::Transition::Pop
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pushdown/transition' unless defined?( Pushdown::Transition )
|
5
|
+
|
6
|
+
|
7
|
+
# A push transition -- add an instance of a given State to the top of the state
|
8
|
+
# stack.
|
9
|
+
class Pushdown::Transition::Push < Pushdown::Transition
|
10
|
+
|
11
|
+
|
12
|
+
### Create a transition that will Push an instance of the given +state_class+ to
|
13
|
+
### the stack.
|
14
|
+
def initialize( name, state_class, data=nil )
|
15
|
+
super( name )
|
16
|
+
|
17
|
+
@state_class = state_class
|
18
|
+
@data = data
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
######
|
23
|
+
public
|
24
|
+
######
|
25
|
+
|
26
|
+
##
|
27
|
+
# The State to push to.
|
28
|
+
attr_reader :state_class
|
29
|
+
|
30
|
+
##
|
31
|
+
# The data object to pass to the #state_class's constructor
|
32
|
+
attr_reader :data
|
33
|
+
|
34
|
+
|
35
|
+
### Apply the transition to the given +stack+.
|
36
|
+
def apply( stack )
|
37
|
+
state = self.state_class.new( self.data )
|
38
|
+
|
39
|
+
self.log.debug "pushing a new state: %p" % [ state ]
|
40
|
+
stack.last.on_pause if stack.last
|
41
|
+
stack.push( state )
|
42
|
+
state.on_start
|
43
|
+
|
44
|
+
return stack
|
45
|
+
end
|
46
|
+
|
47
|
+
end # class Pushdown::Transition::Push
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pushdown/transition' unless defined?( Pushdown::Transition )
|
5
|
+
|
6
|
+
|
7
|
+
# A replace transition -- remove all currents states from the stack and add a
|
8
|
+
# different one.
|
9
|
+
class Pushdown::Transition::Replace < Pushdown::Transition
|
10
|
+
|
11
|
+
### Create a transition that will Replace all the states on the current stack
|
12
|
+
### with an instance of the given +state_class+.
|
13
|
+
def initialize( name, state_class, data=nil )
|
14
|
+
super( name )
|
15
|
+
|
16
|
+
@state_class = state_class
|
17
|
+
@data = data
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
######
|
22
|
+
public
|
23
|
+
######
|
24
|
+
|
25
|
+
##
|
26
|
+
# The State to replace the stack members with.
|
27
|
+
attr_reader :state_class
|
28
|
+
|
29
|
+
##
|
30
|
+
# The data object to pass to the #state_class's constructor
|
31
|
+
attr_reader :data
|
32
|
+
|
33
|
+
|
34
|
+
### Apply the transition to the given +stack+.
|
35
|
+
def apply( stack )
|
36
|
+
state = self.state_class.new( self.data )
|
37
|
+
|
38
|
+
self.log.debug "replacing current state with a new state: %p" % [ state ]
|
39
|
+
while ( old_state = stack.pop )
|
40
|
+
old_state.on_stop
|
41
|
+
end
|
42
|
+
|
43
|
+
stack.push( state )
|
44
|
+
state.on_start
|
45
|
+
|
46
|
+
return stack
|
47
|
+
end
|
48
|
+
|
49
|
+
end # class Pushdown::Transition::Replace
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pushdown/transition' unless defined?( Pushdown::Transition )
|
5
|
+
|
6
|
+
|
7
|
+
# A switch transition -- remove the current state from the stack and add a
|
8
|
+
# different one.
|
9
|
+
class Pushdown::Transition::Switch < Pushdown::Transition
|
10
|
+
|
11
|
+
### Create a transition that will Switch the current State with an instance of
|
12
|
+
### the given +state_class+ on the stack.
|
13
|
+
def initialize( name, state_class, data=nil )
|
14
|
+
super( name )
|
15
|
+
|
16
|
+
@state_class = state_class
|
17
|
+
@data = data
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
######
|
22
|
+
public
|
23
|
+
######
|
24
|
+
|
25
|
+
##
|
26
|
+
# The State to push to.
|
27
|
+
attr_reader :state_class
|
28
|
+
|
29
|
+
##
|
30
|
+
# The data object to pass to the #state_class's constructor
|
31
|
+
attr_reader :data
|
32
|
+
|
33
|
+
|
34
|
+
### Apply the transition to the given +stack+.
|
35
|
+
def apply( stack )
|
36
|
+
raise Pushdown::TransitionError, "can't switch on an empty stack" if stack.empty?
|
37
|
+
|
38
|
+
state = self.state_class.new( self.data )
|
39
|
+
|
40
|
+
self.log.debug "switching current state with a new state: %p" % [ state ]
|
41
|
+
old_state = stack.pop
|
42
|
+
old_state.on_stop if old_state
|
43
|
+
|
44
|
+
stack.push( state )
|
45
|
+
state.on_start
|
46
|
+
|
47
|
+
return stack
|
48
|
+
end
|
49
|
+
|
50
|
+
end # class Pushdown::Transition::Switch
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'loggability'
|
5
|
+
require 'pluggability'
|
6
|
+
|
7
|
+
require 'pushdown' unless defined?( Pushdown )
|
8
|
+
require 'pushdown/state'
|
9
|
+
|
10
|
+
|
11
|
+
# A transition in a Pushdown automaton
|
12
|
+
class Pushdown::Transition
|
13
|
+
extend Loggability,
|
14
|
+
Pluggability
|
15
|
+
|
16
|
+
# Loggability API -- log to the pushdown logger
|
17
|
+
log_to :pushdown
|
18
|
+
|
19
|
+
# Pluggability API -- concrete types live in lib/pushdown/transition/
|
20
|
+
plugin_prefixes 'pushdown/transition'
|
21
|
+
plugin_exclusions 'spec/**/*'
|
22
|
+
|
23
|
+
|
24
|
+
# Don't allow direct instantiation (abstract class)
|
25
|
+
private_class_method :new
|
26
|
+
|
27
|
+
|
28
|
+
### Inheritance hook -- enable instantiation.
|
29
|
+
def self::inherited( subclass )
|
30
|
+
super
|
31
|
+
subclass.public_class_method( :new )
|
32
|
+
if (( type_name = subclass.name&.sub( /.*::/, '' )&.downcase ))
|
33
|
+
Pushdown::State.register_transition( type_name )
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
### Create a new Transition with the given +name+.
|
39
|
+
def initialize( name )
|
40
|
+
@name = name
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
######
|
45
|
+
public
|
46
|
+
######
|
47
|
+
|
48
|
+
##
|
49
|
+
# The name of the transition; mostly for human consumption
|
50
|
+
attr_reader :name
|
51
|
+
|
52
|
+
|
53
|
+
### Return a state +stack+ after the transition has been applied.
|
54
|
+
def apply( stack )
|
55
|
+
raise NotImplementedError, "%p doesn't implement required method #%p" %
|
56
|
+
[ self.class, __method__ ]
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
### Return the transition's type as a lowercase Symbol, such as that specified
|
61
|
+
### in transition declarations.
|
62
|
+
def type_name
|
63
|
+
class_name = self.class.name or return :anonymous
|
64
|
+
return class_name.sub( /.*::/, '' ).downcase.to_sym
|
65
|
+
end
|
66
|
+
|
67
|
+
end # class Pushdown::Transition
|
68
|
+
|
data/lib/pushdown.rb
CHANGED
@@ -15,17 +15,18 @@ module Pushdown
|
|
15
15
|
extend Loggability
|
16
16
|
|
17
17
|
# Package version
|
18
|
-
VERSION = '0.0
|
18
|
+
VERSION = '0.4.0'
|
19
19
|
|
20
20
|
|
21
21
|
# Loggability API -- create a logger for Pushdown classes and modules
|
22
22
|
log_as :pushdown
|
23
23
|
|
24
|
+
end # module Pushdown
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
26
|
+
require 'pushdown/transition'
|
27
|
+
require 'pushdown/state'
|
28
|
+
require 'pushdown/automaton'
|
29
29
|
|
30
|
-
|
30
|
+
Pushdown::Transition.plugin_exclusions( '**/spec/pushdown/transition/**' )
|
31
|
+
Pushdown::Transition.load_all
|
31
32
|
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative '../spec_helper'
|
5
|
+
|
6
|
+
require 'pushdown/automaton'
|
7
|
+
|
8
|
+
|
9
|
+
RSpec.describe( Pushdown::Automaton ) do
|
10
|
+
|
11
|
+
let( :extended_class ) do
|
12
|
+
the_class = Class.new
|
13
|
+
the_class.extend( described_class )
|
14
|
+
the_class
|
15
|
+
end
|
16
|
+
|
17
|
+
let( :starting_state ) do
|
18
|
+
Class.new( Pushdown::State ) do
|
19
|
+
transition_push :run, :running
|
20
|
+
def on_event( event, * )
|
21
|
+
return :run if event == :run
|
22
|
+
return nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
let( :off_state ) do
|
27
|
+
Class.new( Pushdown::State ) do
|
28
|
+
transition_push :start, :starting
|
29
|
+
def update( * )
|
30
|
+
return :start
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
let( :running_state ) { Class.new(Pushdown::State) }
|
35
|
+
|
36
|
+
let( :state_class_registry ) {{
|
37
|
+
starting: starting_state,
|
38
|
+
off: off_state,
|
39
|
+
running: running_state,
|
40
|
+
}}
|
41
|
+
|
42
|
+
|
43
|
+
it "allows a state attribute to be declared" do
|
44
|
+
extended_class.pushdown_state( :state, initial_state: :idle )
|
45
|
+
extended_class.const_set( :Idle, starting_state )
|
46
|
+
|
47
|
+
instance = extended_class.new
|
48
|
+
|
49
|
+
expect( instance.state ).to be_a( starting_state )
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
it "allows a state class registry to be inferred" do
|
54
|
+
extended_class.pushdown_state( :state, initial_state: :starting )
|
55
|
+
extended_class.const_set( :Starting, starting_state )
|
56
|
+
|
57
|
+
expect( extended_class.initial_state ).to be( starting_state )
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
it "allows a state registry to be passed as a Hash-alike" do
|
62
|
+
extended_class.pushdown_state( :status, initial_state: :starting, states: state_class_registry )
|
63
|
+
|
64
|
+
expect( extended_class.initial_status ).to be( starting_state )
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
context "for each pushdown state" do
|
69
|
+
|
70
|
+
it "allows a state registry to be passed as a (pluggable) Pushdown::State subclass" do
|
71
|
+
state_baseclass = Class.new( Pushdown::State ) do
|
72
|
+
singleton_class.attr_accessor :subclasses
|
73
|
+
def self::get_subclass( classname )
|
74
|
+
return subclasses[ classname ]
|
75
|
+
end
|
76
|
+
def self::create( class_name, *args )
|
77
|
+
return self.get_subclass( class_name ).new( *args )
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
state_baseclass.subclasses = state_class_registry
|
82
|
+
extended_class.pushdown_state( :state, initial_state: :starting, states: state_baseclass )
|
83
|
+
|
84
|
+
expect( extended_class.initial_state ).to be( starting_state )
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
it "generates an event method that applies transitions returned from #on_event" do
|
89
|
+
extended_class.pushdown_state( :state, initial_state: :starting )
|
90
|
+
extended_class.const_set( :Starting, starting_state )
|
91
|
+
extended_class.const_set( :Off, off_state )
|
92
|
+
extended_class.const_set( :Running, running_state )
|
93
|
+
|
94
|
+
instance = extended_class.new
|
95
|
+
result = instance.handle_state_event( :run )
|
96
|
+
|
97
|
+
expect( result ).to be_a( Pushdown::Transition::Push )
|
98
|
+
expect( instance.state ).to be_a( running_state )
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
it "generates an periodic update method that applies transitions returned from #update on the current state" do
|
103
|
+
extended_class.pushdown_state( :status, initial_state: :off )
|
104
|
+
extended_class.const_set( :Starting, starting_state )
|
105
|
+
extended_class.const_set( :Off, off_state )
|
106
|
+
extended_class.const_set( :Running, running_state )
|
107
|
+
|
108
|
+
instance = extended_class.new
|
109
|
+
result = instance.update_status
|
110
|
+
|
111
|
+
expect( result ).to be_a( Pushdown::Transition::Push )
|
112
|
+
expect( instance.status ).to be_a( starting_state )
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
it "generates an periodic update method that is called on every state in the stack" do
|
117
|
+
extended_class.pushdown_state( :status, initial_state: :off )
|
118
|
+
extended_class.const_set( :Starting, starting_state )
|
119
|
+
extended_class.const_set( :Off, off_state )
|
120
|
+
extended_class.const_set( :Running, running_state )
|
121
|
+
|
122
|
+
instance = extended_class.new
|
123
|
+
result = instance.shadow_update_status
|
124
|
+
|
125
|
+
expect( result ).to be_nil
|
126
|
+
expect( instance.status ).to be_a( off_state )
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
it "fetches initial state data if the extended class defines a method for doing so" do
|
131
|
+
extended_class.pushdown_state( :state, initial_state: :starting )
|
132
|
+
extended_class.const_set( :Starting, starting_state )
|
133
|
+
extended_class.const_set( :Off, off_state )
|
134
|
+
extended_class.const_set( :Running, running_state )
|
135
|
+
extended_class.attr_accessor :state_data
|
136
|
+
extended_class.define_method( :initial_state_data ) do
|
137
|
+
return self.state_data ||= {}
|
138
|
+
end
|
139
|
+
|
140
|
+
starting_state.define_method( :on_start ) do
|
141
|
+
data[:starting_started] = true
|
142
|
+
end
|
143
|
+
|
144
|
+
instance = extended_class.new
|
145
|
+
|
146
|
+
expect( instance.state_data ).to eq({ starting_started: true })
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|