finite_machine 0.9.2 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Code Climate](https://codeclimate.com/github/peter-murach/finite_machine.png)][codeclimate]
|
5
5
|
[![Coverage Status](https://coveralls.io/repos/peter-murach/finite_machine/badge.png)][coverage]
|
6
6
|
[![Inline docs](http://inch-ci.org/github/peter-murach/finite_machine.png)][inchpages]
|
7
|
+
[![Gitter](https://badges.gitter.im/Join Chat.svg)][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
|