hsume2-state_machine 1.0.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.
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