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.
- data/CHANGELOG.rdoc +413 -0
- data/LICENSE +20 -0
- data/README.rdoc +717 -0
- data/Rakefile +77 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +11 -0
- data/examples/car.rb +19 -0
- data/examples/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +11 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view_edit.html.erb +25 -0
- data/examples/rails-rest/view_index.html.erb +23 -0
- data/examples/rails-rest/view_new.html.erb +14 -0
- data/examples/rails-rest/view_show.html.erb +17 -0
- data/examples/traffic_light.rb +7 -0
- data/examples/vehicle.rb +31 -0
- data/init.rb +1 -0
- data/lib/state_machine.rb +448 -0
- data/lib/state_machine/alternate_machine.rb +79 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/branch.rb +224 -0
- data/lib/state_machine/callback.rb +236 -0
- data/lib/state_machine/condition_proxy.rb +94 -0
- data/lib/state_machine/error.rb +13 -0
- data/lib/state_machine/eval_helpers.rb +86 -0
- data/lib/state_machine/event.rb +304 -0
- data/lib/state_machine/event_collection.rb +139 -0
- data/lib/state_machine/extensions.rb +149 -0
- data/lib/state_machine/initializers.rb +4 -0
- data/lib/state_machine/initializers/merb.rb +1 -0
- data/lib/state_machine/initializers/rails.rb +25 -0
- data/lib/state_machine/integrations.rb +110 -0
- data/lib/state_machine/integrations/active_model.rb +502 -0
- data/lib/state_machine/integrations/active_model/locale.rb +11 -0
- data/lib/state_machine/integrations/active_model/observer.rb +45 -0
- data/lib/state_machine/integrations/active_model/versions.rb +31 -0
- data/lib/state_machine/integrations/active_record.rb +424 -0
- data/lib/state_machine/integrations/active_record/locale.rb +20 -0
- data/lib/state_machine/integrations/active_record/versions.rb +143 -0
- data/lib/state_machine/integrations/base.rb +91 -0
- data/lib/state_machine/integrations/data_mapper.rb +392 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
- data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
- data/lib/state_machine/integrations/mongo_mapper.rb +272 -0
- data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
- data/lib/state_machine/integrations/mongo_mapper/versions.rb +110 -0
- data/lib/state_machine/integrations/mongoid.rb +357 -0
- data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
- data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
- data/lib/state_machine/integrations/sequel.rb +428 -0
- data/lib/state_machine/integrations/sequel/versions.rb +36 -0
- data/lib/state_machine/machine.rb +1873 -0
- data/lib/state_machine/machine_collection.rb +87 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +157 -0
- data/lib/state_machine/path.rb +120 -0
- data/lib/state_machine/path_collection.rb +90 -0
- data/lib/state_machine/state.rb +271 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/transition.rb +458 -0
- data/lib/state_machine/transition_collection.rb +244 -0
- data/lib/tasks/state_machine.rake +1 -0
- data/lib/tasks/state_machine.rb +27 -0
- data/test/files/en.yml +17 -0
- data/test/files/switch.rb +11 -0
- data/test/functional/alternate_state_machine_test.rb +122 -0
- data/test/functional/state_machine_test.rb +993 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/branch_test.rb +890 -0
- data/test/unit/callback_test.rb +701 -0
- data/test/unit/condition_proxy_test.rb +328 -0
- data/test/unit/error_test.rb +43 -0
- data/test/unit/eval_helpers_test.rb +222 -0
- data/test/unit/event_collection_test.rb +358 -0
- data/test/unit/event_test.rb +985 -0
- data/test/unit/integrations/active_model_test.rb +1097 -0
- data/test/unit/integrations/active_record_test.rb +2021 -0
- data/test/unit/integrations/base_test.rb +99 -0
- data/test/unit/integrations/data_mapper_test.rb +1909 -0
- data/test/unit/integrations/mongo_mapper_test.rb +1611 -0
- data/test/unit/integrations/mongoid_test.rb +1591 -0
- data/test/unit/integrations/sequel_test.rb +1523 -0
- data/test/unit/integrations_test.rb +61 -0
- data/test/unit/invalid_event_test.rb +20 -0
- data/test/unit/invalid_parallel_transition_test.rb +18 -0
- data/test/unit/invalid_transition_test.rb +77 -0
- data/test/unit/machine_collection_test.rb +599 -0
- data/test/unit/machine_test.rb +3043 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +217 -0
- data/test/unit/path_collection_test.rb +266 -0
- data/test/unit/path_test.rb +485 -0
- data/test/unit/state_collection_test.rb +310 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +924 -0
- data/test/unit/transition_collection_test.rb +2102 -0
- data/test/unit/transition_test.rb +1541 -0
- 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,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
|