hsume2-state_machine 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. data/CHANGELOG.rdoc +413 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +717 -0
  4. data/Rakefile +77 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine.rb +448 -0
  28. data/lib/state_machine/alternate_machine.rb +79 -0
  29. data/lib/state_machine/assertions.rb +36 -0
  30. data/lib/state_machine/branch.rb +224 -0
  31. data/lib/state_machine/callback.rb +236 -0
  32. data/lib/state_machine/condition_proxy.rb +94 -0
  33. data/lib/state_machine/error.rb +13 -0
  34. data/lib/state_machine/eval_helpers.rb +86 -0
  35. data/lib/state_machine/event.rb +304 -0
  36. data/lib/state_machine/event_collection.rb +139 -0
  37. data/lib/state_machine/extensions.rb +149 -0
  38. data/lib/state_machine/initializers.rb +4 -0
  39. data/lib/state_machine/initializers/merb.rb +1 -0
  40. data/lib/state_machine/initializers/rails.rb +25 -0
  41. data/lib/state_machine/integrations.rb +110 -0
  42. data/lib/state_machine/integrations/active_model.rb +502 -0
  43. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  44. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  45. data/lib/state_machine/integrations/active_model/versions.rb +31 -0
  46. data/lib/state_machine/integrations/active_record.rb +424 -0
  47. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  48. data/lib/state_machine/integrations/active_record/versions.rb +143 -0
  49. data/lib/state_machine/integrations/base.rb +91 -0
  50. data/lib/state_machine/integrations/data_mapper.rb +392 -0
  51. data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
  52. data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
  53. data/lib/state_machine/integrations/mongo_mapper.rb +272 -0
  54. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  55. data/lib/state_machine/integrations/mongo_mapper/versions.rb +110 -0
  56. data/lib/state_machine/integrations/mongoid.rb +357 -0
  57. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  58. data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
  59. data/lib/state_machine/integrations/sequel.rb +428 -0
  60. data/lib/state_machine/integrations/sequel/versions.rb +36 -0
  61. data/lib/state_machine/machine.rb +1873 -0
  62. data/lib/state_machine/machine_collection.rb +87 -0
  63. data/lib/state_machine/matcher.rb +123 -0
  64. data/lib/state_machine/matcher_helpers.rb +54 -0
  65. data/lib/state_machine/node_collection.rb +157 -0
  66. data/lib/state_machine/path.rb +120 -0
  67. data/lib/state_machine/path_collection.rb +90 -0
  68. data/lib/state_machine/state.rb +271 -0
  69. data/lib/state_machine/state_collection.rb +112 -0
  70. data/lib/state_machine/transition.rb +458 -0
  71. data/lib/state_machine/transition_collection.rb +244 -0
  72. data/lib/tasks/state_machine.rake +1 -0
  73. data/lib/tasks/state_machine.rb +27 -0
  74. data/test/files/en.yml +17 -0
  75. data/test/files/switch.rb +11 -0
  76. data/test/functional/alternate_state_machine_test.rb +122 -0
  77. data/test/functional/state_machine_test.rb +993 -0
  78. data/test/test_helper.rb +4 -0
  79. data/test/unit/assertions_test.rb +40 -0
  80. data/test/unit/branch_test.rb +890 -0
  81. data/test/unit/callback_test.rb +701 -0
  82. data/test/unit/condition_proxy_test.rb +328 -0
  83. data/test/unit/error_test.rb +43 -0
  84. data/test/unit/eval_helpers_test.rb +222 -0
  85. data/test/unit/event_collection_test.rb +358 -0
  86. data/test/unit/event_test.rb +985 -0
  87. data/test/unit/integrations/active_model_test.rb +1097 -0
  88. data/test/unit/integrations/active_record_test.rb +2021 -0
  89. data/test/unit/integrations/base_test.rb +99 -0
  90. data/test/unit/integrations/data_mapper_test.rb +1909 -0
  91. data/test/unit/integrations/mongo_mapper_test.rb +1611 -0
  92. data/test/unit/integrations/mongoid_test.rb +1591 -0
  93. data/test/unit/integrations/sequel_test.rb +1523 -0
  94. data/test/unit/integrations_test.rb +61 -0
  95. data/test/unit/invalid_event_test.rb +20 -0
  96. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  97. data/test/unit/invalid_transition_test.rb +77 -0
  98. data/test/unit/machine_collection_test.rb +599 -0
  99. data/test/unit/machine_test.rb +3043 -0
  100. data/test/unit/matcher_helpers_test.rb +37 -0
  101. data/test/unit/matcher_test.rb +155 -0
  102. data/test/unit/node_collection_test.rb +217 -0
  103. data/test/unit/path_collection_test.rb +266 -0
  104. data/test/unit/path_test.rb +485 -0
  105. data/test/unit/state_collection_test.rb +310 -0
  106. data/test/unit/state_machine_test.rb +31 -0
  107. data/test/unit/state_test.rb +924 -0
  108. data/test/unit/transition_collection_test.rb +2102 -0
  109. data/test/unit/transition_test.rb +1541 -0
  110. metadata +207 -0
