golem_statemachine 0.9

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.
@@ -0,0 +1,429 @@
1
+ require 'test_helper'
2
+ require 'statemachine_assertions'
3
+ require File.dirname(__FILE__)+'/../lib/golem'
4
+
5
+ # Because Monster has two statemachines, 'affect' and 'mouth', this test
6
+ # allows for validating statemachine behaviour when there are multiple
7
+ # statemachines defined within the same class.
8
+ class MonsterTest < Test::Unit::TestCase
9
+ include StatemachineAssertions
10
+
11
+ def setup
12
+ @klass = Class.new
13
+ @klass.instance_eval do
14
+ include Golem
15
+ end
16
+ end
17
+
18
+ def test_define_statemachine
19
+ @klass.instance_eval do
20
+ define_statemachine do
21
+ initial_state :test
22
+ end
23
+ end
24
+
25
+ assert_kind_of Golem::Model::StateMachine, @klass.statemachines[:statemachine]
26
+ end
27
+
28
+ def test_define_invalid_statemachine
29
+ assert_raise Golem::DefinitionSyntaxError do
30
+ @klass.instance_eval do
31
+ define_statemachine do
32
+ # no inital_state
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def test_define_multiple_statemachines
39
+ @klass.instance_eval do
40
+ define_statemachine(:alpha) do
41
+ initial_state :test
42
+ end
43
+
44
+ define_statemachine(:beta) do
45
+ initial_state :test
46
+ end
47
+ end
48
+
49
+ assert_kind_of Golem::Model::StateMachine, @klass.statemachines[:alpha]
50
+ assert_kind_of Golem::Model::StateMachine, @klass.statemachines[:beta]
51
+ assert_not_same @klass.statemachines[:alpha], @klass.statemachines[:beta]
52
+
53
+ # check that defining a default statemachine after defining named statemachines is okay
54
+ @klass.instance_eval do
55
+ define_statemachine do
56
+ initial_state :test
57
+ end
58
+ end
59
+
60
+ assert_kind_of Golem::Model::StateMachine, @klass.statemachines[:statemachine]
61
+ assert_not_same @klass.statemachines[:alpha], @klass.statemachines[:statemachine]
62
+ assert_not_same @klass.statemachines[:beta], @klass.statemachines[:statemachine]
63
+ end
64
+
65
+ def test_define_duplicate_statemachine
66
+ @klass.instance_eval do
67
+ define_statemachine(:alpha) do
68
+ initial_state :test
69
+ end
70
+ end
71
+
72
+ assert_raise ArgumentError do
73
+ @klass.instance_eval do
74
+ define_statemachine(:alpha) do
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ def test_define_multiple_invalid_statemachines
81
+ assert_raise Golem::DefinitionSyntaxError do
82
+ @klass.instance_eval do
83
+ define_statemachine(:alpha) do
84
+ initial_state :one
85
+ end
86
+
87
+ define_statemachine(:beta) do
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ def test_define_state_attribute
94
+ @klass.instance_eval do
95
+ define_statemachine do
96
+ initial_state :one
97
+ end
98
+
99
+ define_statemachine(:alpha) do
100
+ initial_state :two
101
+ end
102
+
103
+ define_statemachine(:beta) do
104
+ state_attribute :foo
105
+ initial_state :three
106
+ end
107
+ end
108
+
109
+ obj = @klass.new
110
+ assert_equal :one, obj.state
111
+ assert_equal :two, obj.alpha_state
112
+ assert_equal :three, obj.foo
113
+ end
114
+
115
+
116
+ def test_define_states
117
+ @klass.instance_eval do
118
+ define_statemachine do
119
+ initial_state :one
120
+ state :one
121
+ state :two
122
+ end
123
+
124
+ define_statemachine(:alpha) do
125
+ initial_state :one
126
+ state :one
127
+ state :two
128
+ end
129
+
130
+ define_statemachine(:beta) do
131
+ initial_state :three
132
+ state :one
133
+ state :two
134
+ state :three
135
+ end
136
+ end
137
+
138
+ assert_equal :one, @klass.statemachines[:statemachine].states[:one].name
139
+ assert_equal 2, ([:one, :two] & @klass.statemachines[:statemachine].states.collect{|s| s.name}).size
140
+ assert_equal 2, ([:one, :two] & @klass.statemachines[:alpha].states.collect{|s| s.name}).size
141
+ assert_equal 3, ([:one, :two, :three] & @klass.statemachines[:beta].states.collect{|s| s.name}).size
142
+ assert_not_same @klass.statemachines[:statemachine].states[:one], @klass.statemachines[:statemachine].states[:two]
143
+ assert_not_same @klass.statemachines[:alpha].states[:one], @klass.statemachines[:beta].states[:one]
144
+ end
145
+
146
+ def test_define_events
147
+ @klass.instance_eval do
148
+ define_statemachine(:alpha) do
149
+ initial_state :one
150
+ state :one do
151
+ on :go, :to => :two
152
+ end
153
+ state :two do
154
+ on :go do
155
+ transition :to => :three
156
+ end
157
+ end
158
+ state :three, :to => :one do
159
+ on :go, :to => :one do
160
+ transition
161
+ end
162
+ end
163
+ end
164
+
165
+ define_statemachine(:beta) do
166
+ initial_state :b
167
+ state :a do
168
+ on :doit, :to => :b
169
+ on :foo, :action => Proc.new{|arg| puts arg}
170
+ end
171
+ state :b do
172
+ on :doit do
173
+ transition :to => :a
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ assert_equal [:go], @klass.statemachines[:alpha].events.collect{|e| e.name}
180
+ ev = @klass.statemachines[:alpha].events[:go]
181
+ assert_equal :go, ev.name
182
+
183
+ assert_equal_arrays [:doit, :foo], @klass.statemachines[:beta].events.collect{|e| e.name}
184
+ end
185
+
186
+
187
+ def test_define_transitions_in_event_to
188
+ sm = sm_def_helper do
189
+ state :a do
190
+ on :go, :to => :b
191
+ end
192
+ state :b do
193
+ on :go, :to => :a do
194
+ transition # inherits on :to
195
+ transition :to => :b # overrides on :to
196
+ end
197
+ end
198
+ end
199
+
200
+ assert_transition_on_event sm, :go, :a, :b
201
+ assert_transition_on_event sm, :go, :b, :a
202
+ assert_transition_on_event sm, :go, :b, :b
203
+ assert_no_transition_on_event sm, :go, :a, :a
204
+ end
205
+
206
+
207
+ def test_define_self_transitions
208
+ sm = sm_def_helper do
209
+ state :a do
210
+ on :go
211
+ end
212
+ state :b do
213
+ on :go do
214
+ transition :to => :self
215
+ end
216
+ end
217
+ end
218
+
219
+ assert_transition_on_event sm, :go, :a, :a
220
+ assert_transition_on_event sm, :go, :b, :b
221
+ assert_no_transition_on_event sm, :go, :a, :b
222
+ assert_no_transition_on_event sm, :go, :b, :a
223
+ end
224
+
225
+
226
+ def test_define_state_actions
227
+ enter_b_proc = Proc.new{puts "hi!"}
228
+
229
+ sm = sm_def_helper do
230
+ state :a do
231
+ enter :hi
232
+ exit :bye
233
+ end
234
+ state :b do
235
+ enter enter_b_proc
236
+ exit do
237
+ puts "bye!"
238
+ end
239
+ end
240
+ end
241
+
242
+ assert_equal :hi, sm.states[:a].callbacks[:on_enter].callback
243
+ assert_equal :bye, sm.states[:a].callbacks[:on_exit].callback
244
+ assert_equal enter_b_proc, sm.states[:b].callbacks[:on_enter].callback
245
+ assert_equal Proc, sm.states[:b].callbacks[:on_exit].callback.class
246
+ end
247
+
248
+
249
+ def test_define_transition_guards
250
+ sm = sm_def_helper do
251
+ state :a do
252
+ on :go_1 do
253
+ transition :to => :self do
254
+ guard :ready?
255
+ guard :steady?
256
+ end
257
+ end
258
+ on :go_2 do
259
+ transition :to => :self, :if => :ready?
260
+ end
261
+ on :go_3 do
262
+ transition :to => :self, :if => :ready? do
263
+ guard :steady?
264
+ end
265
+ end
266
+ on :go_4, :if => :ready?
267
+ on :go_5, :if => :ready? do
268
+ transition :to => :b, :if => :steady? do
269
+ guard :maybe?
270
+ end
271
+ transition :to => :c, :if => :maybe?
272
+ transition :to => :self
273
+ end
274
+ end
275
+ end
276
+
277
+ assert_transition_on_event_has_guard sm, :go_1, :a, :a, :ready?
278
+ assert_transition_on_event_has_guard sm, :go_1, :a, :a, :steady?
279
+
280
+ assert_transition_on_event_has_guard sm, :go_2, :a, :a, :ready?
281
+
282
+ assert_transition_on_event_has_guard sm, :go_3, :a, :a, :ready?
283
+ assert_transition_on_event_has_guard sm, :go_3, :a, :a, :steady?
284
+
285
+ assert_transition_on_event_has_guard sm, :go_4, :a, :a, :ready?
286
+
287
+ assert_transition_on_event_has_guard sm, :go_5, :a, :b, :ready?
288
+ assert_transition_on_event_has_guard sm, :go_5, :a, :b, :steady?
289
+ assert_transition_on_event_has_guard sm, :go_5, :a, :b, :maybe?
290
+
291
+ assert_transition_on_event_has_guard sm, :go_5, :a, :c, :ready?
292
+ assert_transition_on_event_has_guard sm, :go_5, :a, :c, :maybe?
293
+ assert_transition_on_event_does_not_have_guard sm, :go_5, :a, :c, :steady?
294
+
295
+ assert_transition_on_event_has_guard sm, :go_5, :a, :a, :ready?
296
+ assert_transition_on_event_does_not_have_guard sm, :go_5, :a, :a, :maybe?
297
+ assert_transition_on_event_does_not_have_guard sm, :go_5, :a, :a, :steady?
298
+ end
299
+
300
+ def test_define_transition_guards_failure_messages
301
+ sm = sm_def_helper do
302
+ state :a do
303
+ on :move, :if => :movable?, :guard_options => {:failure_message => "it's not movable"} do
304
+ transition :to => :b do
305
+ guard :not_stuck?, :failure_message => "it's stuck"
306
+ guard :not_busy?, :failure_message => "it's busy"
307
+ end
308
+ transition :to => :c do
309
+ guard :not_tired?, :failure_message => "it's tired"
310
+ end
311
+ transition :to => :self, :if => :not_busy?
312
+ end
313
+ on :spin do
314
+ transition :to => :self do
315
+ guard :not_tired?, :failure_message => "it's tired"
316
+ guard :not_busy?, :failure_message => "it's busy"
317
+ end
318
+ end
319
+ end
320
+ end
321
+
322
+ @klass.instance_eval do
323
+ attr_accessor :tired
324
+ attr_accessor :stuck
325
+ attr_accessor :movable
326
+ attr_accessor :busy
327
+
328
+ define_method(:movable?) do
329
+ @movable
330
+ end
331
+
332
+ define_method(:not_busy?) do
333
+ !@busy
334
+ end
335
+
336
+ define_method(:not_tired?) do
337
+ !@tired
338
+ end
339
+
340
+ define_method(:not_stuck?) do
341
+ !@stuck
342
+ end
343
+ end
344
+
345
+ assert_transition_on_event_has_guard sm, :move, :a, :b, :movable?
346
+ assert_transition_on_event_has_guard sm, :move, :a, :b, :not_stuck?
347
+ assert_transition_on_event_has_guard sm, :move, :a, :b, :not_busy?
348
+ assert_transition_on_event_does_not_have_guard sm, :move, :a, :b, :not_tired?
349
+
350
+ assert_transition_on_event_has_guard sm, :move, :a, :c, :movable?
351
+ assert_transition_on_event_does_not_have_guard sm, :move, :a, :c, :not_stuck?
352
+ assert_transition_on_event_does_not_have_guard sm, :move, :a, :c, :not_busy?
353
+ assert_transition_on_event_has_guard sm, :move, :a, :c, :not_tired?
354
+
355
+ assert_transition_on_event_has_guard sm, :move, :a, :a, :movable?
356
+ assert_transition_on_event_does_not_have_guard sm, :move, :a, :a, :not_stuck?
357
+ assert_transition_on_event_does_not_have_guard sm, :move, :a, :a, :not_tired?
358
+ assert_transition_on_event_has_guard sm, :move, :a, :a, :not_busy?
359
+
360
+ assert_transition_on_event_does_not_have_guard sm, :spin, :a, :a, :movable?
361
+ assert_transition_on_event_has_guard sm, :spin, :a, :a, :not_busy?
362
+ assert_transition_on_event_has_guard sm, :spin, :a, :a, :not_tired?
363
+ assert_transition_on_event_does_not_have_guard sm, :spin, :a, :a, :not_stuck?
364
+
365
+ obj = @klass.new
366
+ obj.movable = false
367
+
368
+ raised = false
369
+ begin
370
+ obj.move!
371
+ rescue Golem::ImpossibleEvent => e
372
+ assert_match(/because it's not movable/, e.message)
373
+ raised = true
374
+ ensure
375
+ # be careful -- we end up here if assert_match fails!
376
+ assert raised
377
+ end
378
+
379
+ obj.stuck = true
380
+
381
+ raised = false
382
+ begin
383
+ obj.move!
384
+ rescue Golem::ImpossibleEvent => e
385
+ assert_match(/because it's not movable/, e.message)
386
+ raised = true
387
+ ensure
388
+ # be careful -- we end up here if assert_match fails!
389
+ assert raised
390
+ end
391
+
392
+ obj.tired = true
393
+ obj.movable = true
394
+
395
+ assert_nothing_raised do
396
+ obj.move! # follows only available transition --> to self
397
+ end
398
+ assert_equal :a, obj.state
399
+
400
+
401
+ obj.busy = true
402
+ obj.tired = false
403
+
404
+ # make sure second the guard (is_busy?) works
405
+ raised = false
406
+ begin
407
+ obj.spin!
408
+ rescue Golem::ImpossibleEvent => e
409
+ assert_match(/because it's busy/, e.message)
410
+ raised = true
411
+ ensure
412
+ # be careful -- we end up here if assert_match fails!
413
+ assert raised
414
+ end
415
+ end
416
+
417
+ private
418
+
419
+ def sm_def_helper(&block)
420
+ @klass.instance_eval do
421
+ define_statemachine do
422
+ initial_state :a
423
+ instance_eval(&block) if block_given?
424
+ end
425
+ end
426
+ return @klass.statemachines[:statemachine]
427
+ end
428
+ end
429
+
@@ -0,0 +1,110 @@
1
+ require 'test_helper'
2
+ require File.dirname(__FILE__)+'/../examples/monster'
3
+
4
+ # Because the Monster class has two statemachines ('affect' and 'mouth')
5
+ # this test allows for validating statemachine behaviour when there are
6
+ # multiple statemachines defined within the same class.
7
+ class MonsterTest < Test::Unit::TestCase
8
+
9
+ def setup
10
+ @monster = Monster.new
11
+ end
12
+
13
+ def test_initial_state
14
+ assert_equal :sleeping, @monster.affect.initial_state
15
+ assert_equal :closed, @monster.mouth.initial_state
16
+ end
17
+
18
+ def test_non_existent_event
19
+ assert_raise(NoMethodError){@monster.dance!}
20
+ end
21
+
22
+ def test_machine_state_accessors
23
+ assert_equal :sleeping, @monster.affect_state
24
+
25
+ assert_equal :closed, @monster.mouth_state
26
+ end
27
+
28
+ def test_event_possibility_within_states
29
+ assert_equal :sleeping, @monster.affect_state
30
+ assert_equal :closed, @monster.mouth_state
31
+
32
+ assert_raise(Golem::ImpossibleEvent){@monster.feed!}
33
+ assert_raise(Golem::ImpossibleEvent){@monster.lullaby!}
34
+ assert_nothing_raised{@monster.wake_up!}
35
+ assert_equal :hungry, @monster.affect_state
36
+
37
+ assert_raise(Golem::ImpossibleEvent){@monster.wake_up!}
38
+ assert_nothing_raised{@monster.lullaby!}
39
+ assert_equal :hungry, @monster.affect_state
40
+
41
+ assert_raise(Golem::ImpossibleEvent){@monster.feed!(:hamburger)}
42
+ assert_nothing_raised{@monster.tickle!}
43
+ assert_nothing_raised{@monster.feed!(:hamburger)}
44
+ assert_equal :satiated, @monster.affect_state
45
+
46
+ assert_raise(Golem::ImpossibleEvent){@monster.feed!}
47
+ assert_raise(Golem::ImpossibleEvent){@monster.wake_up!}
48
+ end
49
+
50
+ def test_transition_decisions
51
+ @monster.wake_up!
52
+
53
+ assert_equal :hungry, @monster.affect_state
54
+
55
+ @monster.tickle!
56
+
57
+ @monster.feed!(:toast) # monster doesn't care for toast
58
+
59
+ assert_equal :hungry, @monster.affect_state
60
+ assert @monster.deeds.include?(:ate_toast)
61
+ assert !@monster.deeds.include?(:barfed)
62
+
63
+ @monster.feed!(:tofu) # monster hates tofu
64
+
65
+ assert @monster.deeds.include?(:barfed)
66
+ assert_equal :hungry, @monster.affect_state
67
+
68
+ @monster.feed!(:hamburger) # monster likes hamburger
69
+
70
+ assert_equal :satiated, @monster.affect_state
71
+ assert @monster.deeds.include?(:ate_tasty_hamburger)
72
+ end
73
+
74
+ def test_state_actions
75
+ @monster.wake_up!
76
+
77
+ assert_raise(Golem::ImpossibleEvent){@monster.wake_up!}
78
+ assert_equal [:stretched, :grumbled], @monster.deeds
79
+ # The transition didn't take place, so :hungry's enter action should not have been executed
80
+ # (i.e. we should only see one :grumbled).
81
+
82
+ @monster.tickle!
83
+ @monster.feed!(:toast)
84
+ @monster.lullaby!
85
+ @monster.tickle!
86
+ @monster.feed!(:tofu)
87
+
88
+ assert_equal [:stretched, :grumbled, :giggled, :grumbled, :ate_toast, :grumbled, :yawned, :grumbled, :giggled,
89
+ :grumbled, :barfed, :grumbled], @monster.deeds
90
+ # Note that `grumble` is executed even when we transition from :hungry back to :hungry.
91
+ # Enter and exit actions are to be executed even for self-transitions, however they should NOT be executed when
92
+ # transition fails to occur (i.e. when an ImpossibleEvent is raised).
93
+ end
94
+
95
+ def test_transition_actions
96
+ @monster.wake_up!
97
+ @monster.tickle!
98
+ @monster.feed!(:toast)
99
+ @monster.lullaby!
100
+ @monster.tickle!
101
+ @monster.feed!(:tofu)
102
+ @monster.feed!(:hamburger)
103
+ @monster.lullaby!
104
+ @monster.lullaby!
105
+
106
+ assert_equal [:stretched, :grumbled, :giggled, :grumbled, :ate_toast, :grumbled, :yawned, :grumbled, :giggled,
107
+ :grumbled, :barfed, :grumbled, :ate_tasty_hamburger, :yawned], @monster.deeds
108
+ end
109
+ end
110
+