alter-ego 1.0.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.tar.gz.sig +0 -0
- data/History.txt +4 -0
- data/Manifest.txt +19 -0
- data/PostInstall.txt +7 -0
- data/README.rdoc +149 -0
- data/Rakefile +31 -0
- data/State_Design_Pattern_UML_Class_Diagram.png +0 -0
- data/TODO +10 -0
- data/lib/alter_ego.rb +381 -0
- data/lib/assertions.rb +63 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/alter_ego_spec.rb +1051 -0
- data/spec/assertions_spec.rb +284 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/tasks/rspec.rake +21 -0
- metadata +123 -0
- metadata.gz.sig +2 -0
data/lib/assertions.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
class AssertionFailureError < Exception
|
2
|
+
end
|
3
|
+
|
4
|
+
module Assertions
|
5
|
+
|
6
|
+
# Assert that no +values+ are nil or false. Returns the last value.
|
7
|
+
def assert(*values, &block)
|
8
|
+
iterate_and_return_last(values, block) do |v|
|
9
|
+
raise_assertion_error unless v
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# The opposite of #assert.
|
14
|
+
def deny(*values)
|
15
|
+
assert(*values.map{ |v| !v})
|
16
|
+
assert(yield(*values)) if block_given?
|
17
|
+
values.last
|
18
|
+
end
|
19
|
+
|
20
|
+
# Assert that no +values+ are nil. Returns the last value.
|
21
|
+
def assert_exists(*values, &block)
|
22
|
+
iterate_and_return_last(values, block) { |value| deny(value.nil?) }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Assert that +values+ are collections that contain at least one element.
|
26
|
+
# Returns the last value.
|
27
|
+
def assert_one_or_more(*values, &block)
|
28
|
+
iterate_and_return_last(values, block) do |value|
|
29
|
+
assert_exists(value)
|
30
|
+
deny(value.kind_of?(String))
|
31
|
+
deny(value.empty?)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def assert_keys(hash, *keys)
|
36
|
+
assert_exists(hash)
|
37
|
+
assert(hash.respond_to?(:[]))
|
38
|
+
values = keys.inject([]) { |vals, k| vals << assert_exists(hash[k]) }
|
39
|
+
assert(yield(*values)) if block_given?
|
40
|
+
hash
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def iterate_and_return_last(values, block = nil)
|
46
|
+
values.each { |v| yield(v) }
|
47
|
+
if block
|
48
|
+
raise_assertion_error unless block.call(*values)
|
49
|
+
end
|
50
|
+
values.last
|
51
|
+
end
|
52
|
+
|
53
|
+
def raise_assertion_error
|
54
|
+
error = AssertionFailureError.new
|
55
|
+
backtrace = caller
|
56
|
+
trimmed_backtrace = []
|
57
|
+
trimmed_backtrace.unshift(backtrace.pop) until
|
58
|
+
backtrace.last.include?(__FILE__)
|
59
|
+
error.set_backtrace(trimmed_backtrace)
|
60
|
+
raise error
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# File: script/console
|
3
|
+
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
|
4
|
+
|
5
|
+
libs = " -r irb/completion"
|
6
|
+
# Perhaps use a console_lib to store any extra methods I may want available in the cosole
|
7
|
+
# libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
|
8
|
+
libs << " -r #{File.dirname(__FILE__) + '/../lib/alter_ego.rb'}"
|
9
|
+
puts "Loading AlterEgo gem"
|
10
|
+
exec "#{irb} #{libs} --simple-prompt"
|
data/script/destroy
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/destroy'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Destroy.new.run(ARGV)
|
data/script/generate
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/generate'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Generate.new.run(ARGV)
|
@@ -0,0 +1,1051 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require File.expand_path('spec_helper', File.dirname(__FILE__))
|
3
|
+
|
4
|
+
# let's define a traffic light class with three states: proceed, caution, and
|
5
|
+
# stop. We'll leave the DSL for later, and use old-school class definitions to
|
6
|
+
# start out.
|
7
|
+
|
8
|
+
class TrafficLightWithClassicStates
|
9
|
+
include AlterEgo
|
10
|
+
|
11
|
+
class ProceedState < State
|
12
|
+
end
|
13
|
+
|
14
|
+
class CautionState < State
|
15
|
+
end
|
16
|
+
|
17
|
+
class StopState < State
|
18
|
+
end
|
19
|
+
|
20
|
+
add_state(ProceedState)
|
21
|
+
add_state(CautionState)
|
22
|
+
add_state(StopState)
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
describe TrafficLightWithClassicStates do
|
27
|
+
before :each do
|
28
|
+
@it = TrafficLightWithClassicStates
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should have the specified states" do
|
32
|
+
@it.states.values.should include(TrafficLightWithClassicStates::ProceedState)
|
33
|
+
@it.states.values.should include(TrafficLightWithClassicStates::CautionState)
|
34
|
+
@it.states.values.should include(TrafficLightWithClassicStates::StopState)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Before we go any further, we'll define some identifiers for our states. This
|
39
|
+
# will make them easier to work with.
|
40
|
+
|
41
|
+
class TrafficLightWithIdentifiers
|
42
|
+
include AlterEgo
|
43
|
+
|
44
|
+
class ProceedState < State
|
45
|
+
def self.identifier; :proceed; end
|
46
|
+
end
|
47
|
+
|
48
|
+
class CautionState < State
|
49
|
+
def self.identifier; :caution; end
|
50
|
+
end
|
51
|
+
|
52
|
+
class StopState < State
|
53
|
+
def self.identifier; :stop; end
|
54
|
+
end
|
55
|
+
|
56
|
+
add_state(ProceedState)
|
57
|
+
add_state(CautionState)
|
58
|
+
add_state(StopState)
|
59
|
+
|
60
|
+
def initialize(starting_state = :proceed)
|
61
|
+
self.state=(starting_state)
|
62
|
+
end
|
63
|
+
|
64
|
+
def cycle
|
65
|
+
case current_state.identifier
|
66
|
+
when :proceed then transition_to(:caution)
|
67
|
+
when :caution then transition_to(:stop)
|
68
|
+
when :stop then transition_to(:proceed)
|
69
|
+
else raise "Should never get here"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "a green light", :shared => true do
|
75
|
+
it "should be in 'proceed' state" do
|
76
|
+
@it.current_state.should == :proceed
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should change to the caution (yellow) state on cycle" do
|
80
|
+
@it.cycle
|
81
|
+
@it.current_state.should == :caution
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "a yellow light", :shared => true do
|
86
|
+
it "should be in 'caution' state" do
|
87
|
+
@it.current_state.should == :caution
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should change to stop (red) on cycle" do
|
91
|
+
@it.cycle
|
92
|
+
@it.current_state.should == :stop
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe "a red light", :shared => true do
|
97
|
+
it "should be in 'stop' state" do
|
98
|
+
@it.current_state.should == :stop
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should change to proceed (green) on cycle" do
|
102
|
+
@it.cycle
|
103
|
+
@it.current_state.should == :proceed
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe TrafficLightWithIdentifiers, "by default" do
|
108
|
+
before :each do
|
109
|
+
@it = TrafficLightWithIdentifiers.new
|
110
|
+
end
|
111
|
+
|
112
|
+
it_should_behave_like "a green light"
|
113
|
+
end
|
114
|
+
|
115
|
+
describe TrafficLightWithIdentifiers, "when yellow" do
|
116
|
+
before :each do
|
117
|
+
@it = TrafficLightWithIdentifiers.new(:caution)
|
118
|
+
end
|
119
|
+
|
120
|
+
it_should_behave_like "a yellow light"
|
121
|
+
end
|
122
|
+
|
123
|
+
describe TrafficLightWithIdentifiers, "when red" do
|
124
|
+
before :each do
|
125
|
+
@it = TrafficLightWithIdentifiers.new(:stop)
|
126
|
+
end
|
127
|
+
|
128
|
+
it_should_behave_like "a red light"
|
129
|
+
end
|
130
|
+
|
131
|
+
# Being able to go from one state to another isn't that big a deal. Let's add
|
132
|
+
# some state-specific behaviour.
|
133
|
+
|
134
|
+
class TrafficLightWithColors
|
135
|
+
include AlterEgo
|
136
|
+
|
137
|
+
class ProceedState < State
|
138
|
+
def self.identifier
|
139
|
+
:proceed
|
140
|
+
end
|
141
|
+
def color(traffic_light)
|
142
|
+
"green"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class CautionState < State
|
147
|
+
def self.identifier
|
148
|
+
:caution
|
149
|
+
end
|
150
|
+
def color(traffic_light)
|
151
|
+
"yellow"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class StopState < State
|
156
|
+
def self.identifier
|
157
|
+
:stop
|
158
|
+
end
|
159
|
+
def color(traffic_light)
|
160
|
+
"red"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
add_state(ProceedState)
|
165
|
+
add_state(CautionState)
|
166
|
+
add_state(StopState)
|
167
|
+
|
168
|
+
def initialize(starting_state = :proceed)
|
169
|
+
self.state=(starting_state)
|
170
|
+
end
|
171
|
+
|
172
|
+
def cycle
|
173
|
+
case current_state.identifier
|
174
|
+
when :proceed then transition_to(:caution)
|
175
|
+
when :caution then transition_to(:stop)
|
176
|
+
when :stop then transition_to(:proceed)
|
177
|
+
else raise "Should never get here"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe "a green light with color", :shared => true do
|
183
|
+
it_should_behave_like "a green light"
|
184
|
+
it "should have color green" do
|
185
|
+
@it.color.should == "green"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
describe "a yellow light with color", :shared => true do
|
190
|
+
it_should_behave_like "a yellow light"
|
191
|
+
it "should have color yellow" do
|
192
|
+
@it.color.should == "yellow"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
describe "a red light with color", :shared => true do
|
197
|
+
it_should_behave_like "a red light"
|
198
|
+
it "should have color red" do
|
199
|
+
@it.color.should == "red"
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
describe TrafficLightWithColors, "when green" do
|
204
|
+
before :each do
|
205
|
+
@it = TrafficLightWithColors.new(:proceed)
|
206
|
+
end
|
207
|
+
it_should_behave_like "a green light with color"
|
208
|
+
end
|
209
|
+
|
210
|
+
describe TrafficLightWithColors, "when yellow" do
|
211
|
+
before :each do
|
212
|
+
@it = TrafficLightWithColors.new(:caution)
|
213
|
+
end
|
214
|
+
it_should_behave_like "a yellow light with color"
|
215
|
+
end
|
216
|
+
|
217
|
+
describe TrafficLightWithColors, "when red" do
|
218
|
+
before :each do
|
219
|
+
@it = TrafficLightWithColors.new(:stop)
|
220
|
+
end
|
221
|
+
it_should_behave_like "a red light with color"
|
222
|
+
end
|
223
|
+
|
224
|
+
# This is all very verbose. Now that we have a feel for the object model, let's
|
225
|
+
# introduce the DSL syntax. Notice that the identifier becomes an argument to
|
226
|
+
# the 'state' declaration, and the #color methods become "handlers". Also note
|
227
|
+
# there is no longer any need for an explicit #add_state call.
|
228
|
+
|
229
|
+
class TrafficLightDescribedByDsl
|
230
|
+
include AlterEgo
|
231
|
+
|
232
|
+
state :proceed do
|
233
|
+
handle :color do
|
234
|
+
"green"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
state :caution do
|
239
|
+
handle :color do
|
240
|
+
"yellow"
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
state :stop do
|
245
|
+
handle :color do
|
246
|
+
"red"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def initialize(starting_state = :proceed)
|
251
|
+
self.state=(starting_state)
|
252
|
+
end
|
253
|
+
|
254
|
+
def cycle
|
255
|
+
case current_state.identifier
|
256
|
+
when :proceed then transition_to(:caution)
|
257
|
+
when :caution then transition_to(:stop)
|
258
|
+
when :stop then transition_to(:proceed)
|
259
|
+
else raise "Should never get here"
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
describe TrafficLightDescribedByDsl, "when green" do
|
265
|
+
before :each do
|
266
|
+
@it = TrafficLightDescribedByDsl.new(:proceed)
|
267
|
+
end
|
268
|
+
|
269
|
+
it_should_behave_like "a green light with color"
|
270
|
+
end
|
271
|
+
|
272
|
+
describe TrafficLightDescribedByDsl, "when yellow" do
|
273
|
+
before :each do
|
274
|
+
@it = TrafficLightDescribedByDsl.new(:caution)
|
275
|
+
end
|
276
|
+
|
277
|
+
it_should_behave_like "a yellow light with color"
|
278
|
+
end
|
279
|
+
|
280
|
+
describe TrafficLightDescribedByDsl, "when red" do
|
281
|
+
before :each do
|
282
|
+
@it = TrafficLightDescribedByDsl.new(:stop)
|
283
|
+
end
|
284
|
+
|
285
|
+
it_should_behave_like "a red light with color"
|
286
|
+
end
|
287
|
+
|
288
|
+
# Let's redefine #cycle to be just another handler. Note that when defined
|
289
|
+
# with the 'handler' syntax, handler blocks are executed in the context of the
|
290
|
+
# context object, that is, the object which has a state.
|
291
|
+
|
292
|
+
class TrafficLightWithCycleHandler
|
293
|
+
include AlterEgo
|
294
|
+
|
295
|
+
state :proceed do
|
296
|
+
handle :color do
|
297
|
+
"green"
|
298
|
+
end
|
299
|
+
handle :cycle do
|
300
|
+
transition_to(:caution)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
state :caution do
|
305
|
+
handle :color do
|
306
|
+
"yellow"
|
307
|
+
end
|
308
|
+
handle :cycle do
|
309
|
+
transition_to(:stop)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
state :stop do
|
314
|
+
handle :color do
|
315
|
+
"red"
|
316
|
+
end
|
317
|
+
handle :cycle do
|
318
|
+
transition_to(:proceed)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def initialize(starting_state = :proceed)
|
323
|
+
self.state=(starting_state)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
describe TrafficLightWithCycleHandler, "when green" do
|
328
|
+
before :each do
|
329
|
+
@it = TrafficLightWithCycleHandler.new(:proceed)
|
330
|
+
end
|
331
|
+
|
332
|
+
it_should_behave_like "a green light with color"
|
333
|
+
end
|
334
|
+
|
335
|
+
describe TrafficLightWithCycleHandler, "when yellow" do
|
336
|
+
before :each do
|
337
|
+
@it = TrafficLightWithCycleHandler.new(:caution)
|
338
|
+
end
|
339
|
+
|
340
|
+
it_should_behave_like "a yellow light with color"
|
341
|
+
end
|
342
|
+
|
343
|
+
describe TrafficLightWithCycleHandler, "when red" do
|
344
|
+
before :each do
|
345
|
+
@it = TrafficLightWithCycleHandler.new(:stop)
|
346
|
+
end
|
347
|
+
|
348
|
+
it_should_behave_like "a red light with color"
|
349
|
+
end
|
350
|
+
|
351
|
+
# In fact, the pattern of a handler which executes a state transition is common
|
352
|
+
# enough that there is a special syntax for it. Let's convert to using that
|
353
|
+
# syntax.
|
354
|
+
|
355
|
+
# While we're at it, we'll also add a :default keyword to the :green state, and
|
356
|
+
# eliminate the initializer.
|
357
|
+
|
358
|
+
class TrafficLightWithTransitions
|
359
|
+
include AlterEgo
|
360
|
+
|
361
|
+
state :proceed, :default => true do
|
362
|
+
handle :color do
|
363
|
+
"green"
|
364
|
+
end
|
365
|
+
transition :to => :caution, :on => :cycle
|
366
|
+
end
|
367
|
+
|
368
|
+
state :caution do
|
369
|
+
handle :color do
|
370
|
+
"yellow"
|
371
|
+
end
|
372
|
+
transition :to => :stop, :on => :cycle
|
373
|
+
end
|
374
|
+
|
375
|
+
state :stop do
|
376
|
+
handle :color do
|
377
|
+
"red"
|
378
|
+
end
|
379
|
+
transition :to => :proceed, :on => :cycle
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
describe TrafficLightWithTransitions, "by default" do
|
384
|
+
before :each do
|
385
|
+
@it = TrafficLightWithTransitions.new
|
386
|
+
end
|
387
|
+
|
388
|
+
it "should be in the green state" do
|
389
|
+
@it.current_state.should == :proceed
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
describe TrafficLightWithTransitions, "when green" do
|
394
|
+
before :each do
|
395
|
+
@it = TrafficLightWithTransitions.new
|
396
|
+
end
|
397
|
+
|
398
|
+
it_should_behave_like "a green light with color"
|
399
|
+
end
|
400
|
+
|
401
|
+
describe TrafficLightWithTransitions, "when yellow" do
|
402
|
+
before :each do
|
403
|
+
@it = TrafficLightWithTransitions.new
|
404
|
+
@it.cycle
|
405
|
+
end
|
406
|
+
|
407
|
+
it_should_behave_like "a yellow light with color"
|
408
|
+
end
|
409
|
+
|
410
|
+
describe TrafficLightWithTransitions, "when red" do
|
411
|
+
before :each do
|
412
|
+
@it = TrafficLightWithTransitions.new
|
413
|
+
@it.cycle
|
414
|
+
@it.cycle
|
415
|
+
end
|
416
|
+
|
417
|
+
it_should_behave_like "a red light with color"
|
418
|
+
end
|
419
|
+
|
420
|
+
# It is possible to have only some of the states handle a given request. If the
|
421
|
+
# method is called while the object is in a state which doesn't handle it, a
|
422
|
+
# WrongStateError will be raised.
|
423
|
+
#
|
424
|
+
# Let's add a #seconds_till_red method to our traffic light, so that it can show
|
425
|
+
# a countdown letting motorists know exactly how long they have until the light
|
426
|
+
# turns red. Let's say for the sake of example that it will only be valid to
|
427
|
+
# call this method when the light is yellow.
|
428
|
+
|
429
|
+
class TrafficLightWithRedCountdown
|
430
|
+
include AlterEgo
|
431
|
+
|
432
|
+
state :proceed, :default => true do
|
433
|
+
handle :color do
|
434
|
+
"green"
|
435
|
+
end
|
436
|
+
transition :to => :caution, :on => :cycle
|
437
|
+
end
|
438
|
+
|
439
|
+
state :caution do
|
440
|
+
handle :color do
|
441
|
+
"yellow"
|
442
|
+
end
|
443
|
+
handle :seconds_till_red do
|
444
|
+
# ...
|
445
|
+
end
|
446
|
+
transition :to => :stop, :on => :cycle
|
447
|
+
end
|
448
|
+
|
449
|
+
state :stop do
|
450
|
+
handle :color do
|
451
|
+
"red"
|
452
|
+
end
|
453
|
+
transition :to => :proceed, :on => :cycle
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
describe TrafficLightWithRedCountdown, "that is green" do
|
458
|
+
before :each do
|
459
|
+
@it = TrafficLightWithRedCountdown.new
|
460
|
+
end
|
461
|
+
|
462
|
+
it "should raise an error if #seconds_till_red is called" do
|
463
|
+
lambda do
|
464
|
+
@it.seconds_till_red
|
465
|
+
end.should raise_error(AlterEgo::WrongStateError)
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
describe TrafficLightWithRedCountdown, "that is yellow" do
|
470
|
+
before :each do
|
471
|
+
@it = TrafficLightWithRedCountdown.new
|
472
|
+
@it.cycle
|
473
|
+
end
|
474
|
+
|
475
|
+
it "should raise an error if #seconds_till_red is called" do
|
476
|
+
lambda do
|
477
|
+
@it.seconds_till_red
|
478
|
+
end.should_not raise_error
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
# It is possible to get a list of currently handled requests, as well as a list
|
483
|
+
# of all possible requests supported in any state.
|
484
|
+
|
485
|
+
describe TrafficLightWithRedCountdown do
|
486
|
+
before :each do
|
487
|
+
@it = TrafficLightWithRedCountdown.new
|
488
|
+
end
|
489
|
+
|
490
|
+
it "should know what requests are supported by states" do
|
491
|
+
@it.all_handled_requests.should include(:cycle, :color, :seconds_till_red)
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
# The customer has decided the traffic light must sound an audible alert
|
496
|
+
# while in the yellow state, in order to warn vision-impaired pedestrians.
|
497
|
+
#
|
498
|
+
# In order to accomodate this requirement, we will use on_enter and on_exit
|
499
|
+
# handlers to switch an alarm on and off.
|
500
|
+
|
501
|
+
class TrafficLightWithAlarm
|
502
|
+
include AlterEgo
|
503
|
+
|
504
|
+
state :proceed, :default => true do
|
505
|
+
handle :color do
|
506
|
+
"green"
|
507
|
+
end
|
508
|
+
transition :to => :caution, :on => :cycle
|
509
|
+
end
|
510
|
+
|
511
|
+
state :caution do
|
512
|
+
on_enter do
|
513
|
+
turn_on_alarm
|
514
|
+
end
|
515
|
+
on_exit do
|
516
|
+
turn_off_alarm
|
517
|
+
end
|
518
|
+
handle :color do
|
519
|
+
"yellow"
|
520
|
+
end
|
521
|
+
handle :seconds_till_red do
|
522
|
+
# ...
|
523
|
+
end
|
524
|
+
transition :to => :stop, :on => :cycle
|
525
|
+
end
|
526
|
+
|
527
|
+
state :stop do
|
528
|
+
handle :color do
|
529
|
+
"red"
|
530
|
+
end
|
531
|
+
transition :to => :proceed, :on => :cycle
|
532
|
+
end
|
533
|
+
|
534
|
+
def initialize(hardware_controller)
|
535
|
+
@hardware_controller = hardware_controller
|
536
|
+
end
|
537
|
+
|
538
|
+
def turn_on_alarm
|
539
|
+
@hardware_controller.alarm_enabled = true
|
540
|
+
end
|
541
|
+
|
542
|
+
def turn_off_alarm
|
543
|
+
@hardware_controller.alarm_enabled = false
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
describe TrafficLightWithAlarm do
|
548
|
+
it "should not include on_enter or on_exit in list of handlers" do
|
549
|
+
TrafficLightWithAlarm.all_handled_requests.should_not include(:on_enter)
|
550
|
+
TrafficLightWithAlarm.all_handled_requests.should_not include(:on_exit)
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
describe TrafficLightWithAlarm, "when green" do
|
555
|
+
before :each do
|
556
|
+
@hardware_controller = mock("Hardware Controller")
|
557
|
+
@it = TrafficLightWithAlarm.new(@hardware_controller)
|
558
|
+
end
|
559
|
+
|
560
|
+
it "should enable alarm on transition to yellow" do
|
561
|
+
@hardware_controller.should_receive(:alarm_enabled=).
|
562
|
+
with(true)
|
563
|
+
@it.cycle
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
describe TrafficLightWithAlarm, "when yellow" do
|
568
|
+
before :each do
|
569
|
+
@hardware_controller = stub("Hardware Controller", :alarm_enabled= => nil)
|
570
|
+
@it = TrafficLightWithAlarm.new(@hardware_controller)
|
571
|
+
@it.cycle
|
572
|
+
end
|
573
|
+
|
574
|
+
it "should disable alarm on transition to yellow" do
|
575
|
+
@hardware_controller.should_receive(:alarm_enabled=).
|
576
|
+
with(false)
|
577
|
+
@it.cycle
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
# For safety reasons, the light should not allow transitions faster than every
|
582
|
+
# twenty seconds. We'll add state guards to ensure this constraint is observed.
|
583
|
+
# We'll also add a generic state change action for all states to restart the timer
|
584
|
+
# each time the state changes.
|
585
|
+
|
586
|
+
class TrafficLightWithGuards
|
587
|
+
include AlterEgo
|
588
|
+
|
589
|
+
state :proceed, :default => true do
|
590
|
+
handle :color do
|
591
|
+
"green"
|
592
|
+
end
|
593
|
+
transition :to => :caution, :on => :cycle, :if => :min_time_elapsed?
|
594
|
+
end
|
595
|
+
|
596
|
+
state :caution do
|
597
|
+
on_enter do
|
598
|
+
turn_on_alarm
|
599
|
+
end
|
600
|
+
on_exit do
|
601
|
+
turn_off_alarm
|
602
|
+
end
|
603
|
+
handle :color do
|
604
|
+
"yellow"
|
605
|
+
end
|
606
|
+
handle :seconds_till_red do
|
607
|
+
# ...
|
608
|
+
end
|
609
|
+
transition :to => :stop, :on => :cycle, :if => :min_time_elapsed?
|
610
|
+
end
|
611
|
+
|
612
|
+
state :stop do
|
613
|
+
handle :color do
|
614
|
+
"red"
|
615
|
+
end
|
616
|
+
|
617
|
+
# Just to demonstrate that it is possible, we use a proc here instead of a
|
618
|
+
# symbol
|
619
|
+
transition(:to => :proceed, :on => :cycle, :if => proc { min_time_elapsed? })
|
620
|
+
end
|
621
|
+
|
622
|
+
# On state change
|
623
|
+
request_filter :state => any, :request => any, :new_state => not_nil do
|
624
|
+
@hardware_controller.restart_timer
|
625
|
+
end
|
626
|
+
|
627
|
+
def initialize(hardware_controller)
|
628
|
+
@hardware_controller = hardware_controller
|
629
|
+
end
|
630
|
+
|
631
|
+
def turn_on_alarm
|
632
|
+
@hardware_controller.alarm_enabled = true
|
633
|
+
end
|
634
|
+
|
635
|
+
def turn_off_alarm
|
636
|
+
@hardware_controller.alarm_enabled = false
|
637
|
+
end
|
638
|
+
|
639
|
+
def min_time_elapsed?
|
640
|
+
@hardware_controller.time_elapsed >= 20
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
describe TrafficLightWithGuards, "that is green" do
|
645
|
+
before :each do
|
646
|
+
@hardware_controller = stub("Hardware Controller",
|
647
|
+
:time_elapsed => 21,
|
648
|
+
:restart_timer => nil,
|
649
|
+
:alarm_enabled= => nil)
|
650
|
+
@it = TrafficLightWithGuards.new(@hardware_controller)
|
651
|
+
end
|
652
|
+
|
653
|
+
it "should check the hardware controller's #time_elapsed on cycle" do
|
654
|
+
@hardware_controller.should_receive(:time_elapsed).and_return(19)
|
655
|
+
@it.cycle
|
656
|
+
end
|
657
|
+
|
658
|
+
it "should fail to cycle if elapsed time < 20 seconds" do
|
659
|
+
@hardware_controller.stub!(:time_elapsed).and_return(19)
|
660
|
+
@it.cycle.should be_false
|
661
|
+
@it.current_state.should == :proceed
|
662
|
+
end
|
663
|
+
|
664
|
+
it "should cycle if elapsed time >= 20 seconds" do
|
665
|
+
@hardware_controller.stub!(:time_elapsed).and_return(20)
|
666
|
+
@it.cycle.should be_true
|
667
|
+
@it.current_state.should == :caution
|
668
|
+
end
|
669
|
+
|
670
|
+
it "should restart the timer on state change" do
|
671
|
+
@hardware_controller.should_receive(:restart_timer)
|
672
|
+
@it.cycle
|
673
|
+
end
|
674
|
+
|
675
|
+
it "should restart the timer on state change" do
|
676
|
+
@hardware_controller.should_receive(:restart_timer)
|
677
|
+
@it.cycle
|
678
|
+
end
|
679
|
+
end
|
680
|
+
|
681
|
+
describe TrafficLightWithGuards, "that is yellow" do
|
682
|
+
before :each do
|
683
|
+
@hardware_controller = stub("Hardware Controller",
|
684
|
+
:time_elapsed => 21,
|
685
|
+
:restart_timer => nil,
|
686
|
+
:alarm_enabled= => nil)
|
687
|
+
@it = TrafficLightWithGuards.new(@hardware_controller)
|
688
|
+
@it.cycle
|
689
|
+
end
|
690
|
+
|
691
|
+
it "should check the hardware controller's #time_elapsed on cycle" do
|
692
|
+
@hardware_controller.should_receive(:time_elapsed).and_return(19)
|
693
|
+
@it.cycle
|
694
|
+
end
|
695
|
+
|
696
|
+
it "should fail to cycle if elapsed time < 20 seconds" do
|
697
|
+
@hardware_controller.stub!(:time_elapsed).and_return(19)
|
698
|
+
@it.cycle.should be_false
|
699
|
+
end
|
700
|
+
|
701
|
+
it "should remain in :caution state if elapsed time < 20" do
|
702
|
+
@hardware_controller.stub!(:time_elapsed).and_return(19)
|
703
|
+
@it.cycle
|
704
|
+
@it.current_state.should == :caution
|
705
|
+
end
|
706
|
+
|
707
|
+
it "should cycle if elapsed time >= 20 seconds" do
|
708
|
+
@hardware_controller.stub!(:time_elapsed).and_return(20)
|
709
|
+
@it.cycle.should be_true
|
710
|
+
end
|
711
|
+
|
712
|
+
it "should cycle to :stop state if elapsed time >= 20 seconds" do
|
713
|
+
@hardware_controller.stub!(:time_elapsed).and_return(20)
|
714
|
+
@it.cycle
|
715
|
+
@it.current_state.should == :stop
|
716
|
+
end
|
717
|
+
|
718
|
+
it "should restart the timer on state change" do
|
719
|
+
|
720
|
+
@hardware_controller.should_receive(:restart_timer)
|
721
|
+
@it.cycle
|
722
|
+
end
|
723
|
+
end
|
724
|
+
|
725
|
+
describe TrafficLightWithGuards, "that is red" do
|
726
|
+
before :each do
|
727
|
+
@hardware_controller = stub("Hardware Controller",
|
728
|
+
:time_elapsed => 21,
|
729
|
+
:restart_timer => nil,
|
730
|
+
:alarm_enabled= => nil)
|
731
|
+
@it = TrafficLightWithGuards.new(@hardware_controller)
|
732
|
+
@it.cycle
|
733
|
+
@it.cycle
|
734
|
+
end
|
735
|
+
|
736
|
+
it "should fail to cycle if elapsed time < 20 seconds" do
|
737
|
+
@hardware_controller.stub!(:time_elapsed).and_return(19)
|
738
|
+
@it.cycle.should be_false
|
739
|
+
@it.current_state.should == :stop
|
740
|
+
end
|
741
|
+
|
742
|
+
it "should cycle if elapsed time >= 20 seconds" do
|
743
|
+
@hardware_controller.stub!(:time_elapsed).and_return(20)
|
744
|
+
@it.cycle.should be_true
|
745
|
+
@it.current_state.should == :proceed
|
746
|
+
end
|
747
|
+
|
748
|
+
end
|
749
|
+
|
750
|
+
# The traffic light controller actually stores it's current state as three
|
751
|
+
# discrete booleans, one for each light which should be either on or off. We'll
|
752
|
+
# customize the state saving and loading methods in order to support this
|
753
|
+
# arrangement.
|
754
|
+
|
755
|
+
class TrafficLightWithCustomStorage
|
756
|
+
include AlterEgo
|
757
|
+
|
758
|
+
state :proceed, :default => true do
|
759
|
+
handle :color do
|
760
|
+
"green"
|
761
|
+
end
|
762
|
+
transition :to => :caution, :on => :cycle
|
763
|
+
end
|
764
|
+
|
765
|
+
state :caution do
|
766
|
+
on_enter do
|
767
|
+
turn_on_alarm
|
768
|
+
end
|
769
|
+
on_exit do
|
770
|
+
turn_off_alarm
|
771
|
+
end
|
772
|
+
handle :color do
|
773
|
+
"yellow"
|
774
|
+
end
|
775
|
+
handle :seconds_till_red do
|
776
|
+
# ...
|
777
|
+
end
|
778
|
+
transition :to => :stop, :on => :cycle
|
779
|
+
end
|
780
|
+
|
781
|
+
state :stop do
|
782
|
+
handle :color do
|
783
|
+
"red"
|
784
|
+
end
|
785
|
+
transition :to => :proceed, :on => :cycle
|
786
|
+
end
|
787
|
+
|
788
|
+
def initialize(hardware_controller)
|
789
|
+
@hardware_controller = hardware_controller
|
790
|
+
end
|
791
|
+
|
792
|
+
def turn_on_alarm
|
793
|
+
@hardware_controller.alarm_enabled = true
|
794
|
+
end
|
795
|
+
|
796
|
+
def turn_off_alarm
|
797
|
+
@hardware_controller.alarm_enabled = false
|
798
|
+
end
|
799
|
+
|
800
|
+
def state
|
801
|
+
gyr = [
|
802
|
+
@hardware_controller.green,
|
803
|
+
@hardware_controller.yellow,
|
804
|
+
@hardware_controller.red
|
805
|
+
]
|
806
|
+
|
807
|
+
case gyr
|
808
|
+
when [true, false, false] then :proceed
|
809
|
+
when [false, true, false] then :caution
|
810
|
+
when [false, false, true] then :stop
|
811
|
+
else raise "Invalid state!"
|
812
|
+
end
|
813
|
+
end
|
814
|
+
|
815
|
+
def state=(value)
|
816
|
+
gyr = case value
|
817
|
+
when :proceed then [true, false, false]
|
818
|
+
when :caution then [false, true, false]
|
819
|
+
when :stop then [false, false, true]
|
820
|
+
end
|
821
|
+
@hardware_controller.green = gyr[0]
|
822
|
+
@hardware_controller.yellow = gyr[1]
|
823
|
+
@hardware_controller.red = gyr[2]
|
824
|
+
end
|
825
|
+
end
|
826
|
+
|
827
|
+
|
828
|
+
describe TrafficLightWithCustomStorage, "that is green" do
|
829
|
+
before :each do
|
830
|
+
@hardware_controller = OpenStruct.new( :time_elapsed => 21,
|
831
|
+
:restart_timer => nil,
|
832
|
+
:green => true,
|
833
|
+
:yellow => false,
|
834
|
+
:red => false)
|
835
|
+
@it = TrafficLightWithCustomStorage.new(@hardware_controller)
|
836
|
+
end
|
837
|
+
|
838
|
+
it "should be in the proceed state" do
|
839
|
+
@it.current_state.should == :proceed
|
840
|
+
end
|
841
|
+
|
842
|
+
it "should set lights for yellow on cycle" do
|
843
|
+
@it.cycle
|
844
|
+
@hardware_controller.green.should be_false
|
845
|
+
@hardware_controller.yellow.should be_true
|
846
|
+
@hardware_controller.red.should be_false
|
847
|
+
end
|
848
|
+
end
|
849
|
+
|
850
|
+
describe TrafficLightWithCustomStorage, "that is yellow" do
|
851
|
+
before :each do
|
852
|
+
@hardware_controller = OpenStruct.new( :time_elapsed => 21,
|
853
|
+
:restart_timer => nil,
|
854
|
+
:green => false,
|
855
|
+
:yellow => true,
|
856
|
+
:red => false)
|
857
|
+
@it = TrafficLightWithCustomStorage.new(@hardware_controller)
|
858
|
+
end
|
859
|
+
|
860
|
+
it "should be in the caution state" do
|
861
|
+
@it.current_state.should == :caution
|
862
|
+
end
|
863
|
+
|
864
|
+
it "should set lights for red on cycle" do
|
865
|
+
@it.cycle
|
866
|
+
@hardware_controller.green.should be_false
|
867
|
+
@hardware_controller.yellow.should be_false
|
868
|
+
@hardware_controller.red.should be_true
|
869
|
+
end
|
870
|
+
|
871
|
+
end
|
872
|
+
|
873
|
+
describe TrafficLightWithCustomStorage, "that is red" do
|
874
|
+
before :each do
|
875
|
+
@hardware_controller = OpenStruct.new( :time_elapsed => 21,
|
876
|
+
:restart_timer => nil,
|
877
|
+
:green => false,
|
878
|
+
:yellow => false,
|
879
|
+
:red => true)
|
880
|
+
@it = TrafficLightWithCustomStorage.new(@hardware_controller)
|
881
|
+
end
|
882
|
+
|
883
|
+
it "should be in the stop state" do
|
884
|
+
@it.current_state.should == :stop
|
885
|
+
end
|
886
|
+
|
887
|
+
it "should set lights for green on cycle" do
|
888
|
+
@it.cycle
|
889
|
+
@hardware_controller.green.should be_true
|
890
|
+
@hardware_controller.yellow.should be_false
|
891
|
+
@hardware_controller.red.should be_false
|
892
|
+
end
|
893
|
+
|
894
|
+
end
|
895
|
+
|
896
|
+
# In order to integrate with a pedestrian traffic light, our light needs to send
|
897
|
+
# a signal whenever it changes. We'll add a transition action to handle this.
|
898
|
+
#
|
899
|
+
# It also needs to flash a strobe when transitioning to yellow or red. We'll
|
900
|
+
# use a request filter and a state matching pattern to accomplish this.
|
901
|
+
|
902
|
+
class TrafficLightWithTransAction
|
903
|
+
include AlterEgo
|
904
|
+
|
905
|
+
state :proceed, :default => true do
|
906
|
+
handle :color do
|
907
|
+
"green"
|
908
|
+
end
|
909
|
+
transition :to => :caution, :on => :cycle do
|
910
|
+
@hardware_controller.notify(:yellow)
|
911
|
+
end
|
912
|
+
end
|
913
|
+
|
914
|
+
state :caution do
|
915
|
+
on_enter do
|
916
|
+
turn_on_alarm
|
917
|
+
end
|
918
|
+
on_exit do
|
919
|
+
turn_off_alarm
|
920
|
+
end
|
921
|
+
handle :color do
|
922
|
+
"yellow"
|
923
|
+
end
|
924
|
+
handle :seconds_till_red do
|
925
|
+
# ...
|
926
|
+
end
|
927
|
+
transition :to => :stop, :on => :cycle do
|
928
|
+
@hardware_controller.notify(:red)
|
929
|
+
end
|
930
|
+
end
|
931
|
+
|
932
|
+
state :stop do
|
933
|
+
handle :color do
|
934
|
+
"red"
|
935
|
+
end
|
936
|
+
transition :to => :proceed, :on => :cycle do
|
937
|
+
@hardware_controller.notify(:green)
|
938
|
+
end
|
939
|
+
end
|
940
|
+
|
941
|
+
request_filter :state => any,
|
942
|
+
:request => any,
|
943
|
+
:new_state => [:caution, :stop] do
|
944
|
+
@hardware_controller.flash_strobe
|
945
|
+
end
|
946
|
+
|
947
|
+
def initialize(hardware_controller)
|
948
|
+
@hardware_controller = hardware_controller
|
949
|
+
end
|
950
|
+
|
951
|
+
def turn_on_alarm
|
952
|
+
@hardware_controller.alarm_enabled = true
|
953
|
+
end
|
954
|
+
|
955
|
+
def turn_off_alarm
|
956
|
+
@hardware_controller.alarm_enabled = false
|
957
|
+
end
|
958
|
+
|
959
|
+
def state
|
960
|
+
gyr = [
|
961
|
+
@hardware_controller.green,
|
962
|
+
@hardware_controller.yellow,
|
963
|
+
@hardware_controller.red
|
964
|
+
]
|
965
|
+
|
966
|
+
case gyr
|
967
|
+
when [true, false, false] then :proceed
|
968
|
+
when [false, true, false] then :caution
|
969
|
+
when [false, false, true] then :stop
|
970
|
+
else raise "Invalid state!"
|
971
|
+
end
|
972
|
+
end
|
973
|
+
|
974
|
+
def state=(value)
|
975
|
+
gyr = case value
|
976
|
+
when :proceed then [true, false, false]
|
977
|
+
when :caution then [false, true, false]
|
978
|
+
when :stop then [false, false, true]
|
979
|
+
end
|
980
|
+
@hardware_controller.green = gyr[0]
|
981
|
+
@hardware_controller.yellow = gyr[1]
|
982
|
+
@hardware_controller.red = gyr[2]
|
983
|
+
end
|
984
|
+
end
|
985
|
+
|
986
|
+
describe TrafficLightWithTransAction, "that is green" do
|
987
|
+
before :each do
|
988
|
+
@hardware_controller = OpenStruct.new( :time_elapsed => 21,
|
989
|
+
:restart_timer => nil,
|
990
|
+
:green => true,
|
991
|
+
:yellow => false,
|
992
|
+
:red => false)
|
993
|
+
@hardware_controller.stub!(:notify)
|
994
|
+
@it = TrafficLightWithTransAction.new(@hardware_controller)
|
995
|
+
end
|
996
|
+
|
997
|
+
it "should notify that it has turned yellow on cycle" do
|
998
|
+
@hardware_controller.should_receive(:notify).with(:yellow)
|
999
|
+
@it.cycle
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
it "should flash strobe on cycle to yellow" do
|
1003
|
+
@hardware_controller.should_receive(:flash_strobe)
|
1004
|
+
@it.cycle
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
|
1009
|
+
describe TrafficLightWithTransAction, "that is yellow" do
|
1010
|
+
before :each do
|
1011
|
+
@hardware_controller = OpenStruct.new( :time_elapsed => 21,
|
1012
|
+
:restart_timer => nil,
|
1013
|
+
:green => false,
|
1014
|
+
:yellow => true,
|
1015
|
+
:red => false)
|
1016
|
+
@hardware_controller.stub!(:notify)
|
1017
|
+
@it = TrafficLightWithTransAction.new(@hardware_controller)
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
it "should notify that it has turned red on cycle" do
|
1021
|
+
@hardware_controller.should_receive(:notify).with(:red)
|
1022
|
+
@it.cycle
|
1023
|
+
end
|
1024
|
+
|
1025
|
+
it "should flash strobe on cycle to red" do
|
1026
|
+
@hardware_controller.should_receive(:flash_strobe)
|
1027
|
+
@it.cycle
|
1028
|
+
end
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
describe TrafficLightWithTransAction, "that is red" do
|
1032
|
+
before :each do
|
1033
|
+
@hardware_controller = OpenStruct.new( :time_elapsed => 21,
|
1034
|
+
:restart_timer => nil,
|
1035
|
+
:green => false,
|
1036
|
+
:yellow => false,
|
1037
|
+
:red => true)
|
1038
|
+
@hardware_controller.stub!(:notify)
|
1039
|
+
@it = TrafficLightWithTransAction.new(@hardware_controller)
|
1040
|
+
end
|
1041
|
+
|
1042
|
+
it "should notify that it has turned green on cycle" do
|
1043
|
+
@hardware_controller.should_receive(:notify).with(:green)
|
1044
|
+
@it.cycle
|
1045
|
+
end
|
1046
|
+
|
1047
|
+
it "should not flash strobe on cycle to green" do
|
1048
|
+
@hardware_controller.should_not_receive(:flash_strobe)
|
1049
|
+
end
|
1050
|
+
|
1051
|
+
end
|