state-fu 0.11.1
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/LICENSE +40 -0
- data/README.textile +293 -0
- data/Rakefile +114 -0
- data/lib/binding.rb +292 -0
- data/lib/event.rb +192 -0
- data/lib/executioner.rb +120 -0
- data/lib/hooks.rb +39 -0
- data/lib/interface.rb +132 -0
- data/lib/lathe.rb +538 -0
- data/lib/machine.rb +184 -0
- data/lib/method_factory.rb +243 -0
- data/lib/persistence.rb +116 -0
- data/lib/persistence/active_record.rb +34 -0
- data/lib/persistence/attribute.rb +47 -0
- data/lib/persistence/base.rb +100 -0
- data/lib/persistence/relaxdb.rb +23 -0
- data/lib/persistence/session.rb +7 -0
- data/lib/sprocket.rb +58 -0
- data/lib/state-fu.rb +56 -0
- data/lib/state.rb +48 -0
- data/lib/support/active_support_lite/array.rb +9 -0
- data/lib/support/active_support_lite/array/access.rb +60 -0
- data/lib/support/active_support_lite/array/conversions.rb +202 -0
- data/lib/support/active_support_lite/array/extract_options.rb +21 -0
- data/lib/support/active_support_lite/array/grouping.rb +109 -0
- data/lib/support/active_support_lite/array/random_access.rb +13 -0
- data/lib/support/active_support_lite/array/wrapper.rb +25 -0
- data/lib/support/active_support_lite/blank.rb +67 -0
- data/lib/support/active_support_lite/cattr_reader.rb +57 -0
- data/lib/support/active_support_lite/keys.rb +57 -0
- data/lib/support/active_support_lite/misc.rb +59 -0
- data/lib/support/active_support_lite/module.rb +1 -0
- data/lib/support/active_support_lite/module/delegation.rb +130 -0
- data/lib/support/active_support_lite/object.rb +9 -0
- data/lib/support/active_support_lite/string.rb +38 -0
- data/lib/support/active_support_lite/symbol.rb +16 -0
- data/lib/support/applicable.rb +41 -0
- data/lib/support/arrays.rb +197 -0
- data/lib/support/core_ext.rb +90 -0
- data/lib/support/exceptions.rb +106 -0
- data/lib/support/has_options.rb +16 -0
- data/lib/support/logger.rb +165 -0
- data/lib/support/methodical.rb +17 -0
- data/lib/support/no_stdout.rb +55 -0
- data/lib/support/plotter.rb +62 -0
- data/lib/support/vizier.rb +300 -0
- data/lib/tasks/spec_last.rake +55 -0
- data/lib/tasks/state_fu.rake +57 -0
- data/lib/transition.rb +338 -0
- data/lib/transition_query.rb +224 -0
- data/spec/custom_formatter.rb +49 -0
- data/spec/features/binding_and_transition_helper_mixin_spec.rb +111 -0
- data/spec/features/method_missing_only_once_spec.rb +28 -0
- data/spec/features/not_requirements_spec.rb +118 -0
- data/spec/features/plotter_spec.rb +97 -0
- data/spec/features/shared_log_spec.rb +7 -0
- data/spec/features/singleton_machine_spec.rb +39 -0
- data/spec/features/state_and_array_options_accessor_spec.rb +47 -0
- data/spec/features/transition_boolean_comparison_spec.rb +101 -0
- data/spec/helper.rb +13 -0
- data/spec/integration/active_record_persistence_spec.rb +202 -0
- data/spec/integration/binding_extension_spec.rb +41 -0
- data/spec/integration/class_accessor_spec.rb +117 -0
- data/spec/integration/event_definition_spec.rb +74 -0
- data/spec/integration/example_01_document_spec.rb +133 -0
- data/spec/integration/example_02_string_spec.rb +88 -0
- data/spec/integration/instance_accessor_spec.rb +97 -0
- data/spec/integration/lathe_extension_spec.rb +67 -0
- data/spec/integration/machine_duplication_spec.rb +101 -0
- data/spec/integration/relaxdb_persistence_spec.rb +97 -0
- data/spec/integration/requirement_reflection_spec.rb +270 -0
- data/spec/integration/state_definition_spec.rb +163 -0
- data/spec/integration/transition_spec.rb +1033 -0
- data/spec/spec.opts +9 -0
- data/spec/spec_helper.rb +132 -0
- data/spec/state_fu_spec.rb +948 -0
- data/spec/units/binding_spec.rb +192 -0
- data/spec/units/event_spec.rb +214 -0
- data/spec/units/exceptions_spec.rb +82 -0
- data/spec/units/lathe_spec.rb +570 -0
- data/spec/units/machine_spec.rb +229 -0
- data/spec/units/method_factory_spec.rb +366 -0
- data/spec/units/sprocket_spec.rb +69 -0
- data/spec/units/state_spec.rb +59 -0
- metadata +171 -0
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
thisdir = File.expand_path(File.dirname(__FILE__))
|
3
|
+
|
4
|
+
# ensure we require state-fu from lib, not gems
|
5
|
+
$LOAD_PATH.unshift( "#{thisdir}/../lib" )
|
6
|
+
require 'state-fu'
|
7
|
+
require 'support/no_stdout'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'rubygems'
|
10
|
+
require 'spec'
|
11
|
+
|
12
|
+
# record the log output on each run
|
13
|
+
LOGFILE = File.join('log', 'spec.log') unless Object.const_defined?('LOGFILE')
|
14
|
+
FileUtils.rm LOGFILE if File.exists?(LOGFILE)
|
15
|
+
StateFu::Logger.level = Logger::INFO
|
16
|
+
StateFu::Logger.logger = Logger.new(LOGFILE)
|
17
|
+
|
18
|
+
module MySpecHelper
|
19
|
+
include NoStdout
|
20
|
+
|
21
|
+
def prepare_active_record( options={}, &migration )
|
22
|
+
if skip_slow_specs?
|
23
|
+
skip_slow_specs and return false
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'activesupport'
|
28
|
+
require 'active_record'
|
29
|
+
require 'sqlite3'
|
30
|
+
rescue LoadError => e
|
31
|
+
pending "skipping specifications due to load error: #{e}"
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
|
35
|
+
options.symbolize_keys!
|
36
|
+
options.assert_valid_keys( :db_config, :migration_name, :hidden )
|
37
|
+
|
38
|
+
# connect ActiveRecord
|
39
|
+
db_config = options.delete(:db_config) || {
|
40
|
+
:adapter => 'sqlite3',
|
41
|
+
:database => ':memory:'
|
42
|
+
}
|
43
|
+
ActiveRecord::Base.establish_connection( db_config )
|
44
|
+
|
45
|
+
return unless block_given?
|
46
|
+
|
47
|
+
# prepare the migration
|
48
|
+
migration_class_name =
|
49
|
+
options.delete(:migration_name) || 'BeforeSpecMigration'
|
50
|
+
make_pristine_class( migration_class_name, ActiveRecord::Migration )
|
51
|
+
migration_class = migration_class_name.constantize
|
52
|
+
migration_class.class_eval( &migration )
|
53
|
+
|
54
|
+
# run the migration without spewing crap everywhere
|
55
|
+
if options.delete(:hidden) != false
|
56
|
+
no_stdout { migration_class.migrate( :up ) }
|
57
|
+
else
|
58
|
+
migration_class.migrate( :up )
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def skip_slow_specs?
|
63
|
+
!!ENV['SKIP_SLOW_SPECS']
|
64
|
+
end
|
65
|
+
|
66
|
+
def skip_slow_specs
|
67
|
+
pending('Skipping slow specs - run $ rake all if you want them')
|
68
|
+
end
|
69
|
+
|
70
|
+
def skip_unless_relaxdb
|
71
|
+
unless Object.const_defined?( 'RelaxDB' )
|
72
|
+
pending('Skipping specs because you do not have the relaxdb gem (paulcarey-relaxdb) installed ...')
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def prepare_relaxdb( options={} )
|
77
|
+
if skip_slow_specs?
|
78
|
+
return false
|
79
|
+
end
|
80
|
+
begin
|
81
|
+
require 'relaxdb'
|
82
|
+
if Object.const_defined?( "RelaxDB" )
|
83
|
+
RelaxDB.configure :host => "localhost", :port => 5984, :design_doc => "spec_doc"
|
84
|
+
RelaxDB.delete_db "relaxdb_spec" rescue "ok"
|
85
|
+
RelaxDB.use_db "relaxdb_spec"
|
86
|
+
RelaxDB.enable_view_creation
|
87
|
+
end
|
88
|
+
rescue LoadError => e
|
89
|
+
# pending "skipping specifications due to load error: #{e}"
|
90
|
+
return false
|
91
|
+
end
|
92
|
+
begin
|
93
|
+
RelaxDB.replicate_db "relaxdb_spec_base", "relaxdb_spec"
|
94
|
+
RelaxDB.enable_view_creation
|
95
|
+
rescue => e
|
96
|
+
puts "\n===== Run rake create_base_db before the first spec run ====="
|
97
|
+
puts
|
98
|
+
exit!
|
99
|
+
end
|
100
|
+
#
|
101
|
+
end
|
102
|
+
|
103
|
+
def make_pristine_class(class_name, superklass=Object, &block)
|
104
|
+
@class_names ||= []
|
105
|
+
@class_names << class_name
|
106
|
+
klass = Class.new( superklass )
|
107
|
+
klass.send( :include, StateFu )
|
108
|
+
Object.send(:remove_const, class_name ) if Object.const_defined?( class_name )
|
109
|
+
Object.const_set(class_name, klass)
|
110
|
+
klass.class_eval &block if block_given?
|
111
|
+
end
|
112
|
+
|
113
|
+
def reset!
|
114
|
+
@class_names ||= []
|
115
|
+
@class_names.each do |class_name|
|
116
|
+
Object.send(:remove_const, class_name ) if Object.const_defined?( class_name )
|
117
|
+
end
|
118
|
+
@class_names = []
|
119
|
+
end
|
120
|
+
|
121
|
+
def set_method_arity( object, method_name, needed_arity = 1 )
|
122
|
+
raise caller.first.inspect
|
123
|
+
a = Proc.new {}
|
124
|
+
stub( a ).arity() { needed_arity }
|
125
|
+
stub( object ).method( anything ) { |x| object.send(x) }
|
126
|
+
stub( object ).method( method_name ) { a }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
Spec::Runner.configure do |config|
|
131
|
+
config.include MySpecHelper
|
132
|
+
end
|
@@ -0,0 +1,948 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
#
|
4
|
+
# Door
|
5
|
+
#
|
6
|
+
|
7
|
+
describe "A door which opens and shuts:" do
|
8
|
+
before :all do
|
9
|
+
|
10
|
+
# class Door
|
11
|
+
make_pristine_class('Door') do
|
12
|
+
include StateFu
|
13
|
+
|
14
|
+
attr_accessor :locked
|
15
|
+
|
16
|
+
def shut
|
17
|
+
"I don't know how to shut!"
|
18
|
+
end
|
19
|
+
|
20
|
+
def locked?
|
21
|
+
!!locked
|
22
|
+
end
|
23
|
+
|
24
|
+
def method_missing(method_name, *args, &block)
|
25
|
+
raise NoMethodError.new("I'm just a door!" )
|
26
|
+
end
|
27
|
+
|
28
|
+
machine do
|
29
|
+
event :shut, :transitions_from => :open, :to => :closed
|
30
|
+
event :slam, :transitions_from => :open, :to => :closed
|
31
|
+
event :open, :transitions_from => :closed, :to => :open,
|
32
|
+
:requires => :not_locked?,
|
33
|
+
:message => "Sorry, it's locked."
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end # before
|
37
|
+
|
38
|
+
describe "Door's state machine" do
|
39
|
+
it "have two states, :open and :closed" do
|
40
|
+
Door.machine.states.names.should == [:open, :closed]
|
41
|
+
end
|
42
|
+
|
43
|
+
it "have two events, :shut and :open" do
|
44
|
+
Door.machine.events.names.should == [:shut, :slam, :open]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "have an initial state of :open, the first state defined" do
|
48
|
+
Door.machine.initial_state.name.should == :open
|
49
|
+
end
|
50
|
+
|
51
|
+
it "have an initial state of :open, the first state defined" do
|
52
|
+
Door.machine.initial_state.name.should == :open
|
53
|
+
end
|
54
|
+
|
55
|
+
it "have a requirement :not_locked? for the :open event" do
|
56
|
+
Door.machine.events[:open].requirements.should == [:not_locked?]
|
57
|
+
end
|
58
|
+
|
59
|
+
it "have a requirement_message 'Sorry, it's locked.' for :not_locked?" do
|
60
|
+
Door.machine.requirement_messages[:not_locked?].should == "Sorry, it's locked."
|
61
|
+
end
|
62
|
+
|
63
|
+
it "can reflect on event origin and target states" do
|
64
|
+
event = Door.machine.events[:open]
|
65
|
+
event.origin_names.should == [:closed]
|
66
|
+
event.target_names.should == [:open]
|
67
|
+
event.simple?.should == true # because there's only one possible target
|
68
|
+
event.origin.should == :closed # because there's only one origin
|
69
|
+
event.target.should == :open # because there's only one target
|
70
|
+
event.machine.should == Door.machine
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "door" do
|
76
|
+
before do
|
77
|
+
@door = Door.new
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "magic event methods" do
|
81
|
+
|
82
|
+
it "doesn't normally have a method #shut!" do
|
83
|
+
@door.respond_to?(:shut!).should == false
|
84
|
+
end
|
85
|
+
|
86
|
+
it "will define #shut! when method_missing is called for the first time" do
|
87
|
+
begin
|
88
|
+
@door.play_the_ukelele
|
89
|
+
rescue NoMethodError
|
90
|
+
end
|
91
|
+
@door.respond_to?(:shut!).should == true
|
92
|
+
end
|
93
|
+
|
94
|
+
it "will keep any existing methods when method_missing is triggered" do
|
95
|
+
@door.respond_to?(:shut).should == true
|
96
|
+
@door.respond_to?(:can_shut?).should == false
|
97
|
+
@door.shut.should == "I don't know how to shut!"
|
98
|
+
@door.can_shut?.should == true # triggers method_missing
|
99
|
+
@door.respond_to?(:shut).should == true
|
100
|
+
@door.respond_to?(:can_shut?).should == true # new methods defined
|
101
|
+
@door.shut.should == "I don't know how to shut!" # old method retained
|
102
|
+
end
|
103
|
+
|
104
|
+
it "gets a set of new methods when any magic method is called" do
|
105
|
+
@door.respond_to?(:shut).should == true # already defined
|
106
|
+
@door.respond_to?(:open).should == false
|
107
|
+
@door.respond_to?(:can_shut?).should == false
|
108
|
+
@door.respond_to?(:can_open?).should == false
|
109
|
+
@door.respond_to?(:shut!).should == false
|
110
|
+
@door.respond_to?(:open!).should == false
|
111
|
+
@door.can_shut?.should == true # call one of them (triggers method_missing)
|
112
|
+
@door.respond_to?(:open).should == false # a private method: Kernel#open
|
113
|
+
@door.respond_to?(:can_shut?).should == true # but these are all newly defined public methods
|
114
|
+
@door.respond_to?(:can_open?).should == true
|
115
|
+
@door.respond_to?(:shut!).should == true
|
116
|
+
@door.respond_to?(:open!).should == true
|
117
|
+
end
|
118
|
+
|
119
|
+
it "retains any previously defined method_missing" do
|
120
|
+
begin
|
121
|
+
@door.hug_me
|
122
|
+
rescue NoMethodError => e
|
123
|
+
e.message.should == "I'm just a door!"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe "for :slam - " do
|
128
|
+
describe "#slam" do
|
129
|
+
it "should return an unfired StateFu::Transition" do
|
130
|
+
t = @door.slam
|
131
|
+
t.should be_kind_of(StateFu::Transition)
|
132
|
+
t.fired?.should == false
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe "#can_slam?" do
|
137
|
+
it "should be true if the transition is valid for the current state" do
|
138
|
+
@door.current_state.should == :open
|
139
|
+
@door.can_slam?.should == true
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should be false when the transition has unmet requirements" do
|
143
|
+
Door.machine.events[:slam].lathe do
|
144
|
+
requires :some_impossible_condition do
|
145
|
+
false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
@door.can_slam?.should == false
|
149
|
+
end
|
150
|
+
|
151
|
+
it "should be nil when the transition is invalid for the current state" do
|
152
|
+
@door.shut!
|
153
|
+
@door.current_state.should == :closed
|
154
|
+
@door.can_slam?.should == nil
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
end # magic event methods
|
160
|
+
|
161
|
+
describe "magic state methods" do
|
162
|
+
it "should be defined for each state by method_missing voodoo" do
|
163
|
+
@door.should_not respond_to(:closed?)
|
164
|
+
@door.should_not respond_to(:open?)
|
165
|
+
@door.open?.should == true
|
166
|
+
@door.should respond_to(:closed?)
|
167
|
+
@door.should respond_to(:open?)
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "for :closed - " do
|
171
|
+
describe "#closed?" do
|
172
|
+
it "should be true when the current_state is :closed" do
|
173
|
+
@door.current_state.should == :open
|
174
|
+
@door.closed?.should == false
|
175
|
+
@door.shut!
|
176
|
+
@door.closed?.should == true
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end # magic state methods
|
181
|
+
|
182
|
+
it "#can_shut? when the current state is open" do
|
183
|
+
@door.current_state.should == :open
|
184
|
+
# @door.state_fu.valid_transitions.map(&:destination).inspect
|
185
|
+
@door.can_shut?.should == true
|
186
|
+
@door.can_open?.should == nil # not a valid transition from this state -> nil
|
187
|
+
end
|
188
|
+
|
189
|
+
it "transitions from :open to :closed on #shut!" do
|
190
|
+
@door.current_state.should == :open
|
191
|
+
shut_result = @door.shut!
|
192
|
+
shut_result.should be_true
|
193
|
+
shut_result.should be_kind_of(StateFu::Transition)
|
194
|
+
shut_result.should be_complete
|
195
|
+
@door.current_state.should == :closed
|
196
|
+
end
|
197
|
+
|
198
|
+
it "raises a StateFu::IllegalTransition if #shut! is called when already :closed" do
|
199
|
+
@door.current_state.should == :open
|
200
|
+
@door.shut!.should be_true
|
201
|
+
@door.current_state.should == :closed
|
202
|
+
lambda do
|
203
|
+
t = @door.shut!
|
204
|
+
t.origin.should == :open
|
205
|
+
end.should raise_error(StateFu::IllegalTransition)
|
206
|
+
end
|
207
|
+
|
208
|
+
it "raises StateFu::RequirementError if #open! is called when it is locked" do
|
209
|
+
@door.shut!
|
210
|
+
@door.locked = true
|
211
|
+
lambda { @door.open! }.should raise_error(StateFu::RequirementError)
|
212
|
+
end
|
213
|
+
|
214
|
+
it "tells you why it won't open if you ask nicely" do
|
215
|
+
@door.shut!
|
216
|
+
@door.locked = true
|
217
|
+
@door.locked?.should be_true
|
218
|
+
|
219
|
+
transition = @door.state_fu.transition :open
|
220
|
+
transition.requirement_errors.should == {:not_locked? => "Sorry, it's locked."}
|
221
|
+
end
|
222
|
+
|
223
|
+
it "gives you information about the requirement errors if you rescue the RequirementError" do
|
224
|
+
@door.shut!
|
225
|
+
@door.locked = true
|
226
|
+
@door.locked?.should be_true
|
227
|
+
begin
|
228
|
+
@door.open!
|
229
|
+
rescue StateFu::RequirementError => e
|
230
|
+
e.to_a.should == ["Sorry, it's locked."]
|
231
|
+
e.to_h.should == {:not_locked? => "Sorry, it's locked."}
|
232
|
+
e.to_enum.should be_kind_of(Enumerable::Enumerator)
|
233
|
+
e.should_not be_empty
|
234
|
+
e.length.should == 1
|
235
|
+
e.each do |requirement, message|
|
236
|
+
requirement.should == :not_locked?
|
237
|
+
message.should == "Sorry, it's locked."
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
describe "Transition objects" do
|
243
|
+
|
244
|
+
# TODO refactor me
|
245
|
+
def should_be_an_unfired_transition_with_the_event_slam_from_open_to_closed(transition)
|
246
|
+
transition.should be_kind_of(StateFu::Transition)
|
247
|
+
transition.fired?.should == false
|
248
|
+
transition.current_state.should == :open
|
249
|
+
transition.event.should == :slam
|
250
|
+
transition.origin.should == :open
|
251
|
+
transition.target.should == :closed
|
252
|
+
end
|
253
|
+
|
254
|
+
it "returns a Transition on #slam" do
|
255
|
+
@door.slam do |transition|
|
256
|
+
transition.should be_kind_of(StateFu::Transition)
|
257
|
+
transition.fired?.should == false
|
258
|
+
transition.current_state.should == :open
|
259
|
+
transition.event.should == :slam
|
260
|
+
transition.origin.should == :open
|
261
|
+
transition.target.should == :closed
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
it "returns a Transition on #state_fu.slam" do
|
266
|
+
transition = @door.state_fu.slam
|
267
|
+
should_be_an_unfired_transition_with_the_event_slam_from_open_to_closed( transition )
|
268
|
+
end
|
269
|
+
|
270
|
+
it "returns a Transition on #state_fu.transition :slam" do
|
271
|
+
transition = @door.state_fu.transition :slam
|
272
|
+
should_be_an_unfired_transition_with_the_event_slam_from_open_to_closed( transition )
|
273
|
+
end
|
274
|
+
|
275
|
+
it "returns a Transition on #state_fu.transition [:slam, :closed]" do
|
276
|
+
transition = @door.state_fu.transition [:slam, :closed]
|
277
|
+
should_be_an_unfired_transition_with_the_event_slam_from_open_to_closed( transition )
|
278
|
+
end
|
279
|
+
|
280
|
+
it "changes the door's state when you #fire! the transition" do
|
281
|
+
transition = @door.slam
|
282
|
+
transition.fire!
|
283
|
+
transition.fired?.should == true
|
284
|
+
transition.complete?.should == true
|
285
|
+
@door.current_state.should == :closed
|
286
|
+
end
|
287
|
+
|
288
|
+
it "can tell you its #origin and #target states" do
|
289
|
+
transition = @door.state_fu.transition :shut
|
290
|
+
transition.origin.should be_kind_of(StateFu::State)
|
291
|
+
transition.target.should be_kind_of(StateFu::State)
|
292
|
+
transition.origin.should == :open
|
293
|
+
transition.target.should == :closed
|
294
|
+
end
|
295
|
+
|
296
|
+
it "can give you information about any requirement errors" do
|
297
|
+
@door.shut!
|
298
|
+
@door.locked = true
|
299
|
+
transition = @door.state_fu.transition :open
|
300
|
+
transition.valid?.should == false
|
301
|
+
transition.unmet_requirements.should == [:not_locked?]
|
302
|
+
transition.unmet_requirement_messages.should == ["Sorry, it's locked."]
|
303
|
+
transition.requirement_errors.should == {:not_locked? => "Sorry, it's locked."}
|
304
|
+
transition.first_unmet_requirement.should == :not_locked?
|
305
|
+
transition.first_unmet_requirement_message.should == "Sorry, it's locked."
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
# TODO save this for later ...............
|
310
|
+
describe "#state_fu_binding" do
|
311
|
+
it "be a StateFu::Binding" do
|
312
|
+
@door.state_fu_binding.should be_kind_of StateFu::Binding
|
313
|
+
end
|
314
|
+
|
315
|
+
it "have a current_state which is initially :open" do
|
316
|
+
@door.state_fu_binding.current_state.should == :open
|
317
|
+
end
|
318
|
+
|
319
|
+
it "have two events, :shut and :slam" do
|
320
|
+
@door.state_fu_binding.events.should == [:shut, :slam]
|
321
|
+
end
|
322
|
+
|
323
|
+
it "have a list of #valid_transitions" do
|
324
|
+
@door.state_fu_binding.valid_transitions.should be_kind_of(StateFu::TransitionQuery)
|
325
|
+
@door.state_fu_binding.valid_transitions.length.should == 2
|
326
|
+
transition = @door.state_fu_binding.valid_transitions.first
|
327
|
+
transition.event.name.should == :shut
|
328
|
+
transition.origin.name.should == :open
|
329
|
+
transition.target.name.should == :closed
|
330
|
+
transition = @door.state_fu_binding.valid_transitions.last
|
331
|
+
transition.event.name.should == :slam
|
332
|
+
transition.origin.name.should == :open
|
333
|
+
transition.target.name.should == :closed
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
describe "#state_fu" do
|
338
|
+
it "be the same as door#state_fu_binding" do
|
339
|
+
@door.state_fu.should == @door.state_fu_binding
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
describe "#stfu" do
|
344
|
+
it "be the same as door#state_fu_binding" do
|
345
|
+
@door.stfu.should == @door.state_fu_binding
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
describe "#fu" do
|
350
|
+
it "be the same as door#state_fu_binding" do
|
351
|
+
@door.fu.should == @door.state_fu_binding
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
#
|
359
|
+
# Heart
|
360
|
+
#
|
361
|
+
|
362
|
+
describe "a simple machine, a heart which beats:" do
|
363
|
+
|
364
|
+
before :all do
|
365
|
+
make_pristine_class('Heart') do
|
366
|
+
include StateFu
|
367
|
+
|
368
|
+
def heartbeats
|
369
|
+
@heartbeats ||= []
|
370
|
+
end
|
371
|
+
|
372
|
+
machine do
|
373
|
+
cycle :state => :beating, :on => :beat do
|
374
|
+
causes(:heartbeat) { heartbeats << :thumpthump }
|
375
|
+
end
|
376
|
+
event :stop, :from => { :beating => :stopped }
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end # before
|
380
|
+
|
381
|
+
describe "the machine" do
|
382
|
+
it "have two states, :beating and :stopped" do
|
383
|
+
Heart.machine.states.names.should == [:beating,:stopped]
|
384
|
+
end
|
385
|
+
|
386
|
+
it "have two events, :beat and :stop" do
|
387
|
+
Heart.machine.events.names.should == [:beat, :stop]
|
388
|
+
end
|
389
|
+
|
390
|
+
it "have an initial state of :beating" do
|
391
|
+
Heart.machine.initial_state.name.should == :beating
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
describe "it" do
|
396
|
+
before do
|
397
|
+
@heart = Heart.new
|
398
|
+
end
|
399
|
+
|
400
|
+
it "cause a heartbeat on heart#beat!" do
|
401
|
+
@heart.heartbeats.should == []
|
402
|
+
@heart.beat!.should be_true
|
403
|
+
@heart.heartbeats.should == [:thumpthump]
|
404
|
+
end
|
405
|
+
|
406
|
+
it "raise an IllegalTransition if it tries to beat after it's stopped" do
|
407
|
+
@heart.stop!
|
408
|
+
@heart.current_state.should == :stopped
|
409
|
+
lambda { @heart.beat! }.should raise_error(StateFu::IllegalTransition)
|
410
|
+
end
|
411
|
+
|
412
|
+
it "transition to :stopped on #next!" do
|
413
|
+
@heart.current_state.should == :beating
|
414
|
+
@heart.state_fu.transitions.not_cyclic.length.should == 1
|
415
|
+
@heart.state_fu.next_transition.should_not == nil
|
416
|
+
@heart.state_fu.next_state.should_not == nil
|
417
|
+
@heart.next_state!
|
418
|
+
@heart.current_state.should == :stopped
|
419
|
+
end
|
420
|
+
|
421
|
+
it "transition to :stopped on #next_state!" do
|
422
|
+
@heart.current_state.should == :beating
|
423
|
+
@heart.next_state!
|
424
|
+
@heart.current_state.should == :stopped
|
425
|
+
end
|
426
|
+
|
427
|
+
it "transition to :stopped on #next_transition!" do
|
428
|
+
@heart.current_state.should == :beating
|
429
|
+
@heart.next_state!
|
430
|
+
@heart.current_state.should == :stopped
|
431
|
+
end
|
432
|
+
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
#
|
437
|
+
# Traffic Lights
|
438
|
+
#
|
439
|
+
|
440
|
+
describe "a simple machine, a set of traffic lights:" do
|
441
|
+
before :all do
|
442
|
+
|
443
|
+
make_pristine_class('TrafficLights') do
|
444
|
+
include StateFu
|
445
|
+
attr_reader :photos
|
446
|
+
|
447
|
+
def initialize
|
448
|
+
@photos = []
|
449
|
+
end
|
450
|
+
|
451
|
+
def red_light_camera
|
452
|
+
@photos << :click
|
453
|
+
end
|
454
|
+
|
455
|
+
machine do
|
456
|
+
state :go, :colour => :green
|
457
|
+
state :caution, :colour => :amber
|
458
|
+
state :stop, :colour => :red do
|
459
|
+
on_entry :red_light_camera
|
460
|
+
end
|
461
|
+
|
462
|
+
connect_states :go, :caution, :stop, :go
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end # before
|
466
|
+
|
467
|
+
describe "the machine:" do
|
468
|
+
it "have three states, :go, :caution, and :stop" do
|
469
|
+
TrafficLights.machine.states.names.should == [:go, :caution, :stop]
|
470
|
+
end
|
471
|
+
|
472
|
+
it "have three events :go_to_caution, :caution_to_stop, and :stop_to_go" do
|
473
|
+
TrafficLights.machine.events.names.should == [:go_to_caution, :caution_to_stop, :stop_to_go]
|
474
|
+
end
|
475
|
+
|
476
|
+
it "have an initial_state of :go" do
|
477
|
+
TrafficLights.machine.initial_state.name.should == :go
|
478
|
+
end
|
479
|
+
|
480
|
+
describe "the states' options" do
|
481
|
+
it "have an appropriate colour" do
|
482
|
+
TrafficLights.machine.states[:go] [:colour].should == :green
|
483
|
+
TrafficLights.machine.states[:caution][:colour].should == :amber
|
484
|
+
TrafficLights.machine.states[:stop] [:colour].should == :red
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
describe "it" do
|
490
|
+
before do
|
491
|
+
@lights = TrafficLights.new
|
492
|
+
end
|
493
|
+
|
494
|
+
it "transition from :go to :caution on #go_to_caution!" do
|
495
|
+
@lights.current_state.should == :go
|
496
|
+
@lights.go_to_caution!
|
497
|
+
@lights.current_state.should == :caution
|
498
|
+
end
|
499
|
+
|
500
|
+
it "transition from :go to :caution on #next!" do
|
501
|
+
@lights.current_state.should == :go
|
502
|
+
@lights.next!
|
503
|
+
@lights.current_state.should == :caution
|
504
|
+
end
|
505
|
+
|
506
|
+
it "transition from :go to :caution on #next_state!" do
|
507
|
+
@lights.current_state.should == :go
|
508
|
+
@lights.next_state!
|
509
|
+
@lights.current_state.should == :caution
|
510
|
+
end
|
511
|
+
|
512
|
+
it "transition from :go to :caution on #fire_next_transition!" do
|
513
|
+
@lights.current_state.should == :go
|
514
|
+
@lights.fire_next_transition!
|
515
|
+
@lights.current_state.should == :caution
|
516
|
+
end
|
517
|
+
|
518
|
+
describe "when entering the :stop state" do
|
519
|
+
it "fire :red_light_camera" do
|
520
|
+
@lights.next!
|
521
|
+
@lights.photos.should be_empty
|
522
|
+
@lights.next!
|
523
|
+
@lights.current_state.should == :stop
|
524
|
+
@lights.photos.length.should == 1
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
#
|
531
|
+
# Recorder
|
532
|
+
#
|
533
|
+
describe "arguments given to different method signatures" do
|
534
|
+
before :all do
|
535
|
+
make_pristine_class('Recorder') do
|
536
|
+
include StateFu
|
537
|
+
attr_accessor :received
|
538
|
+
|
539
|
+
def initialize
|
540
|
+
@received = {}
|
541
|
+
end
|
542
|
+
|
543
|
+
# arguments passed to methods / procs:
|
544
|
+
# these method signatures get a transition
|
545
|
+
def a1(t) received[:a1] = [t] end
|
546
|
+
def b1(t=nil) received[:b1] = [t] end
|
547
|
+
def c1(*t) received[:c1] = [t] end
|
548
|
+
|
549
|
+
# these method signatures get a transition and a list of arguments
|
550
|
+
def a2(t,a) received[:a2] = [t,a] end
|
551
|
+
def b2(t,a=nil) received[:b2] = [t,a] end
|
552
|
+
def c2(t,*a) received[:c2] = [t,a] end
|
553
|
+
|
554
|
+
# these method signatures get a transition, a list of arguments,
|
555
|
+
# and the object which owns the machine
|
556
|
+
def a3(t,a,o) received[:a3] = [t,a,o] end
|
557
|
+
def b3(t,a,o=nil) received[:b3] = [t,a,o] end
|
558
|
+
def c3(t,a,*o) received[:c3] = [t,a,o] end
|
559
|
+
|
560
|
+
machine do
|
561
|
+
cycle :state => :observing, :on => :observe do
|
562
|
+
trigger :a1, :b1, :c1, :a2, :b2, :c2, :a3, :b3, :c3
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
end
|
567
|
+
end # before
|
568
|
+
|
569
|
+
describe "the machine" do
|
570
|
+
it "have an event :observe which is a #cycle?" do
|
571
|
+
Recorder.machine.events[:observe].cycle?.should be_true
|
572
|
+
end
|
573
|
+
|
574
|
+
it "have a list of execute hooks" do
|
575
|
+
Recorder.machine.events[:observe].hooks[:execute].should == [:a1, :b1, :c1, :a2, :b2, :c2, :a3, :b3, :c3]
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
describe "it" do
|
580
|
+
before do
|
581
|
+
@recorder = Recorder.new
|
582
|
+
end
|
583
|
+
|
584
|
+
it "fire a transition on #observe!" do
|
585
|
+
t = @recorder.observe!
|
586
|
+
results = @recorder.received
|
587
|
+
t.should be_kind_of(StateFu::Transition)
|
588
|
+
t.should be_complete
|
589
|
+
end
|
590
|
+
|
591
|
+
describe "observing method calls on #observe!" do
|
592
|
+
before do
|
593
|
+
@t = @recorder.observe!
|
594
|
+
@results = @recorder.received
|
595
|
+
end
|
596
|
+
|
597
|
+
it "call the event's :execute hooks on #observe!" do
|
598
|
+
@results.keys.should =~ [:a1, :b1, :c1, :a2, :b2, :c2, :a3, :b3, :c3]
|
599
|
+
end
|
600
|
+
|
601
|
+
describe "methods which expect one argument" do
|
602
|
+
it "receive a StateFu::Transition" do
|
603
|
+
@results[:a1].should == [@t]
|
604
|
+
@results[:b1].should == [@t]
|
605
|
+
@results[:c1].should == [[@t]]
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
describe "methods which expect two arguments" do
|
610
|
+
it "receive a StateFu::Transition and an argument list" do
|
611
|
+
@results[:a2].should == [@t, @t.args]
|
612
|
+
@results[:b2].should == [@t, @t.args]
|
613
|
+
@results[:c2].should == [@t, [@t.args]]
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
describe "methods which expect three arguments" do
|
618
|
+
it "receive a StateFu::Transition, an argument list and the recorder object" do
|
619
|
+
@results[:a3].should == [@t, @t.args, @recorder]
|
620
|
+
@results[:b3].should == [@t, @t.args, @recorder]
|
621
|
+
@results[:c3].should == [@t, @t.args, [@recorder]]
|
622
|
+
end
|
623
|
+
end
|
624
|
+
end
|
625
|
+
end
|
626
|
+
end
|
627
|
+
|
628
|
+
#
|
629
|
+
# Pokies
|
630
|
+
#
|
631
|
+
|
632
|
+
describe "sitting at a poker machine" do
|
633
|
+
|
634
|
+
before :all do
|
635
|
+
make_pristine_class('PokerMachine') do
|
636
|
+
|
637
|
+
attr_accessor :silly_noises_inflicted
|
638
|
+
|
639
|
+
def insert_coins n
|
640
|
+
@credits = n * PokerMachine::CREDITS_PER_COIN
|
641
|
+
end
|
642
|
+
|
643
|
+
# sets coins to 0 and returns what it was
|
644
|
+
def refund_coins
|
645
|
+
(self.credits, x = 0, self.credits / PokerMachine::CREDITS_PER_COIN)[1]
|
646
|
+
end
|
647
|
+
|
648
|
+
def play_a_silly_noise
|
649
|
+
@silly_noises_inflicted << [:silly_noise]
|
650
|
+
end
|
651
|
+
|
652
|
+
# an array with the accessors (StateFu::Bindings)
|
653
|
+
# for each of the wheels' state machines, for convenience
|
654
|
+
def wheels
|
655
|
+
[wheel_one, wheel_two, wheel_three]
|
656
|
+
end
|
657
|
+
|
658
|
+
def wheels_spinning?
|
659
|
+
wheels.any?(&:spinning?)
|
660
|
+
end
|
661
|
+
|
662
|
+
def display
|
663
|
+
wheels.map(&:current_state_name)
|
664
|
+
end
|
665
|
+
|
666
|
+
def wait
|
667
|
+
while wheels_spinning?
|
668
|
+
spin_wheels!
|
669
|
+
end
|
670
|
+
stop_spinning!
|
671
|
+
end
|
672
|
+
|
673
|
+
PokerMachine::CREDITS_TO_PLAY = 5
|
674
|
+
PokerMachine::CREDITS_PER_COIN = 5
|
675
|
+
|
676
|
+
attr_accessor :credits
|
677
|
+
|
678
|
+
def initialize
|
679
|
+
@credits = 0
|
680
|
+
@silly_noises_inflicted = []
|
681
|
+
end
|
682
|
+
|
683
|
+
machine do
|
684
|
+
# adds a hook to the machine's global after slot
|
685
|
+
after_everything :play_a_silly_noise
|
686
|
+
|
687
|
+
# Define helper methods with 'proc' or its alias 'define'. This is
|
688
|
+
# implicit when you supply a block and a symbol for an event or state
|
689
|
+
# hook, a requirement, or a requirement failure message.
|
690
|
+
#
|
691
|
+
# Named procs are "machine-local": they are available in any other
|
692
|
+
# block evaluated by StateFu for a given machine, but are not defined
|
693
|
+
# on the stateful class itself.
|
694
|
+
#
|
695
|
+
# Use them to extend the state machine DSL without cluttering up your
|
696
|
+
# classes themselves.
|
697
|
+
#
|
698
|
+
# If you want a method which spans multiple machines (eg 'wheels',
|
699
|
+
# above) or which is available to your object in any context, define
|
700
|
+
# it as a standard method. You will then be able to access it in any
|
701
|
+
# of your state machines.
|
702
|
+
named_proc(:wheel_states) { wheels.map(&:current_state) }
|
703
|
+
named_proc(:wheels_stopped?) do
|
704
|
+
!wheels.any?(&:spinning?)
|
705
|
+
end
|
706
|
+
|
707
|
+
state :ready do
|
708
|
+
|
709
|
+
event :pull_lever, :transitions_to => :spinning do
|
710
|
+
# The execution context always provides handy access to all the
|
711
|
+
# methods of the PokerMachine instance - however, constants must
|
712
|
+
# still be qualified.
|
713
|
+
requires(:enough_credits) { self.credits >= PokerMachine::CREDITS_TO_PLAY }
|
714
|
+
triggers(:deduct_credits) { self.credits -= PokerMachine::CREDITS_TO_PLAY }
|
715
|
+
triggers(:spin_wheels) { [wheel_one, wheel_two,wheel_three].each(&:start!) }
|
716
|
+
# if we enable this line, the machine will #wait automatically
|
717
|
+
# so that merely pulling the lever causes it to return to the ready state:
|
718
|
+
#
|
719
|
+
# after :wait
|
720
|
+
end # :pull_lever event
|
721
|
+
end # :ready state
|
722
|
+
|
723
|
+
state :spinning do
|
724
|
+
cycle :spin_wheels do
|
725
|
+
# executes after the transition has been accepted
|
726
|
+
after do
|
727
|
+
wheels.each do |wheel|
|
728
|
+
if wheel.spinning?
|
729
|
+
wheel.spin!
|
730
|
+
end
|
731
|
+
end
|
732
|
+
end # execute
|
733
|
+
end # :spinning state
|
734
|
+
|
735
|
+
event :stop_spinning, :to => :ready do
|
736
|
+
requires :wheels_stopped?
|
737
|
+
execute :payout do
|
738
|
+
if wheel_states == wheel_states.uniq
|
739
|
+
self.credits += wheel_states.first[:value]
|
740
|
+
end
|
741
|
+
end
|
742
|
+
end # :stop_spinning event
|
743
|
+
end # spinning state
|
744
|
+
end # default machine
|
745
|
+
|
746
|
+
[:one, :two, :three].each do |wheel|
|
747
|
+
machine "wheel_#{wheel}" do
|
748
|
+
|
749
|
+
state :bomb, :value => -5
|
750
|
+
state :cherry, :value => 5
|
751
|
+
state :smiley, :value => 10
|
752
|
+
state :gold, :value => 15
|
753
|
+
|
754
|
+
state :spinning do
|
755
|
+
cycle :spin do
|
756
|
+
execute do
|
757
|
+
silly_noises_inflicted << :spinning_noise
|
758
|
+
end
|
759
|
+
after do
|
760
|
+
if rand(3) == 0
|
761
|
+
# we use binding.stop! rather than self.stop! here
|
762
|
+
# to disambiguate which machine we're sending the event to.
|
763
|
+
#
|
764
|
+
# .binding yields a StateFu::Binding, which has all the same
|
765
|
+
# magic methods as @pokie, but is explicitly for one machine,
|
766
|
+
# and one @pokie.
|
767
|
+
#
|
768
|
+
# @pokie.stop! would always cause the same wheel to stop
|
769
|
+
# (the first one, becuase it was defined first, and automatically
|
770
|
+
# defined methods never clobber any pre-existing methods) -
|
771
|
+
# which isn't what we want here.
|
772
|
+
binding.stop!([:bomb, :cherry, :smiley, :gold].rand)
|
773
|
+
end
|
774
|
+
end
|
775
|
+
end
|
776
|
+
end
|
777
|
+
|
778
|
+
initial_state states.except(:spinning).rand
|
779
|
+
|
780
|
+
event :start, :from => states.except(:spinning), :to => :spinning
|
781
|
+
event :stop, :from => :spinning, :to => states.except(:spinning)
|
782
|
+
|
783
|
+
end # machine :cell_#{cell}
|
784
|
+
end # each cell
|
785
|
+
end # PokerMachine
|
786
|
+
end # before
|
787
|
+
|
788
|
+
describe "the state machine" do
|
789
|
+
end
|
790
|
+
|
791
|
+
before :each do
|
792
|
+
@pokie = PokerMachine.new
|
793
|
+
end
|
794
|
+
|
795
|
+
# just a sanity check for method_missing
|
796
|
+
it "doesn't talk to you" do
|
797
|
+
lambda { @pokie.talk_to_me }.should raise_error(NoMethodError)
|
798
|
+
end
|
799
|
+
|
800
|
+
it "you need credits to pull the lever" do
|
801
|
+
@pokie.state_fu!
|
802
|
+
@pokie.credits.should == 0
|
803
|
+
@pokie.state_fu!
|
804
|
+
@pokie.can_pull_lever?.should == false
|
805
|
+
lambda { @pokie.pull_lever! }.should raise_error(StateFu::RequirementError)
|
806
|
+
end
|
807
|
+
|
808
|
+
it "has three wheels" do
|
809
|
+
@pokie.wheels.length.should == 3
|
810
|
+
end
|
811
|
+
|
812
|
+
it "displays three icons" do
|
813
|
+
@pokie.display.should be_kind_of(Array)
|
814
|
+
@pokie.display.map(&:class).should == [Symbol, Symbol, Symbol]
|
815
|
+
(@pokie.display - [:bomb, :cherry, :smiley, :gold]).should be_empty
|
816
|
+
end
|
817
|
+
|
818
|
+
describe "putting in 20 coins" do
|
819
|
+
before do
|
820
|
+
@pokie.insert_coins(20)
|
821
|
+
end
|
822
|
+
|
823
|
+
it "gives you 100 credits" do
|
824
|
+
@pokie.credits.should == 100
|
825
|
+
end
|
826
|
+
|
827
|
+
describe "then pulling the lever" do
|
828
|
+
|
829
|
+
it "spins the icons" do
|
830
|
+
@pokie.pull_lever!
|
831
|
+
@pokie.display.should == [:spinning, :spinning, :spinning]
|
832
|
+
end
|
833
|
+
|
834
|
+
it "takes away credits" do
|
835
|
+
credits_before_pulling_lever = @pokie.credits
|
836
|
+
@pokie.pull_lever!
|
837
|
+
@pokie.credits.should == credits_before_pulling_lever - PokerMachine::CREDITS_TO_PLAY
|
838
|
+
end
|
839
|
+
|
840
|
+
it "makes a silly noise" do
|
841
|
+
lambda { @pokie.pull_lever! }.should change(@pokie.silly_noises_inflicted, :length)
|
842
|
+
end
|
843
|
+
|
844
|
+
it "wont let you pull it again while it's still spinning" do
|
845
|
+
@pokie.pull_lever!
|
846
|
+
@pokie.spinning?.should be_true
|
847
|
+
@pokie.can_pull_lever?.should == nil
|
848
|
+
lambda{ @pokie.pull_lever! }.should raise_error(StateFu::IllegalTransition)
|
849
|
+
end
|
850
|
+
|
851
|
+
it "makes a spinning sound while you wait" do
|
852
|
+
@pokie.pull_lever!
|
853
|
+
noises_before = @pokie.silly_noises_inflicted
|
854
|
+
@pokie.wait
|
855
|
+
(@pokie.silly_noises_inflicted).should include(:spinning_noise)
|
856
|
+
end
|
857
|
+
|
858
|
+
it "it stops spinning after a little #wait" do
|
859
|
+
@pokie.pull_lever!
|
860
|
+
@pokie.wait
|
861
|
+
@pokie.spinning?.should be_false
|
862
|
+
end
|
863
|
+
|
864
|
+
it "gives you more credits if all the icons are the same" do
|
865
|
+
@pokie.pull_lever!
|
866
|
+
@pokie.wheel_one.stop! :smiley
|
867
|
+
@pokie.wheel_two.stop! :smiley
|
868
|
+
@pokie.wheel_three.stop! :smiley
|
869
|
+
@pokie.wait
|
870
|
+
@pokie.credits.should == 105
|
871
|
+
end
|
872
|
+
end
|
873
|
+
end
|
874
|
+
end
|
875
|
+
|
876
|
+
describe "Chameleon" do
|
877
|
+
|
878
|
+
before do
|
879
|
+
make_pristine_class('Chameleon') do
|
880
|
+
|
881
|
+
machine :location do
|
882
|
+
initial_state :outside
|
883
|
+
|
884
|
+
event :go_outside, :from => {:inside => :outside}
|
885
|
+
event :go_inside, :from => {:outside => :inside}
|
886
|
+
end
|
887
|
+
|
888
|
+
machine :skin do
|
889
|
+
initial_state :green
|
890
|
+
|
891
|
+
states :plaid, :paisley, :tartan, :location => :indoors
|
892
|
+
states :bark, :pebbles, :foliage, :location => :outdoors
|
893
|
+
|
894
|
+
define :change_according_to_surroundings? do |transition|
|
895
|
+
if transition.cycle?
|
896
|
+
false
|
897
|
+
else
|
898
|
+
case transition.target[:location]
|
899
|
+
when :indoors
|
900
|
+
inside?
|
901
|
+
when :outdoors
|
902
|
+
outside?
|
903
|
+
else
|
904
|
+
true
|
905
|
+
end
|
906
|
+
end
|
907
|
+
end
|
908
|
+
|
909
|
+
event :camoflage, :from => states.all, :to => states.all do
|
910
|
+
requires :change_according_to_surroundings?, :message => lambda { |t| "It's no good looking like #{t.target.name} when you're #{t.object.location.current_state_name}!"}
|
911
|
+
end
|
912
|
+
|
913
|
+
end
|
914
|
+
end # Chameleon
|
915
|
+
end # before
|
916
|
+
|
917
|
+
describe "changing its skin" do
|
918
|
+
before do
|
919
|
+
@chameleon = Chameleon.new
|
920
|
+
end
|
921
|
+
|
922
|
+
it "should change its skin according to its surroundings" do
|
923
|
+
@chameleon.current_state(:location).should == :outside
|
924
|
+
@chameleon.current_state(:skin).should == :green
|
925
|
+
|
926
|
+
@chameleon.outside?.should == true
|
927
|
+
|
928
|
+
@chameleon.skin.valid_transitions.targets.names.should == [:bark, :pebbles, :foliage]
|
929
|
+
@chameleon.camoflage!(:bark)
|
930
|
+
@chameleon.skin.should == :bark
|
931
|
+
|
932
|
+
@chameleon.go_inside!
|
933
|
+
@chameleon.skin.valid_transitions.targets.names.should == [:green, :plaid, :paisley, :tartan]
|
934
|
+
|
935
|
+
@chameleon.camoflage!(:tartan)
|
936
|
+
@chameleon.skin.should == :tartan
|
937
|
+
|
938
|
+
@chameleon.camoflage!(:green)
|
939
|
+
@chameleon.skin.should == :green
|
940
|
+
|
941
|
+
lambda { @chameleon.camoflage!(:bark) }.should raise_error(StateFu::RequirementError)
|
942
|
+
@chameleon.camoflage(:bark).error_messages.should == ["It's no good looking like bark when you're inside!"]
|
943
|
+
end
|
944
|
+
|
945
|
+
end
|
946
|
+
end
|
947
|
+
|
948
|
+
|