@@ -0,0 +1,244 @@
1
+ module StateMachine
2
+ # Represents a collection of transitions in a state machine
3
+ class TransitionCollection < Array
4
+ include Assertions
5
+
6
+ # Whether to skip running the action for each transition's machine
7
+ attr_reader :skip_actions
8
+
9
+ # Whether to skip running the after callbacks
10
+ attr_reader :skip_after
11
+
12
+ # Whether transitions should wrapped around a transaction block
13
+ attr_reader :use_transaction
14
+
15
+ # Creates a new collection of transitions that can be run in parallel. Each
16
+ # transition *must* be for a different attribute.
17
+ #
18
+ # Configuration options:
19
+ # * <tt>:actions</tt> - Whether to run the action configured for each transition
20
+ # * <tt>:after</tt> - Whether to run after callbacks
21
+ # * <tt>:transaction</tt> - Whether to wrap transitions within a transaction
22
+ def initialize(transitions = [], options = {})
23
+ super(transitions)
24
+
25
+ # Determine the validity of the transitions as a whole
26
+ @valid = all?
27
+ reject! {|transition| !transition}
28
+
29
+ attributes = map {|transition| transition.attribute}.uniq
30
+ raise ArgumentError, 'Cannot perform multiple transitions in parallel for the same state machine attribute' if attributes.length != length
31
+
32
+ assert_valid_keys(options, :actions, :after, :transaction)
33
+ options = {:actions => true, :after => true, :transaction => true}.merge(options)
34
+ @skip_actions = !options[:actions]
35
+ @skip_after = !options[:after]
36
+ @use_transaction = options[:transaction]
37
+ end
38
+
39
+ # Runs each of the collection's transitions in parallel.
40
+ #
41
+ # All transitions will run through the following steps:
42
+ # 1. Before callbacks
43
+ # 2. Persist state
44
+ # 3. Invoke action
45
+ # 4. After callbacks (if configured)
46
+ # 5. Rollback (if action is unsuccessful)
47
+ #
48
+ # If a block is passed to this method, that block will be called instead
49
+ # of invoking each transition's action.
50
+ def perform(&block)
51
+ reset
52
+
53
+ if valid?
54
+ if use_event_attributes? && !block_given?
55
+ each do |transition|
56
+ transition.transient = true
57
+ transition.machine.write(object, :event_transition, transition)
58
+ end
59
+
60
+ run_actions
61
+ else
62
+ within_transaction do
63
+ catch(:halt) { run_callbacks(&block) }
64
+ rollback unless success?
65
+ end
66
+ end
67
+ end
68
+
69
+ if actions.length == 1 && results.include?(actions.first)
70
+ results[actions.first]
71
+ else
72
+ success?
73
+ end
74
+ end
75
+
76
+ private
77
+ attr_reader :results #:nodoc:
78
+
79
+ # Is this a valid set of transitions? If the collection was creating with
80
+ # any +false+ values for transitions, then the the collection will be
81
+ # marked as invalid.
82
+ def valid?
83
+ @valid
84
+ end
85
+
86
+ # Did each transition perform successfully? This will only be true if the
87
+ # following requirements are met:
88
+ # * No +before+ callbacks halt
89
+ # * All actions run successfully (always true if skipping actions)
90
+ def success?
91
+ @success
92
+ end
93
+
94
+ # Gets the object being transitioned
95
+ def object
96
+ first.object
97
+ end
98
+
99
+ # Gets the list of actions to run. If configured to skip actions, then
100
+ # this will return an empty collection.
101
+ def actions
102
+ empty? ? [nil] : map {|transition| transition.action}.uniq
103
+ end
104
+
105
+ # Determines whether an event attribute be used to trigger the transitions
106
+ # in this collection or whether the transitions be run directly *outside*
107
+ # of the action.
108
+ def use_event_attributes?
109
+ !skip_actions && !skip_after && actions.all? && actions.length == 1 && first.machine.action_hook?
110
+ end
111
+
112
+ # Resets any information tracked from previous attempts to perform the
113
+ # collection
114
+ def reset
115
+ @results = {}
116
+ @success = false
117
+ end
118
+
119
+ # Runs each transition's callbacks recursively. Once all before callbacks
120
+ # have been executed, the transitions will then be persisted and the
121
+ # configured actions will be run.
122
+ #
123
+ # If any transition fails to run its callbacks, :halt will be thrown.
124
+ def run_callbacks(index = 0, &block)
125
+ if transition = self[index]
126
+ throw :halt unless transition.run_callbacks(:after => !skip_after) do
127
+ run_callbacks(index + 1, &block)
128
+ {:result => results[transition.action], :success => success?}
129
+ end
130
+ else
131
+ persist
132
+ run_actions(&block)
133
+ end
134
+ end
135
+
136
+ # Transitions the current value of the object's states to those specified by
137
+ # each transition
138
+ def persist
139
+ each {|transition| transition.persist}
140
+ end
141
+
142
+ # Runs the actions for each transition. If a block is given method, then it
143
+ # will be called instead of invoking each transition's action.
144
+ #
145
+ # The results of the actions will be used to determine #success?.
146
+ def run_actions
147
+ catch_exceptions do
148
+ @success = if block_given?
149
+ result = yield
150
+ actions.each {|action| results[action] = result}
151
+ !!result
152
+ else
153
+ actions.compact.each {|action| !skip_actions && results[action] = object.send(action)}
154
+ results.values.all?
155
+ end
156
+ end
157
+ end
158
+
159
+ # Rolls back changes made to the object's states via each transition
160
+ def rollback
161
+ each {|transition| transition.rollback}
162
+ end
163
+
164
+ # Wraps the given block with a rescue handler so that any exceptions that
165
+ # occur will automatically result in the transition rolling back any changes
166
+ # that were made to the object involved.
167
+ def catch_exceptions
168
+ begin
169
+ yield
170
+ rescue Exception
171
+ rollback
172
+ raise
173
+ end
174
+ end
175
+
176
+ # Runs a block within a transaction for the object being transitioned. If
177
+ # transactions are disabled, then this is a no-op.
178
+ def within_transaction
179
+ if use_transaction && !empty?
180
+ first.within_transaction do
181
+ yield
182
+ success?
183
+ end
184
+ else
185
+ yield
186
+ end
187
+ end
188
+ end
189
+
190
+ # Represents a collection of transitions that were generated from attribute-
191
+ # based events
192
+ class AttributeTransitionCollection < TransitionCollection
193
+ def initialize(transitions = [], options = {}) #:nodoc:
194
+ super(transitions, {:transaction => false, :actions => false}.merge(options))
195
+ end
196
+
197
+ private
198
+ # Hooks into running transition callbacks so that event / event transition
199
+ # attributes can be properly updated
200
+ def run_callbacks(index = 0)
201
+ if index == 0
202
+ # Clears any traces of the event attribute to prevent it from being
203
+ # evaluated multiple times if actions are nested
204
+ each do |transition|
205
+ transition.machine.write(object, :event, nil)
206
+ transition.machine.write(object, :event_transition, nil)
207
+ end
208
+
209
+ # Rollback only if exceptions occur during before callbacks
210
+ begin
211
+ super
212
+ rescue Exception
213
+ rollback unless @before_run
214
+ raise
215
+ end
216
+
217
+ # Persists transitions on the object if partial transition was successful.
218
+ # This allows us to reference them later to complete the transition with
219
+ # after callbacks.
220
+ each {|transition| transition.machine.write(object, :event_transition, transition)} if skip_after && success?
221
+ else
222
+ super
223
+ end
224
+ end
225
+
226
+ # Tracks that before callbacks have now completed
227
+ def persist
228
+ @before_run = true
229
+ super
230
+ end
231
+
232
+ # Resets callback tracking
233
+ def reset
234
+ super
235
+ @before_run = false
236
+ end
237
+
238
+ # Resets the event attribute so it can be re-evaluated if attempted again
239
+ def rollback
240
+ super
241
+ each {|transition| transition.machine.write(object, :event, transition.event) unless transition.transient?}
242
+ end
243
+ end
244
+ end
@@ -0,0 +1 @@
1
+ require File.join("#{File.dirname(__FILE__)}/state_machine")
@@ -0,0 +1,27 @@
1
+ namespace :state_machine do
2
+ desc 'Draws a set of state machines using GraphViz. Target files to load with FILE=x,y,z; Machine class with CLASS=x,y,z; Font name with FONT=x; Image format with FORMAT=x; Orientation with ORIENTATION=x'
3
+ task :draw do
4
+ if defined?(Rails)
5
+ Rake::Task['environment'].invoke
6
+ elsif defined?(Merb)
7
+ Rake::Task['merb_env'].invoke
8
+
9
+ # Fix ruby-graphviz being incompatible with Merb's process title
10
+ $0 = 'rake'
11
+ else
12
+ # Load the library
13
+ $:.unshift(File.dirname(__FILE__) + '/..')
14
+ require 'state_machine'
15
+ end
16
+
17
+ # Build drawing options
18
+ options = {}
19
+ options[:file] = ENV['FILE'] if ENV['FILE']
20
+ options[:path] = ENV['TARGET'] if ENV['TARGET']
21
+ options[:format] = ENV['FORMAT'] if ENV['FORMAT']
22
+ options[:font] = ENV['FONT'] if ENV['FONT']
23
+ options[:orientation] = ENV['ORIENTATION'] if ENV['ORIENTATION']
24
+
25
+ StateMachine::Machine.draw(ENV['CLASS'], options)
26
+ end
27
+ end
data/test/files/en.yml ADDED
@@ -0,0 +1,17 @@
1
+ en:
2
+ activerecord:
3
+ errors:
4
+ messages:
5
+ invalid_transition: "cannot transition"
6
+ activemodel:
7
+ errors:
8
+ messages:
9
+ invalid_transition: "cannot %{event}"
10
+ mongoid:
11
+ errors:
12
+ messages:
13
+ invalid_transition: "cannot transition"
14
+ mongo_mapper:
15
+ errors:
16
+ messages:
17
+ invalid_transition: "cannot transition"
@@ -0,0 +1,11 @@
1
+ class Switch
2
+ state_machine do
3
+ event :turn_on do
4
+ transition all => :on
5
+ end
6
+
7
+ event :turn_off do
8
+ transition all => :off
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,122 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ class AlternateAutoShop
4
+ attr_accessor :num_customers
5
+ attr_accessor :weekend
6
+ attr_accessor :have_parts
7
+
8
+ def initialize
9
+ @num_customers = 0
10
+ @weekend = false
11
+ @have_parts = true
12
+ super
13
+ end
14
+
15
+ state_machine :initial => :available, :syntax => :alternate do
16
+ after_transition :available => any, :do => :increment_customers
17
+ after_transition :busy => any, :do => :decrement_customers
18
+
19
+ state :available do
20
+ event :tow_vehicle, :to => :busy, :unless => :weekend?
21
+ end
22
+
23
+ state :busy do
24
+ event :fix_vehicle, :to => :available, :if => :have_parts?
25
+ end
26
+ end
27
+
28
+ def weekend?
29
+ !!weekend
30
+ end
31
+
32
+ def have_parts?
33
+ !!have_parts
34
+ end
35
+
36
+ # Increments the number of customers in service
37
+ def increment_customers
38
+ self.num_customers += 1
39
+ end
40
+
41
+ # Decrements the number of customers in service
42
+ def decrement_customers
43
+ self.num_customers -= 1
44
+ end
45
+ end
46
+
47
+ class AlternateAutoShopAvailableTest < Test::Unit::TestCase
48
+ def setup
49
+ @auto_shop = AlternateAutoShop.new
50
+ end
51
+
52
+ def test_should_be_in_available_state
53
+ assert_equal 'available', @auto_shop.state
54
+ end
55
+
56
+ def test_should_allow_tow_vehicle
57
+ assert @auto_shop.tow_vehicle
58
+ end
59
+
60
+ def test_should_allow_tow_vehicle_on_weekends
61
+ @auto_shop.weekend = true
62
+ assert !@auto_shop.tow_vehicle
63
+ end
64
+
65
+ def test_should_not_allow_fix_vehicle
66
+ assert !@auto_shop.fix_vehicle
67
+ end
68
+
69
+ def test_should_append_to_machine
70
+ AlternateAutoShop.class_eval do
71
+ state_machine :initial => :available, :syntax => :alternate do
72
+ state any do
73
+ event :close, :to => :closed
74
+ end
75
+ end
76
+ end
77
+
78
+ assert @auto_shop.close
79
+ assert @auto_shop.closed?
80
+ end
81
+
82
+ def test_should_not_allow_event_outside_state
83
+ assert_raises(StateMachine::AlternateMachine::InvalidEventError) do
84
+ AlternateAutoShop.class_eval do
85
+ state_machine :initial => :available, :syntax => :alternate do
86
+ state any do
87
+ end
88
+
89
+ event :not_work, :to => :never
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ class AlternateAutoShopBusyTest < Test::Unit::TestCase
97
+ def setup
98
+ @auto_shop = AlternateAutoShop.new
99
+ @auto_shop.tow_vehicle
100
+ end
101
+
102
+ def test_should_be_in_busy_state
103
+ assert_equal 'busy', @auto_shop.state
104
+ end
105
+
106
+ def test_should_have_incremented_number_of_customers
107
+ assert_equal 1, @auto_shop.num_customers
108
+ end
109
+
110
+ def test_should_not_allow_tow_vehicle
111
+ assert !@auto_shop.tow_vehicle
112
+ end
113
+
114
+ def test_should_allow_fix_vehicle
115
+ assert @auto_shop.fix_vehicle
116
+ end
117
+
118
+ def test_should_not_allow_fix_vehicle_if_dont_have_parts
119
+ @auto_shop.have_parts = false
120
+ assert !@auto_shop.fix_vehicle
121
+ end
122
+ end
@@ -0,0 +1,993 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ class AutoShop
4
+ attr_accessor :num_customers
5
+
6
+ def initialize
7
+ @num_customers = 0
8
+ super
9
+ end
10
+
11
+ state_machine :initial => :available do
12
+ after_transition :available => any, :do => :increment_customers
13
+ after_transition :busy => any, :do => :decrement_customers
14
+
15
+ event :tow_vehicle do
16
+ transition :available => :busy
17
+ end
18
+
19
+ event :fix_vehicle do
20
+ transition :busy => :available
21
+ end
22
+ end
23
+
24
+ # Increments the number of customers in service
25
+ def increment_customers
26
+ self.num_customers += 1
27
+ end
28
+
29
+ # Decrements the number of customers in service
30
+ def decrement_customers
31
+ self.num_customers -= 1
32
+ end
33
+ end
34
+
35
+ class ModelBase
36
+ def save
37
+ @saved = true
38
+ self
39
+ end
40
+ end
41
+
42
+ class Vehicle < ModelBase
43
+ attr_accessor :auto_shop, :seatbelt_on, :insurance_premium, :force_idle, :callbacks, :saved, :time_elapsed
44
+
45
+ def initialize(attributes = {})
46
+ attributes = {
47
+ :auto_shop => AutoShop.new,
48
+ :seatbelt_on => false,
49
+ :insurance_premium => 50,
50
+ :force_idle => false,
51
+ :callbacks => [],
52
+ :saved => false
53
+ }.merge(attributes)
54
+
55
+ attributes.each {|attr, value| send("#{attr}=", value)}
56
+ super()
57
+ end
58
+
59
+ # Defines the state machine for the state of the vehicled
60
+ state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked}, :action => :save do
61
+ before_transition :parked => any, :do => :put_on_seatbelt
62
+ before_transition any => :stalled, :do => :increase_insurance_premium
63
+ after_transition any => :parked, :do => lambda {|vehicle| vehicle.seatbelt_on = false}
64
+ after_transition :on => :crash, :do => :tow
65
+ after_transition :on => :repair, :do => :fix
66
+
67
+ # Callback tracking for initial state callbacks
68
+ after_transition any => :parked, :do => lambda {|vehicle| vehicle.callbacks << 'before_enter_parked'}
69
+ before_transition any => :idling, :do => lambda {|vehicle| vehicle.callbacks << 'before_enter_idling'}
70
+
71
+ around_transition do |vehicle, transition, block|
72
+ time = Time.now
73
+ block.call
74
+ vehicle.time_elapsed = Time.now - time
75
+ end
76
+
77
+ event :park do
78
+ transition [:idling, :first_gear] => :parked
79
+ end
80
+
81
+ event :ignite do
82
+ transition :stalled => :stalled
83
+ transition :parked => :idling
84
+ end
85
+
86
+ event :idle do
87
+ transition :first_gear => :idling
88
+ end
89
+
90
+ event :shift_up do
91
+ transition :idling => :first_gear, :first_gear => :second_gear, :second_gear => :third_gear
92
+ end
93
+
94
+ event :shift_down do
95
+ transition :third_gear => :second_gear
96
+ transition :second_gear => :first_gear
97
+ end
98
+
99
+ event :crash do
100
+ transition [:first_gear, :second_gear, :third_gear] => :stalled, :if => lambda {|vehicle| vehicle.auto_shop.available?}
101
+ end
102
+
103
+ event :repair do
104
+ transition :stalled => :parked, :if => :auto_shop_busy?
105
+ end
106
+ end
107
+
108
+ state_machine :insurance_state, :initial => :inactive, :namespace => 'insurance' do
109
+ event :buy do
110
+ transition :inactive => :active
111
+ end
112
+
113
+ event :cancel do
114
+ transition :active => :inactive
115
+ end
116
+ end
117
+
118
+ def save
119
+ super
120
+ end
121
+
122
+ def new_record?
123
+ @saved == false
124
+ end
125
+
126
+ def park
127
+ super
128
+ end
129
+
130
+ # Tows the vehicle to the auto shop
131
+ def tow
132
+ auto_shop.tow_vehicle
133
+ end
134
+
135
+ # Fixes the vehicle; it will no longer be in the auto shop
136
+ def fix
137
+ auto_shop.fix_vehicle
138
+ end
139
+
140
+ def decibels
141
+ 0.0
142
+ end
143
+
144
+ private
145
+ # Safety first! Puts on our seatbelt
146
+ def put_on_seatbelt
147
+ self.seatbelt_on = true
148
+ end
149
+
150
+ # We crashed! Increase the insurance premium on the vehicle
151
+ def increase_insurance_premium
152
+ self.insurance_premium += 100
153
+ end
154
+
155
+ # Is the auto shop currently servicing another customer?
156
+ def auto_shop_busy?
157
+ auto_shop.busy?
158
+ end
159
+ end
160
+
161
+ class Car < Vehicle
162
+ state_machine do
163
+ event :reverse do
164
+ transition [:parked, :idling, :first_gear] => :backing_up
165
+ end
166
+
167
+ event :park do
168
+ transition :backing_up => :parked
169
+ end
170
+
171
+ event :idle do
172
+ transition :backing_up => :idling
173
+ end
174
+
175
+ event :shift_up do
176
+ transition :backing_up => :first_gear
177
+ end
178
+ end
179
+ end
180
+
181
+ class Motorcycle < Vehicle
182
+ state_machine :initial => :idling do
183
+ state :first_gear do
184
+ def decibels
185
+ 1.0
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ class TrafficLight
192
+ state_machine :initial => :stop do
193
+ event :cycle do
194
+ transition :stop => :proceed, :proceed=> :caution, :caution => :stop
195
+ end
196
+
197
+ state :stop do
198
+ def color(transform)
199
+ value = 'red'
200
+
201
+ if block_given?
202
+ yield value
203
+ else
204
+ value.send(transform)
205
+ end
206
+
207
+ value
208
+ end
209
+ end
210
+
211
+ state :proceed do
212
+ def color(transform)
213
+ 'green'
214
+ end
215
+ end
216
+
217
+ state :caution do
218
+ def color(transform)
219
+ 'yellow'
220
+ end
221
+ end
222
+ end
223
+
224
+ def color(transform = :to_s)
225
+ super
226
+ end
227
+ end
228
+
229
+ class VehicleTest < Test::Unit::TestCase
230
+ def setup
231
+ @vehicle = Vehicle.new
232
+ end
233
+
234
+ def test_should_not_allow_access_to_subclass_events
235
+ assert !@vehicle.respond_to?(:reverse)
236
+ end
237
+
238
+ def test_should_have_human_state_names
239
+ assert_equal 'parked', Vehicle.human_state_name(:parked)
240
+ end
241
+
242
+ def test_should_have_human_state_event_names
243
+ assert_equal 'park', Vehicle.human_state_event_name(:park)
244
+ end
245
+ end
246
+
247
+ class VehicleUnsavedTest < Test::Unit::TestCase
248
+ def setup
249
+ @vehicle = Vehicle.new
250
+ end
251
+
252
+ def test_should_be_in_parked_state
253
+ assert_equal 'parked', @vehicle.state
254
+ end
255
+
256
+ def test_should_raise_exception_if_checking_invalid_state
257
+ assert_raise(IndexError) { @vehicle.state?(:invalid) }
258
+ end
259
+
260
+ def test_should_raise_exception_if_getting_name_of_invalid_state
261
+ @vehicle.state = 'invalid'
262
+ assert_raise(ArgumentError) { @vehicle.state_name }
263
+ end
264
+
265
+ def test_should_be_parked
266
+ assert @vehicle.parked?
267
+ assert @vehicle.state?(:parked)
268
+ assert_equal :parked, @vehicle.state_name
269
+ assert_equal 'parked', @vehicle.human_state_name
270
+ end
271
+
272
+ def test_should_not_be_idling
273
+ assert !@vehicle.idling?
274
+ end
275
+
276
+ def test_should_not_be_first_gear
277
+ assert !@vehicle.first_gear?
278
+ end
279
+
280
+ def test_should_not_be_second_gear
281
+ assert !@vehicle.second_gear?
282
+ end
283
+
284
+ def test_should_not_be_stalled
285
+ assert !@vehicle.stalled?
286
+ end
287
+
288
+ def test_should_not_be_able_to_park
289
+ assert !@vehicle.can_park?
290
+ end
291
+
292
+ def test_should_not_have_a_transition_for_park
293
+ assert_nil @vehicle.park_transition
294
+ end
295
+
296
+ def test_should_not_allow_park
297
+ assert !@vehicle.park
298
+ end
299
+
300
+ def test_should_be_able_to_ignite
301
+ assert @vehicle.can_ignite?
302
+ end
303
+
304
+ def test_should_have_a_transition_for_ignite
305
+ transition = @vehicle.ignite_transition
306
+ assert_not_nil transition
307
+ assert_equal 'parked', transition.from
308
+ assert_equal 'idling', transition.to
309
+ assert_equal :ignite, transition.event
310
+ assert_equal :state, transition.attribute
311
+ assert_equal @vehicle, transition.object
312
+ end
313
+
314
+ def test_should_have_a_list_of_possible_events
315
+ assert_equal [:ignite], @vehicle.state_events
316
+ end
317
+
318
+ def test_should_have_a_list_of_possible_transitions
319
+ assert_equal [{:object => @vehicle, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}], @vehicle.state_transitions.map {|transition| transition.attributes}
320
+ end
321
+
322
+ def test_should_have_a_list_of_possible_paths
323
+ assert_equal [[
324
+ StateMachine::Transition.new(@vehicle, Vehicle.state_machine, :ignite, :parked, :idling),
325
+ StateMachine::Transition.new(@vehicle, Vehicle.state_machine, :shift_up, :idling, :first_gear)
326
+ ]], @vehicle.state_paths(:to => :first_gear)
327
+ end
328
+
329
+ def test_should_allow_ignite
330
+ assert @vehicle.ignite
331
+ assert_equal 'idling', @vehicle.state
332
+ end
333
+
334
+ def test_should_allow_ignite_with_skipped_action
335
+ assert @vehicle.ignite(false)
336
+ assert @vehicle.new_record?
337
+ end
338
+
339
+ def test_should_allow_ignite_bang
340
+ assert @vehicle.ignite!
341
+ end
342
+
343
+ def test_should_allow_ignite_bang_with_skipped_action
344
+ assert @vehicle.ignite!(false)
345
+ assert @vehicle.new_record?
346
+ end
347
+
348
+ def test_should_be_saved_after_successful_event
349
+ @vehicle.ignite
350
+ assert !@vehicle.new_record?
351
+ end
352
+
353
+ def test_should_not_allow_idle
354
+ assert !@vehicle.idle
355
+ end
356
+
357
+ def test_should_not_allow_shift_up
358
+ assert !@vehicle.shift_up
359
+ end
360
+
361
+ def test_should_not_allow_shift_down
362
+ assert !@vehicle.shift_down
363
+ end
364
+
365
+ def test_should_not_allow_crash
366
+ assert !@vehicle.crash
367
+ end
368
+
369
+ def test_should_not_allow_repair
370
+ assert !@vehicle.repair
371
+ end
372
+
373
+ def test_should_be_insurance_inactive
374
+ assert @vehicle.insurance_inactive?
375
+ end
376
+
377
+ def test_should_be_able_to_buy
378
+ assert @vehicle.can_buy_insurance?
379
+ end
380
+
381
+ def test_should_allow_buying_insurance
382
+ assert @vehicle.buy_insurance
383
+ end
384
+
385
+ def test_should_allow_buying_insurance_bang
386
+ assert @vehicle.buy_insurance!
387
+ end
388
+
389
+ def test_should_allow_ignite_buying_insurance_with_skipped_action
390
+ assert @vehicle.buy_insurance!(false)
391
+ assert @vehicle.new_record?
392
+ end
393
+
394
+ def test_should_not_be_insurance_active
395
+ assert !@vehicle.insurance_active?
396
+ end
397
+
398
+ def test_should_not_be_able_to_cancel
399
+ assert !@vehicle.can_cancel_insurance?
400
+ end
401
+
402
+ def test_should_not_allow_cancelling_insurance
403
+ assert !@vehicle.cancel_insurance
404
+ end
405
+ end
406
+
407
+ class VehicleParkedTest < Test::Unit::TestCase
408
+ def setup
409
+ @vehicle = Vehicle.new
410
+ end
411
+
412
+ def test_should_be_in_parked_state
413
+ assert_equal 'parked', @vehicle.state
414
+ end
415
+
416
+ def test_should_not_have_the_seatbelt_on
417
+ assert !@vehicle.seatbelt_on
418
+ end
419
+
420
+ def test_should_not_allow_park
421
+ assert !@vehicle.park
422
+ end
423
+
424
+ def test_should_allow_ignite
425
+ assert @vehicle.ignite
426
+ assert_equal 'idling', @vehicle.state
427
+ end
428
+
429
+ def test_should_not_allow_idle
430
+ assert !@vehicle.idle
431
+ end
432
+
433
+ def test_should_not_allow_shift_up
434
+ assert !@vehicle.shift_up
435
+ end
436
+
437
+ def test_should_not_allow_shift_down
438
+ assert !@vehicle.shift_down
439
+ end
440
+
441
+ def test_should_not_allow_crash
442
+ assert !@vehicle.crash
443
+ end
444
+
445
+ def test_should_not_allow_repair
446
+ assert !@vehicle.repair
447
+ end
448
+
449
+ def test_should_raise_exception_if_repair_not_allowed!
450
+ exception = assert_raise(StateMachine::InvalidTransition) {@vehicle.repair!}
451
+ assert_equal @vehicle, exception.object
452
+ assert_equal Vehicle.state_machine(:state), exception.machine
453
+ assert_equal :repair, exception.event
454
+ assert_equal 'parked', exception.from
455
+ end
456
+ end
457
+
458
+ class VehicleIdlingTest < Test::Unit::TestCase
459
+ def setup
460
+ @vehicle = Vehicle.new
461
+ @vehicle.ignite
462
+ end
463
+
464
+ def test_should_be_in_idling_state
465
+ assert_equal 'idling', @vehicle.state
466
+ end
467
+
468
+ def test_should_be_idling
469
+ assert @vehicle.idling?
470
+ end
471
+
472
+ def test_should_have_seatbelt_on
473
+ assert @vehicle.seatbelt_on
474
+ end
475
+
476
+ def test_should_track_time_elapsed
477
+ assert_not_nil @vehicle.time_elapsed
478
+ end
479
+
480
+ def test_should_allow_park
481
+ assert @vehicle.park
482
+ end
483
+
484
+ def test_should_call_park_with_bang_action
485
+ class << @vehicle
486
+ def park
487
+ super && 1
488
+ end
489
+ end
490
+
491
+ assert_equal 1, @vehicle.park!
492
+ end
493
+
494
+ def test_should_not_allow_idle
495
+ assert !@vehicle.idle
496
+ end
497
+
498
+ def test_should_allow_shift_up
499
+ assert @vehicle.shift_up
500
+ end
501
+
502
+ def test_should_not_allow_shift_down
503
+ assert !@vehicle.shift_down
504
+ end
505
+
506
+ def test_should_not_allow_crash
507
+ assert !@vehicle.crash
508
+ end
509
+
510
+ def test_should_not_allow_repair
511
+ assert !@vehicle.repair
512
+ end
513
+ end
514
+
515
+ class VehicleFirstGearTest < Test::Unit::TestCase
516
+ def setup
517
+ @vehicle = Vehicle.new
518
+ @vehicle.ignite
519
+ @vehicle.shift_up
520
+ end
521
+
522
+ def test_should_be_in_first_gear_state
523
+ assert_equal 'first_gear', @vehicle.state
524
+ end
525
+
526
+ def test_should_be_first_gear
527
+ assert @vehicle.first_gear?
528
+ end
529
+
530
+ def test_should_allow_park
531
+ assert @vehicle.park
532
+ end
533
+
534
+ def test_should_allow_idle
535
+ assert @vehicle.idle
536
+ end
537
+
538
+ def test_should_allow_shift_up
539
+ assert @vehicle.shift_up
540
+ end
541
+
542
+ def test_should_not_allow_shift_down
543
+ assert !@vehicle.shift_down
544
+ end
545
+
546
+ def test_should_allow_crash
547
+ assert @vehicle.crash
548
+ end
549
+
550
+ def test_should_not_allow_repair
551
+ assert !@vehicle.repair
552
+ end
553
+ end
554
+
555
+ class VehicleSecondGearTest < Test::Unit::TestCase
556
+ def setup
557
+ @vehicle = Vehicle.new
558
+ @vehicle.ignite
559
+ 2.times {@vehicle.shift_up}
560
+ end
561
+
562
+ def test_should_be_in_second_gear_state
563
+ assert_equal 'second_gear', @vehicle.state
564
+ end
565
+
566
+ def test_should_be_second_gear
567
+ assert @vehicle.second_gear?
568
+ end
569
+
570
+ def test_should_not_allow_park
571
+ assert !@vehicle.park
572
+ end
573
+
574
+ def test_should_not_allow_idle
575
+ assert !@vehicle.idle
576
+ end
577
+
578
+ def test_should_allow_shift_up
579
+ assert @vehicle.shift_up
580
+ end
581
+
582
+ def test_should_allow_shift_down
583
+ assert @vehicle.shift_down
584
+ end
585
+
586
+ def test_should_allow_crash
587
+ assert @vehicle.crash
588
+ end
589
+
590
+ def test_should_not_allow_repair
591
+ assert !@vehicle.repair
592
+ end
593
+ end
594
+
595
+ class VehicleThirdGearTest < Test::Unit::TestCase
596
+ def setup
597
+ @vehicle = Vehicle.new
598
+ @vehicle.ignite
599
+ 3.times {@vehicle.shift_up}
600
+ end
601
+
602
+ def test_should_be_in_third_gear_state
603
+ assert_equal 'third_gear', @vehicle.state
604
+ end
605
+
606
+ def test_should_be_third_gear
607
+ assert @vehicle.third_gear?
608
+ end
609
+
610
+ def test_should_not_allow_park
611
+ assert !@vehicle.park
612
+ end
613
+
614
+ def test_should_not_allow_idle
615
+ assert !@vehicle.idle
616
+ end
617
+
618
+ def test_should_not_allow_shift_up
619
+ assert !@vehicle.shift_up
620
+ end
621
+
622
+ def test_should_allow_shift_down
623
+ assert @vehicle.shift_down
624
+ end
625
+
626
+ def test_should_allow_crash
627
+ assert @vehicle.crash
628
+ end
629
+
630
+ def test_should_not_allow_repair
631
+ assert !@vehicle.repair
632
+ end
633
+ end
634
+
635
+ class VehicleStalledTest < Test::Unit::TestCase
636
+ def setup
637
+ @vehicle = Vehicle.new
638
+ @vehicle.ignite
639
+ @vehicle.shift_up
640
+ @vehicle.crash
641
+ end
642
+
643
+ def test_should_be_in_stalled_state
644
+ assert_equal 'stalled', @vehicle.state
645
+ end
646
+
647
+ def test_should_be_stalled
648
+ assert @vehicle.stalled?
649
+ end
650
+
651
+ def test_should_be_towed
652
+ assert @vehicle.auto_shop.busy?
653
+ assert_equal 1, @vehicle.auto_shop.num_customers
654
+ end
655
+
656
+ def test_should_have_an_increased_insurance_premium
657
+ assert_equal 150, @vehicle.insurance_premium
658
+ end
659
+
660
+ def test_should_not_allow_park
661
+ assert !@vehicle.park
662
+ end
663
+
664
+ def test_should_allow_ignite
665
+ assert @vehicle.ignite
666
+ end
667
+
668
+ def test_should_not_change_state_when_ignited
669
+ assert_equal 'stalled', @vehicle.state
670
+ end
671
+
672
+ def test_should_not_allow_idle
673
+ assert !@vehicle.idle
674
+ end
675
+
676
+ def test_should_now_allow_shift_up
677
+ assert !@vehicle.shift_up
678
+ end
679
+
680
+ def test_should_not_allow_shift_down
681
+ assert !@vehicle.shift_down
682
+ end
683
+
684
+ def test_should_not_allow_crash
685
+ assert !@vehicle.crash
686
+ end
687
+
688
+ def test_should_allow_repair_if_auto_shop_is_busy
689
+ assert @vehicle.repair
690
+ end
691
+
692
+ def test_should_not_allow_repair_if_auto_shop_is_available
693
+ @vehicle.auto_shop.fix_vehicle
694
+ assert !@vehicle.repair
695
+ end
696
+ end
697
+
698
+ class VehicleRepairedTest < Test::Unit::TestCase
699
+ def setup
700
+ @vehicle = Vehicle.new
701
+ @vehicle.ignite
702
+ @vehicle.shift_up
703
+ @vehicle.crash
704
+ @vehicle.repair
705
+ end
706
+
707
+ def test_should_be_in_parked_state
708
+ assert_equal 'parked', @vehicle.state
709
+ end
710
+
711
+ def test_should_not_have_a_busy_auto_shop
712
+ assert @vehicle.auto_shop.available?
713
+ end
714
+ end
715
+
716
+ class VehicleWithParallelEventsTest < Test::Unit::TestCase
717
+ def setup
718
+ @vehicle = Vehicle.new
719
+ end
720
+
721
+ def test_should_fail_if_any_event_cannot_transition
722
+ assert !@vehicle.fire_events(:ignite, :cancel_insurance)
723
+ end
724
+
725
+ def test_should_be_successful_if_all_events_transition
726
+ assert @vehicle.fire_events(:ignite, :buy_insurance)
727
+ end
728
+
729
+ def test_should_not_save_if_skipping_action
730
+ assert @vehicle.fire_events(:ignite, :buy_insurance, false)
731
+ assert !@vehicle.saved
732
+ end
733
+
734
+ def test_should_raise_exception_if_any_event_cannot_transition_on_bang
735
+ exception = assert_raise(StateMachine::InvalidParallelTransition) { @vehicle.fire_events!(:ignite, :cancel_insurance) }
736
+ assert_equal @vehicle, exception.object
737
+ assert_equal [:ignite, :cancel_insurance], exception.events
738
+ end
739
+
740
+ def test_should_not_raise_exception_if_all_events_transition_on_bang
741
+ assert @vehicle.fire_events!(:ignite, :buy_insurance)
742
+ end
743
+
744
+ def test_should_not_save_if_skipping_action_on_bang
745
+ assert @vehicle.fire_events!(:ignite, :buy_insurance, false)
746
+ assert !@vehicle.saved
747
+ end
748
+ end
749
+
750
+ class VehicleWithEventAttributesTest < Test::Unit::TestCase
751
+ def setup
752
+ @vehicle = Vehicle.new
753
+ @vehicle.state_event = 'ignite'
754
+ end
755
+
756
+ def test_should_fail_if_event_is_invalid
757
+ @vehicle.state_event = 'invalid'
758
+ assert !@vehicle.save
759
+ assert_equal 'parked', @vehicle.state
760
+ end
761
+
762
+ def test_should_fail_if_event_has_no_transition
763
+ @vehicle.state_event = 'park'
764
+ assert !@vehicle.save
765
+ assert_equal 'parked', @vehicle.state
766
+ end
767
+
768
+ def test_should_return_original_action_value_on_success
769
+ assert_equal @vehicle, @vehicle.save
770
+ end
771
+
772
+ def test_should_transition_state_on_success
773
+ @vehicle.save
774
+ assert_equal 'idling', @vehicle.state
775
+ end
776
+ end
777
+
778
+ class MotorcycleTest < Test::Unit::TestCase
779
+ def setup
780
+ @motorcycle = Motorcycle.new
781
+ end
782
+
783
+ def test_should_be_in_idling_state
784
+ assert_equal 'idling', @motorcycle.state
785
+ end
786
+
787
+ def test_should_allow_park
788
+ assert @motorcycle.park
789
+ end
790
+
791
+ def test_should_not_allow_ignite
792
+ assert !@motorcycle.ignite
793
+ end
794
+
795
+ def test_should_allow_shift_up
796
+ assert @motorcycle.shift_up
797
+ end
798
+
799
+ def test_should_not_allow_shift_down
800
+ assert !@motorcycle.shift_down
801
+ end
802
+
803
+ def test_should_not_allow_crash
804
+ assert !@motorcycle.crash
805
+ end
806
+
807
+ def test_should_not_allow_repair
808
+ assert !@motorcycle.repair
809
+ end
810
+
811
+ def test_should_inherit_decibels_from_superclass
812
+ @motorcycle.park
813
+ assert_equal 0.0, @motorcycle.decibels
814
+ end
815
+
816
+ def test_should_use_decibels_defined_in_state
817
+ @motorcycle.shift_up
818
+ assert_equal 1.0, @motorcycle.decibels
819
+ end
820
+ end
821
+
822
+ class CarTest < Test::Unit::TestCase
823
+ def setup
824
+ @car = Car.new
825
+ end
826
+
827
+ def test_should_be_in_parked_state
828
+ assert_equal 'parked', @car.state
829
+ end
830
+
831
+ def test_should_not_have_the_seatbelt_on
832
+ assert !@car.seatbelt_on
833
+ end
834
+
835
+ def test_should_not_allow_park
836
+ assert !@car.park
837
+ end
838
+
839
+ def test_should_allow_ignite
840
+ assert @car.ignite
841
+ assert_equal 'idling', @car.state
842
+ end
843
+
844
+ def test_should_not_allow_idle
845
+ assert !@car.idle
846
+ end
847
+
848
+ def test_should_not_allow_shift_up
849
+ assert !@car.shift_up
850
+ end
851
+
852
+ def test_should_not_allow_shift_down
853
+ assert !@car.shift_down
854
+ end
855
+
856
+ def test_should_not_allow_crash
857
+ assert !@car.crash
858
+ end
859
+
860
+ def test_should_not_allow_repair
861
+ assert !@car.repair
862
+ end
863
+
864
+ def test_should_allow_reverse
865
+ assert @car.reverse
866
+ end
867
+ end
868
+
869
+ class CarBackingUpTest < Test::Unit::TestCase
870
+ def setup
871
+ @car = Car.new
872
+ @car.reverse
873
+ end
874
+
875
+ def test_should_be_in_backing_up_state
876
+ assert_equal 'backing_up', @car.state
877
+ end
878
+
879
+ def test_should_allow_park
880
+ assert @car.park
881
+ end
882
+
883
+ def test_should_not_allow_ignite
884
+ assert !@car.ignite
885
+ end
886
+
887
+ def test_should_allow_idle
888
+ assert @car.idle
889
+ end
890
+
891
+ def test_should_allow_shift_up
892
+ assert @car.shift_up
893
+ end
894
+
895
+ def test_should_not_allow_shift_down
896
+ assert !@car.shift_down
897
+ end
898
+
899
+ def test_should_not_allow_crash
900
+ assert !@car.crash
901
+ end
902
+
903
+ def test_should_not_allow_repair
904
+ assert !@car.repair
905
+ end
906
+
907
+ def test_should_not_allow_reverse
908
+ assert !@car.reverse
909
+ end
910
+ end
911
+
912
+ class AutoShopAvailableTest < Test::Unit::TestCase
913
+ def setup
914
+ @auto_shop = AutoShop.new
915
+ end
916
+
917
+ def test_should_be_in_available_state
918
+ assert_equal 'available', @auto_shop.state
919
+ end
920
+
921
+ def test_should_allow_tow_vehicle
922
+ assert @auto_shop.tow_vehicle
923
+ end
924
+
925
+ def test_should_not_allow_fix_vehicle
926
+ assert !@auto_shop.fix_vehicle
927
+ end
928
+ end
929
+
930
+ class AutoShopBusyTest < Test::Unit::TestCase
931
+ def setup
932
+ @auto_shop = AutoShop.new
933
+ @auto_shop.tow_vehicle
934
+ end
935
+
936
+ def test_should_be_in_busy_state
937
+ assert_equal 'busy', @auto_shop.state
938
+ end
939
+
940
+ def test_should_have_incremented_number_of_customers
941
+ assert_equal 1, @auto_shop.num_customers
942
+ end
943
+
944
+ def test_should_not_allow_tow_vehicle
945
+ assert !@auto_shop.tow_vehicle
946
+ end
947
+
948
+ def test_should_allow_fix_vehicle
949
+ assert @auto_shop.fix_vehicle
950
+ end
951
+ end
952
+
953
+ class TrafficLightStopTest < Test::Unit::TestCase
954
+ def setup
955
+ @light = TrafficLight.new
956
+ @light.state = 'stop'
957
+ end
958
+
959
+ def test_should_use_stop_color
960
+ assert_equal 'red', @light.color
961
+ end
962
+
963
+ def test_should_pass_arguments_through
964
+ assert_equal 'RED', @light.color(:upcase!)
965
+ end
966
+
967
+ def test_should_pass_block_through
968
+ color = @light.color {|value| value.upcase!}
969
+ assert_equal 'RED', color
970
+ end
971
+ end
972
+
973
+ class TrafficLightProceedTest < Test::Unit::TestCase
974
+ def setup
975
+ @light = TrafficLight.new
976
+ @light.state = 'proceed'
977
+ end
978
+
979
+ def test_should_use_proceed_color
980
+ assert_equal 'green', @light.color
981
+ end
982
+ end
983
+
984
+ class TrafficLightCautionTest < Test::Unit::TestCase
985
+ def setup
986
+ @light = TrafficLight.new
987
+ @light.state = 'caution'
988
+ end
989
+
990
+ def test_should_use_caution_color
991
+ assert_equal 'yellow', @light.color
992
+ end
993
+ end