finite_machine 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +73 -6
- data/lib/finite_machine/async_call.rb +24 -25
- data/lib/finite_machine/async_proxy.rb +4 -1
- data/lib/finite_machine/dsl.rb +29 -0
- data/lib/finite_machine/event_queue.rb +4 -9
- data/lib/finite_machine/listener.rb +7 -0
- data/lib/finite_machine/logger.rb +19 -0
- data/lib/finite_machine/observer.rb +2 -2
- data/lib/finite_machine/state_machine.rb +12 -5
- data/lib/finite_machine/transition_event.rb +10 -9
- data/lib/finite_machine/version.rb +1 -1
- data/lib/finite_machine.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/unit/alias_target_spec.rb +108 -0
- data/spec/unit/async_events_spec.rb +34 -2
- data/spec/unit/{standalone_spec.rb → definition_spec.rb} +0 -0
- data/spec/unit/handlers_spec.rb +3 -3
- data/spec/unit/if_unless_spec.rb +58 -0
- data/spec/unit/initialize_spec.rb +2 -2
- data/spec/unit/log_transitions_spec.rb +30 -0
- data/spec/unit/logger_spec.rb +7 -0
- metadata +8 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f3f7827c0cdd099708b2dd384b7d7acfe82449d8
|
4
|
+
data.tar.gz: 1aa5b2f9964b04c5ff1c7d642acd6eb67b985411
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2146d17c6edc868af3e1dc7e0a772893e2b7c4b1954a2a259dd3a9bca6a750083ac06a78ce1bc10fbc3c04902a98ce7ae659a42948d558a3a3fd25ccc942ae4b
|
7
|
+
data.tar.gz: 4ac655d7e523e6c2d872844b65de8fa0b29449b7d33b4c6cf8834e8586b5b68503cda2653f266e4a4f9cf07291d47031513caf97d04fe2e33f88e6e154f917ad
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
0.10.0 (November 16, 2014)
|
2
|
+
|
3
|
+
* Add #alias_target to allow renaming of target object by @reggieb
|
4
|
+
* Fix issue with async calls passing wrong arguments to conditionals
|
5
|
+
* Change TransitionEvent, AsyncCall to be immutable
|
6
|
+
* Add :log_transitions option for easy transition debugging
|
7
|
+
* Increase test coverage to 99%
|
8
|
+
|
1
9
|
0.9.2 (September 27, 2014)
|
2
10
|
|
3
11
|
* Removes use of class variable to share Sync by @reggieb
|
data/README.md
CHANGED
@@ -4,12 +4,14 @@
|
|
4
4
|
[][codeclimate]
|
5
5
|
[][coverage]
|
6
6
|
[][inchpages]
|
7
|
+
[][gitter]
|
7
8
|
|
8
9
|
[gem]: http://badge.fury.io/rb/finite_machine
|
9
10
|
[travis]: http://travis-ci.org/peter-murach/finite_machine
|
10
11
|
[codeclimate]: https://codeclimate.com/github/peter-murach/finite_machine
|
11
12
|
[coverage]: https://coveralls.io/r/peter-murach/finite_machine
|
12
13
|
[inchpages]: http://inch-ci.org/github/peter-murach/finite_machine
|
14
|
+
[gitter]: https://gitter.im/peter-murach/finite_machine?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
13
15
|
|
14
16
|
A minimal finite state machine with a straightforward and intuitive syntax. You can quickly model states and add callbacks that can be triggered synchronously or asynchronously. The machine is event driven with a focus on passing synchronous and asynchronous messages to trigger state transitions.
|
15
17
|
|
@@ -50,7 +52,8 @@ Or install it yourself as:
|
|
50
52
|
* [1.5 can? and cannot?](#15-can-and-cannot)
|
51
53
|
* [1.6 states](#16-states)
|
52
54
|
* [1.7 target](#17-target)
|
53
|
-
* [1.8
|
55
|
+
* [1.8 Alias target](#18-alias-target)
|
56
|
+
* [1.9 restore!](#19-restore)
|
54
57
|
* [2. Transitions](#2-transitions)
|
55
58
|
* [2.1 Performing transitions](#21-performing-transitions)
|
56
59
|
* [2.2 Forcing transitions](#22-forcing-transitions)
|
@@ -59,6 +62,7 @@ Or install it yourself as:
|
|
59
62
|
* [2.5 From :any state](#25-from-any-state)
|
60
63
|
* [2.6 Grouping states under single event](#26-grouping-states-under-single-event)
|
61
64
|
* [2.7 Silent transitions](#27-silent-transitions)
|
65
|
+
* [2.8 Log transitions](#28-log-transitions)
|
62
66
|
* [3. Conditional transitions](#3-conditional-transitions)
|
63
67
|
* [3.1 Using a Proc](#31-using-a-proc)
|
64
68
|
* [3.2 Using a Symbol](#32-using-a-symbol)
|
@@ -371,7 +375,32 @@ fm = FiniteMachine.define do
|
|
371
375
|
end
|
372
376
|
```
|
373
377
|
|
374
|
-
### 1.8
|
378
|
+
### 1.8 Alias target
|
379
|
+
|
380
|
+
If you need to better express the intention behind the target name, in particular when calling actions in callbacks, you can use the `alias_target` helper:
|
381
|
+
|
382
|
+
```ruby
|
383
|
+
car = Car.new
|
384
|
+
|
385
|
+
fm = FiniteMachine.define do
|
386
|
+
initial :neutral
|
387
|
+
|
388
|
+
target car
|
389
|
+
|
390
|
+
alias_target :car
|
391
|
+
|
392
|
+
events {
|
393
|
+
event :start, :neutral => :one, if: "engine_on?"
|
394
|
+
}
|
395
|
+
|
396
|
+
callbacks {
|
397
|
+
on_enter_start do |event| car.turn_engine_on end
|
398
|
+
on_exit_start do |event| car.turn_engine_off end
|
399
|
+
}
|
400
|
+
end
|
401
|
+
```
|
402
|
+
|
403
|
+
### 1.9 restore!
|
375
404
|
|
376
405
|
In order to set the machine to a given state and thus skip triggering callbacks use the `restore!` method:
|
377
406
|
|
@@ -436,7 +465,13 @@ fm.current # => :yellow
|
|
436
465
|
In order to fire the event transition asynchronously use the `async` scope like so
|
437
466
|
|
438
467
|
```ruby
|
439
|
-
fm.async.ready # => executes in separate Thread
|
468
|
+
fm.async.ready('Piotr') # => executes in separate Thread
|
469
|
+
```
|
470
|
+
|
471
|
+
The `async` call allows for alternative syntax whereby the method name is passed as one of the parameters like so:
|
472
|
+
|
473
|
+
```ruby
|
474
|
+
fm.async(:ready, 'Piotr')
|
440
475
|
```
|
441
476
|
|
442
477
|
### 2.4 Multiple from states
|
@@ -526,6 +561,16 @@ fsm.go # no callbacks
|
|
526
561
|
fms.stop # callbacks are fired
|
527
562
|
```
|
528
563
|
|
564
|
+
### 2.8 Log transitions
|
565
|
+
|
566
|
+
To help debug your state machine, **FiniteMachine** provides `:log_transitions` option.
|
567
|
+
|
568
|
+
```ruby
|
569
|
+
FiniteMachine.define log_transitions: true do
|
570
|
+
...
|
571
|
+
end
|
572
|
+
```
|
573
|
+
|
529
574
|
## 3 Conditional transitions
|
530
575
|
|
531
576
|
Each event takes an optional `:if` and `:unless` options which act as a predicate for the transition. The `:if` and `:unless` can take a symbol, a string, a Proc or an array. Use `:if` option when you want to specify when the transition **should** happen. If you want to specify when the transition **should not** happen then use `:unless` option.
|
@@ -546,6 +591,27 @@ fm.slow # doesn't transition to :yellow state
|
|
546
591
|
fm.current # => :green
|
547
592
|
```
|
548
593
|
|
594
|
+
Condition by default receives the current context, which is the current state machine instance, followed by extra arguments.
|
595
|
+
|
596
|
+
```ruby
|
597
|
+
fsm = FiniteMachine.define do
|
598
|
+
initial :red
|
599
|
+
|
600
|
+
events {
|
601
|
+
event :go, :red => :green,
|
602
|
+
if: -> (context, a) { context.current == a }
|
603
|
+
}
|
604
|
+
end
|
605
|
+
fm.go(:yellow) # doesn't transition
|
606
|
+
fm.go # raises ArgumentError
|
607
|
+
```
|
608
|
+
|
609
|
+
**Note** If you specify condition with a given arguments then you need to call an event with the exact number of arguments, otherwise you will get `ArgumentError`. Thus in above scenario to prevent errors specify condition like so:
|
610
|
+
|
611
|
+
```ruby
|
612
|
+
if: -> (context, *args) { ... }
|
613
|
+
```
|
614
|
+
|
549
615
|
Provided your **FiniteMachine** is associated with another object through `target` helper. Then the target object together with event arguments will be passed to the `:if` or `:unless` condition scope.
|
550
616
|
|
551
617
|
```ruby
|
@@ -1231,8 +1297,10 @@ class Account < ActiveRecord::Base
|
|
1231
1297
|
self.state = manage.current
|
1232
1298
|
end
|
1233
1299
|
|
1234
|
-
|
1235
|
-
|
1300
|
+
after_find :restore_state
|
1301
|
+
after_initialize :restore_state
|
1302
|
+
|
1303
|
+
def restore_state
|
1236
1304
|
manage.restore!(state.to_sym) if state.present?
|
1237
1305
|
end
|
1238
1306
|
|
@@ -1251,7 +1319,6 @@ class Account < ActiveRecord::Base
|
|
1251
1319
|
callbacks {
|
1252
1320
|
on_enter do |event|
|
1253
1321
|
target.state = event.to
|
1254
|
-
target.save
|
1255
1322
|
end
|
1256
1323
|
}
|
1257
1324
|
end
|
@@ -2,25 +2,14 @@
|
|
2
2
|
|
3
3
|
module FiniteMachine
|
4
4
|
# An asynchronouse call representation
|
5
|
+
#
|
6
|
+
# Used internally by {EventQueue} to schedule events
|
7
|
+
#
|
8
|
+
# @api private
|
5
9
|
class AsyncCall
|
6
10
|
include Threadable
|
7
11
|
|
8
|
-
|
9
|
-
|
10
|
-
attr_threadsafe :callable
|
11
|
-
|
12
|
-
attr_threadsafe :arguments
|
13
|
-
|
14
|
-
attr_threadsafe :block
|
15
|
-
|
16
|
-
# Create an AsynCall
|
17
|
-
#
|
18
|
-
# @api private
|
19
|
-
def initialize
|
20
|
-
@mutex = Mutex.new
|
21
|
-
end
|
22
|
-
|
23
|
-
# Build asynchronous call instance
|
12
|
+
# Create asynchronous call instance
|
24
13
|
#
|
25
14
|
# @param [Object] context
|
26
15
|
# @param [Callable] callable
|
@@ -28,18 +17,18 @@ module FiniteMachine
|
|
28
17
|
# @param [#call] block
|
29
18
|
#
|
30
19
|
# @example
|
31
|
-
# AsyncCall.
|
20
|
+
# AsyncCall.new(context, Callable.new(:method), :a, :b)
|
32
21
|
#
|
33
22
|
# @return [self]
|
34
23
|
#
|
35
24
|
# @api public
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
25
|
+
def initialize(context, callable, *args, &block)
|
26
|
+
@context = context
|
27
|
+
@callable = callable
|
28
|
+
@arguments = args.dup
|
29
|
+
@block = block
|
30
|
+
@mutex = Mutex.new
|
31
|
+
freeze
|
43
32
|
end
|
44
33
|
|
45
34
|
# Dispatch the event to the context
|
@@ -49,8 +38,18 @@ module FiniteMachine
|
|
49
38
|
# @api private
|
50
39
|
def dispatch
|
51
40
|
@mutex.synchronize do
|
52
|
-
callable.call(context, *arguments, block)
|
41
|
+
callable.call(context, *arguments, &block)
|
53
42
|
end
|
54
43
|
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
attr_threadsafe :context
|
48
|
+
|
49
|
+
attr_threadsafe :callable
|
50
|
+
|
51
|
+
attr_threadsafe :arguments
|
52
|
+
|
53
|
+
attr_threadsafe :block
|
55
54
|
end # AsyncCall
|
56
55
|
end # FiniteMachine
|
@@ -22,7 +22,10 @@ module FiniteMachine
|
|
22
22
|
#
|
23
23
|
# @api private
|
24
24
|
def method_missing(method_name, *args, &block)
|
25
|
-
|
25
|
+
callable = Callable.new(method_name)
|
26
|
+
async_call = AsyncCall.new(context, callable, *args, &block)
|
27
|
+
|
28
|
+
event_queue << async_call
|
26
29
|
end
|
27
30
|
end # AsyncProxy
|
28
31
|
end # FiniteMachine
|
data/lib/finite_machine/dsl.rb
CHANGED
@@ -123,6 +123,27 @@ module FiniteMachine
|
|
123
123
|
end
|
124
124
|
end
|
125
125
|
|
126
|
+
# Use alternative name for target
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# target_alias: :car
|
130
|
+
#
|
131
|
+
# callbacks {
|
132
|
+
# on_transition do |event|
|
133
|
+
# car.state = event.to
|
134
|
+
# end
|
135
|
+
# }
|
136
|
+
#
|
137
|
+
# @param [Symbol] alias_name
|
138
|
+
# the name to alias target to
|
139
|
+
#
|
140
|
+
# @return [FiniteMachine::StateMachine]
|
141
|
+
#
|
142
|
+
# @api public
|
143
|
+
def alias_target(alias_name)
|
144
|
+
machine.env.aliases << alias_name.to_sym
|
145
|
+
end
|
146
|
+
|
126
147
|
# Define terminal state
|
127
148
|
#
|
128
149
|
# @example
|
@@ -170,6 +191,13 @@ module FiniteMachine
|
|
170
191
|
machine.errors.call(&block)
|
171
192
|
end
|
172
193
|
|
194
|
+
# Decide whether to log transitions
|
195
|
+
#
|
196
|
+
# @api public
|
197
|
+
def log_transitions(value)
|
198
|
+
machine.log_transitions = value
|
199
|
+
end
|
200
|
+
|
173
201
|
private
|
174
202
|
|
175
203
|
# Initialize state machine properties based off attributes
|
@@ -179,6 +207,7 @@ module FiniteMachine
|
|
179
207
|
attrs[:initial] and initial(attrs[:initial])
|
180
208
|
attrs[:target] and target(attrs[:target])
|
181
209
|
attrs[:terminal] and terminal(attrs[:terminal])
|
210
|
+
log_transitions(attrs.fetch(:log_transitions, false))
|
182
211
|
end
|
183
212
|
|
184
213
|
# Parse initial options
|
@@ -40,12 +40,7 @@ module FiniteMachine
|
|
40
40
|
#
|
41
41
|
# @api public
|
42
42
|
def <<(event)
|
43
|
-
@mutex.
|
44
|
-
begin
|
45
|
-
@queue << event
|
46
|
-
ensure
|
47
|
-
@mutex.unlock rescue nil
|
48
|
-
end
|
43
|
+
@mutex.synchronize { @queue << event }
|
49
44
|
self
|
50
45
|
end
|
51
46
|
|
@@ -53,7 +48,7 @@ module FiniteMachine
|
|
53
48
|
#
|
54
49
|
# @api public
|
55
50
|
def subscribe(*args, &block)
|
56
|
-
listener = Listener.new
|
51
|
+
listener = Listener.new(*args)
|
57
52
|
listener.on_delivery(&block)
|
58
53
|
@listeners << listener
|
59
54
|
end
|
@@ -103,7 +98,7 @@ module FiniteMachine
|
|
103
98
|
#
|
104
99
|
# @api public
|
105
100
|
def shutdown
|
106
|
-
|
101
|
+
fail EventQueueDeadError, 'event queue already dead' if @dead
|
107
102
|
|
108
103
|
@mutex.lock
|
109
104
|
begin
|
@@ -154,7 +149,7 @@ module FiniteMachine
|
|
154
149
|
event.dispatch
|
155
150
|
end
|
156
151
|
rescue Exception => ex
|
157
|
-
Logger.error "Error while running event: #{ex}"
|
152
|
+
Logger.error "Error while running event: #{Logger.format_error(ex)}"
|
158
153
|
end
|
159
154
|
end # EventQueue
|
160
155
|
end # FiniteMachine
|
@@ -19,5 +19,24 @@ module FiniteMachine
|
|
19
19
|
def error(message)
|
20
20
|
FiniteMachine.logger.error(message)
|
21
21
|
end
|
22
|
+
|
23
|
+
def format_error(error)
|
24
|
+
message = "#{error.class}: #{error.message}\n\t"
|
25
|
+
if error.backtrace
|
26
|
+
message << "occured at #{error.backtrace.join("\n\t")}"
|
27
|
+
else
|
28
|
+
message << "EMPTY BACKTRACE\n\t"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def report_transition(event_transition, *args)
|
33
|
+
message = "Transition: @event=#{event_transition.name} "
|
34
|
+
unless args.empty?
|
35
|
+
message << "@with=[#{args.join(',')}] "
|
36
|
+
end
|
37
|
+
message << "#{event_transition.from_state} -> "
|
38
|
+
message << "#{event_transition.machine.current}"
|
39
|
+
info(message)
|
40
|
+
end
|
22
41
|
end # Logger
|
23
42
|
end # FiniteMachine
|
@@ -126,7 +126,7 @@ module FiniteMachine
|
|
126
126
|
#
|
127
127
|
# @api private
|
128
128
|
def defer(callable, trans_event, *data)
|
129
|
-
async_call = AsyncCall.
|
129
|
+
async_call = AsyncCall.new(machine, callable, trans_event, *data)
|
130
130
|
machine.event_queue << async_call
|
131
131
|
end
|
132
132
|
|
@@ -145,7 +145,7 @@ module FiniteMachine
|
|
145
145
|
# @api private
|
146
146
|
def handle_callback(hook, event)
|
147
147
|
data = event.data
|
148
|
-
trans_event = TransitionEvent.
|
148
|
+
trans_event = TransitionEvent.new(event.transition, *data)
|
149
149
|
callable = create_callable(hook)
|
150
150
|
|
151
151
|
if hook.is_a?(Async)
|
@@ -44,7 +44,11 @@ module FiniteMachine
|
|
44
44
|
# The state machine event definitions
|
45
45
|
attr_threadsafe :events_chain
|
46
46
|
|
47
|
-
|
47
|
+
# Allow or not logging of transitions
|
48
|
+
attr_threadsafe :log_transitions
|
49
|
+
|
50
|
+
def_delegators :@dsl, :initial, :terminal, :target, :trigger_init,
|
51
|
+
:alias_target
|
48
52
|
|
49
53
|
def_delegator :@events_dsl, :event
|
50
54
|
|
@@ -63,7 +67,7 @@ module FiniteMachine
|
|
63
67
|
@observer = Observer.new(self)
|
64
68
|
@transitions = Hash.new { |hash, name| hash[name] = Hash.new }
|
65
69
|
@events_chain = EventsChain.new(self)
|
66
|
-
@env = Environment.new(
|
70
|
+
@env = Environment.new(self, [])
|
67
71
|
@dsl = DSL.new(self, attributes)
|
68
72
|
|
69
73
|
@dsl.call(&block) if block_given?
|
@@ -277,6 +281,7 @@ module FiniteMachine
|
|
277
281
|
|
278
282
|
begin
|
279
283
|
event_transition.call(*args)
|
284
|
+
Logger.report_transition(event_transition, *args) if log_transitions
|
280
285
|
|
281
286
|
notify HookEvent::Transition, event_transition, *args
|
282
287
|
rescue Exception => e
|
@@ -299,8 +304,7 @@ module FiniteMachine
|
|
299
304
|
#
|
300
305
|
# @api private
|
301
306
|
def raise_transition_error(error)
|
302
|
-
fail
|
303
|
-
"occured at #{error.backtrace.join("\n")}")
|
307
|
+
fail TransitionError, Logger.format_error(error)
|
304
308
|
end
|
305
309
|
|
306
310
|
# Forward the message to observer or self
|
@@ -315,6 +319,8 @@ module FiniteMachine
|
|
315
319
|
def method_missing(method_name, *args, &block)
|
316
320
|
if observer.respond_to?(method_name.to_sym)
|
317
321
|
observer.public_send(method_name.to_sym, *args, &block)
|
322
|
+
elsif env.aliases.include?(method_name.to_sym)
|
323
|
+
env.send(:target, *args, &block)
|
318
324
|
else
|
319
325
|
super
|
320
326
|
end
|
@@ -330,7 +336,8 @@ module FiniteMachine
|
|
330
336
|
#
|
331
337
|
# @api private
|
332
338
|
def respond_to_missing?(method_name, include_private = false)
|
333
|
-
observer.respond_to?(method_name.to_sym) ||
|
339
|
+
observer.respond_to?(method_name.to_sym) ||
|
340
|
+
env.aliases.include?(method_name.to_sym) || super
|
334
341
|
end
|
335
342
|
end # StateMachine
|
336
343
|
end # FiniteMachine
|
@@ -7,24 +7,26 @@ module FiniteMachine
|
|
7
7
|
#
|
8
8
|
# @api private
|
9
9
|
class TransitionEvent
|
10
|
+
include Threadable
|
11
|
+
|
10
12
|
# This event from state name
|
11
13
|
#
|
12
14
|
# @return [Object]
|
13
15
|
#
|
14
16
|
# @api public
|
15
|
-
|
17
|
+
attr_threadsafe :from
|
16
18
|
|
17
19
|
# This event to state name
|
18
20
|
#
|
19
21
|
# @return [Object]
|
20
22
|
#
|
21
23
|
# @api public
|
22
|
-
|
24
|
+
attr_threadsafe :to
|
23
25
|
|
24
26
|
# This event name
|
25
27
|
#
|
26
28
|
# @api public
|
27
|
-
|
29
|
+
attr_threadsafe :name
|
28
30
|
|
29
31
|
# Build a transition event
|
30
32
|
#
|
@@ -33,12 +35,11 @@ module FiniteMachine
|
|
33
35
|
# @return [self]
|
34
36
|
#
|
35
37
|
# @api private
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
instance
|
38
|
+
def initialize(transition, *data)
|
39
|
+
@name = transition.name
|
40
|
+
@from = transition.latest_from_state
|
41
|
+
@to = transition.to_state(*data)
|
42
|
+
freeze
|
42
43
|
end
|
43
44
|
end # TransitionEvent
|
44
45
|
end # FiniteMachine
|
data/lib/finite_machine.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -30,7 +30,7 @@ RSpec.configure do |config|
|
|
30
30
|
|
31
31
|
# Remove defined constants
|
32
32
|
config.before :each do
|
33
|
-
[:Car, :
|
33
|
+
[:Car, :DummyLogger, :Bug, :User, :Engine].each do |class_name|
|
34
34
|
if Object.const_defined?(class_name)
|
35
35
|
Object.send(:remove_const, class_name)
|
36
36
|
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe FiniteMachine::Definition, '#alias_target' do
|
6
|
+
|
7
|
+
before do
|
8
|
+
Car = Class.new do
|
9
|
+
def turn_reverse_lights_off
|
10
|
+
@reverse_lights = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def turn_reverse_lights_on
|
14
|
+
@reverse_lights = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def reverse_lights?
|
18
|
+
@reverse_lights ||= false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it "aliases target" do
|
24
|
+
car = Car.new
|
25
|
+
fsm = FiniteMachine.new
|
26
|
+
fsm.target(car)
|
27
|
+
|
28
|
+
expect(fsm.target).to eq(car)
|
29
|
+
expect { fsm.car }.to raise_error(NoMethodError)
|
30
|
+
|
31
|
+
fsm.alias_target(:delorean)
|
32
|
+
expect(fsm.delorean).to eq(car)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "scopes the target alias to a state machine instance" do
|
36
|
+
delorean = Car.new
|
37
|
+
batmobile = Car.new
|
38
|
+
fsm_a = FiniteMachine.new
|
39
|
+
fsm_a.target(delorean)
|
40
|
+
fsm_b = FiniteMachine.new
|
41
|
+
fsm_b.target(batmobile)
|
42
|
+
|
43
|
+
fsm_a.alias_target(:delorean)
|
44
|
+
fsm_b.alias_target(:batmobile)
|
45
|
+
|
46
|
+
expect(fsm_a.delorean).to eq(delorean)
|
47
|
+
expect { fsm_a.batmobile }.to raise_error(NoMethodError)
|
48
|
+
|
49
|
+
expect(fsm_b.batmobile).to eq(batmobile)
|
50
|
+
expect { fsm_b.delorean }.to raise_error(NoMethodError)
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'when inside definition' do
|
54
|
+
before do
|
55
|
+
class Engine < FiniteMachine::Definition
|
56
|
+
initial :neutral
|
57
|
+
|
58
|
+
alias_target :car
|
59
|
+
|
60
|
+
events {
|
61
|
+
event :forward, [:reverse, :neutral] => :one
|
62
|
+
event :shift, :one => :two
|
63
|
+
event :shift, :two => :one
|
64
|
+
event :back, [:neutral, :one] => :reverse
|
65
|
+
}
|
66
|
+
|
67
|
+
callbacks {
|
68
|
+
on_enter :reverse do |event|
|
69
|
+
car.turn_reverse_lights_on
|
70
|
+
end
|
71
|
+
|
72
|
+
on_exit :reverse do |event|
|
73
|
+
car.turn_reverse_lights_off
|
74
|
+
end
|
75
|
+
}
|
76
|
+
|
77
|
+
handlers {
|
78
|
+
handle FiniteMachine::InvalidStateError do |exception| end
|
79
|
+
}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it "creates unique instances" do
|
84
|
+
engine_a = Engine.new
|
85
|
+
engine_b = Engine.new
|
86
|
+
expect(engine_a).not_to be(engine_b)
|
87
|
+
|
88
|
+
engine_a.forward
|
89
|
+
expect(engine_a.current).to eq(:one)
|
90
|
+
expect(engine_b.current).to eq(:neutral)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "allows to create standalone machine" do
|
94
|
+
car = Car.new
|
95
|
+
engine = Engine.new
|
96
|
+
engine.target car
|
97
|
+
expect(engine.current).to eq(:neutral)
|
98
|
+
|
99
|
+
engine.forward
|
100
|
+
expect(engine.current).to eq(:one)
|
101
|
+
expect(car.reverse_lights?).to be false
|
102
|
+
|
103
|
+
engine.back
|
104
|
+
expect(engine.current).to eq(:reverse)
|
105
|
+
expect(car.reverse_lights?).to be true
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -10,8 +10,8 @@ describe FiniteMachine, 'async_events' do
|
|
10
10
|
initial :green
|
11
11
|
|
12
12
|
events {
|
13
|
-
event :slow,
|
14
|
-
event :stop,
|
13
|
+
event :slow, :green => :yellow
|
14
|
+
event :stop, :yellow => :red
|
15
15
|
event :ready, :red => :yellow
|
16
16
|
event :go, :yellow => :green
|
17
17
|
}
|
@@ -38,6 +38,38 @@ describe FiniteMachine, 'async_events' do
|
|
38
38
|
])
|
39
39
|
end
|
40
40
|
|
41
|
+
it 'correctly passes parameters to conditionals' do
|
42
|
+
called = []
|
43
|
+
fsm = FiniteMachine.define do
|
44
|
+
events {
|
45
|
+
event :go, :none => :green,
|
46
|
+
if: proc { |context, arg|
|
47
|
+
called << "cond_none_green(#{context},#{arg})"; true
|
48
|
+
}
|
49
|
+
|
50
|
+
event :stop, from: :any do
|
51
|
+
choice :red, if: proc { |context, arg|
|
52
|
+
called << "cond_any_red(#{context},#{arg})"; true
|
53
|
+
}
|
54
|
+
end
|
55
|
+
}
|
56
|
+
end
|
57
|
+
expect(fsm.current).to eql(:none)
|
58
|
+
fsm.async.go(:foo)
|
59
|
+
fsm.event_queue.join 0.01
|
60
|
+
expect(fsm.current).to eql(:green)
|
61
|
+
expect(called).to eql(["cond_none_green(#{fsm},foo)"])
|
62
|
+
|
63
|
+
expect(fsm.current).to eql(:green)
|
64
|
+
fsm.async.stop(:bar)
|
65
|
+
fsm.event_queue.join 0.01
|
66
|
+
expect(fsm.current).to eql(:red)
|
67
|
+
expect(called).to match_array([
|
68
|
+
"cond_none_green(#{fsm},foo)",
|
69
|
+
"cond_any_red(#{fsm},bar)"
|
70
|
+
])
|
71
|
+
end
|
72
|
+
|
41
73
|
it "ensure queue per thread" do
|
42
74
|
called = []
|
43
75
|
fsmFoo = FiniteMachine.define do
|
File without changes
|
data/spec/unit/handlers_spec.rb
CHANGED
@@ -5,7 +5,7 @@ require 'spec_helper'
|
|
5
5
|
describe FiniteMachine, 'handlers' do
|
6
6
|
|
7
7
|
before(:each) {
|
8
|
-
|
8
|
+
DummyLogger = Class.new do
|
9
9
|
attr_reader :result
|
10
10
|
|
11
11
|
def log_error(exception)
|
@@ -44,7 +44,7 @@ describe FiniteMachine, 'handlers' do
|
|
44
44
|
end
|
45
45
|
|
46
46
|
it 'allows for :with to be symbol' do
|
47
|
-
logger =
|
47
|
+
logger = DummyLogger.new
|
48
48
|
fsm = FiniteMachine.define do
|
49
49
|
initial :green
|
50
50
|
|
@@ -67,7 +67,7 @@ describe FiniteMachine, 'handlers' do
|
|
67
67
|
end
|
68
68
|
|
69
69
|
it 'allows for error type as string' do
|
70
|
-
logger =
|
70
|
+
logger = DummyLogger.new
|
71
71
|
called = []
|
72
72
|
fsm = FiniteMachine.define do
|
73
73
|
initial :green
|
data/spec/unit/if_unless_spec.rb
CHANGED
@@ -27,6 +27,64 @@ describe FiniteMachine, ':if, :unless' do
|
|
27
27
|
end
|
28
28
|
}
|
29
29
|
|
30
|
+
it "passes context to conditionals" do
|
31
|
+
called = []
|
32
|
+
fsm = FiniteMachine.define do
|
33
|
+
initial :red
|
34
|
+
|
35
|
+
events {
|
36
|
+
event :go, :red => :green,
|
37
|
+
if: proc { |context| called << "cond_red_green(#{context})"; true}
|
38
|
+
event :stop, from: :any do
|
39
|
+
choice :red,
|
40
|
+
if: proc { |context| called << "cond_any_red(#{context})"; true }
|
41
|
+
end
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
expect(fsm.current).to eq(:red)
|
46
|
+
|
47
|
+
fsm.go
|
48
|
+
expect(fsm.current).to eq(:green)
|
49
|
+
expect(called).to eq(["cond_red_green(#{fsm})"])
|
50
|
+
|
51
|
+
fsm.stop
|
52
|
+
expect(fsm.current).to eq(:red)
|
53
|
+
expect(called).to match_array([
|
54
|
+
"cond_red_green(#{fsm})",
|
55
|
+
"cond_any_red(#{fsm})"
|
56
|
+
])
|
57
|
+
end
|
58
|
+
|
59
|
+
it "passes context & arguments to conditionals" do
|
60
|
+
called = []
|
61
|
+
fsm = FiniteMachine.define do
|
62
|
+
initial :red
|
63
|
+
|
64
|
+
events {
|
65
|
+
event :go, :red => :green,
|
66
|
+
if: proc { |_, a| called << "cond_red_green(#{a})"; true }
|
67
|
+
event :stop, from: :any do
|
68
|
+
choice :red,
|
69
|
+
if: proc { |_, b| called << "cond_any_red(#{b})"; true }
|
70
|
+
end
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
expect(fsm.current).to eq(:red)
|
75
|
+
|
76
|
+
fsm.go(:foo)
|
77
|
+
expect(fsm.current).to eq(:green)
|
78
|
+
expect(called).to eq(["cond_red_green(foo)"])
|
79
|
+
|
80
|
+
fsm.stop(:bar)
|
81
|
+
expect(fsm.current).to eq(:red)
|
82
|
+
expect(called).to match_array([
|
83
|
+
"cond_red_green(foo)",
|
84
|
+
"cond_any_red(bar)"
|
85
|
+
])
|
86
|
+
end
|
87
|
+
|
30
88
|
it "allows to cancel event with :if option" do
|
31
89
|
called = []
|
32
90
|
|
@@ -5,7 +5,7 @@ require 'spec_helper'
|
|
5
5
|
describe FiniteMachine, 'initialize' do
|
6
6
|
|
7
7
|
before(:each) {
|
8
|
-
|
8
|
+
DummyLogger = Class.new do
|
9
9
|
attr_accessor :level
|
10
10
|
|
11
11
|
def initialize
|
@@ -128,7 +128,7 @@ describe FiniteMachine, 'initialize' do
|
|
128
128
|
end
|
129
129
|
|
130
130
|
it "evaluates initial state" do
|
131
|
-
logger =
|
131
|
+
logger = DummyLogger.new
|
132
132
|
fsm = FiniteMachine.define do
|
133
133
|
initial logger.level
|
134
134
|
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe FiniteMachine, 'log_transitions' do
|
6
|
+
let(:output) { StringIO.new('', 'w+')}
|
7
|
+
|
8
|
+
before { FiniteMachine.logger = ::Logger.new(output) }
|
9
|
+
|
10
|
+
after { FiniteMachine.logger = ::Logger.new($stderr) }
|
11
|
+
|
12
|
+
it "logs transitions" do
|
13
|
+
fsm = FiniteMachine.define log_transitions: true do
|
14
|
+
initial :green
|
15
|
+
|
16
|
+
events {
|
17
|
+
event :slow, :green => :yellow
|
18
|
+
event :stop, :yellow => :red
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
fsm.slow
|
23
|
+
output.rewind
|
24
|
+
expect(output.read).to match(/Transition: @event=slow green -> yellow/)
|
25
|
+
|
26
|
+
fsm.stop(1, 2)
|
27
|
+
output.rewind
|
28
|
+
expect(output.read).to match(/Transition: @event=stop @with=\[1,2\] yellow -> red/)
|
29
|
+
end
|
30
|
+
end
|
data/spec/unit/logger_spec.rb
CHANGED
@@ -30,4 +30,11 @@ describe FiniteMachine::Logger do
|
|
30
30
|
expect(log).to receive(:error).with(message)
|
31
31
|
logger.error(message)
|
32
32
|
end
|
33
|
+
|
34
|
+
it "reports transition" do
|
35
|
+
expect(log).to receive(:info).with("Transition: @event=go red -> green")
|
36
|
+
machine = double(:machine, current: :green)
|
37
|
+
transition = double(:transition, name: "go", from_state: :red, machine: machine)
|
38
|
+
logger.report_transition(transition)
|
39
|
+
end
|
33
40
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: finite_machine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Murach
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-11-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -75,12 +75,14 @@ files:
|
|
75
75
|
- lib/finite_machine/two_phase_lock.rb
|
76
76
|
- lib/finite_machine/version.rb
|
77
77
|
- spec/spec_helper.rb
|
78
|
+
- spec/unit/alias_target_spec.rb
|
78
79
|
- spec/unit/async_events_spec.rb
|
79
80
|
- spec/unit/callable/call_spec.rb
|
80
81
|
- spec/unit/callbacks_spec.rb
|
81
82
|
- spec/unit/can_spec.rb
|
82
83
|
- spec/unit/choice_spec.rb
|
83
84
|
- spec/unit/define_spec.rb
|
85
|
+
- spec/unit/definition_spec.rb
|
84
86
|
- spec/unit/event/add_spec.rb
|
85
87
|
- spec/unit/event/eql_spec.rb
|
86
88
|
- spec/unit/event/initialize_spec.rb
|
@@ -103,9 +105,9 @@ files:
|
|
103
105
|
- spec/unit/initialize_spec.rb
|
104
106
|
- spec/unit/inspect_spec.rb
|
105
107
|
- spec/unit/is_spec.rb
|
108
|
+
- spec/unit/log_transitions_spec.rb
|
106
109
|
- spec/unit/logger_spec.rb
|
107
110
|
- spec/unit/respond_to_spec.rb
|
108
|
-
- spec/unit/standalone_spec.rb
|
109
111
|
- spec/unit/state_parser/inspect_spec.rb
|
110
112
|
- spec/unit/state_parser/parse_states_spec.rb
|
111
113
|
- spec/unit/states_spec.rb
|
@@ -143,12 +145,14 @@ specification_version: 4
|
|
143
145
|
summary: A minimal finite state machine with a straightforward syntax.
|
144
146
|
test_files:
|
145
147
|
- spec/spec_helper.rb
|
148
|
+
- spec/unit/alias_target_spec.rb
|
146
149
|
- spec/unit/async_events_spec.rb
|
147
150
|
- spec/unit/callable/call_spec.rb
|
148
151
|
- spec/unit/callbacks_spec.rb
|
149
152
|
- spec/unit/can_spec.rb
|
150
153
|
- spec/unit/choice_spec.rb
|
151
154
|
- spec/unit/define_spec.rb
|
155
|
+
- spec/unit/definition_spec.rb
|
152
156
|
- spec/unit/event/add_spec.rb
|
153
157
|
- spec/unit/event/eql_spec.rb
|
154
158
|
- spec/unit/event/initialize_spec.rb
|
@@ -171,9 +175,9 @@ test_files:
|
|
171
175
|
- spec/unit/initialize_spec.rb
|
172
176
|
- spec/unit/inspect_spec.rb
|
173
177
|
- spec/unit/is_spec.rb
|
178
|
+
- spec/unit/log_transitions_spec.rb
|
174
179
|
- spec/unit/logger_spec.rb
|
175
180
|
- spec/unit/respond_to_spec.rb
|
176
|
-
- spec/unit/standalone_spec.rb
|
177
181
|
- spec/unit/state_parser/inspect_spec.rb
|
178
182
|
- spec/unit/state_parser/parse_states_spec.rb
|
179
183
|
- spec/unit/states_spec.rb
|