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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 64cf549e74a02a5036aec535a7be9784a10224bf
4
- data.tar.gz: e7733f5805b8989033e229109200b7f69feebe3a
3
+ metadata.gz: f3f7827c0cdd099708b2dd384b7d7acfe82449d8
4
+ data.tar.gz: 1aa5b2f9964b04c5ff1c7d642acd6eb67b985411
5
5
  SHA512:
6
- metadata.gz: db31c094295a9ed657daca0960b3299d00ee131a3149ce99750d14b44555e0e5375e5f0e3b5c0d70ecf955a895cd4addafc1b9e99576dd03a21df9ca2b271547
7
- data.tar.gz: bff4f5c620276cf7cc664662d0c3aa66e555d1e09fd242e9d22ab5577f0425ee94c4db7bf804e4102a2e99d759eb1180fc04988ccff3fca2368879d4cc02ab1d
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 restore!](#18-restore)
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 restore!
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
- def initialize(attrs = {})
1235
- super
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
- attr_threadsafe :context
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.build(self, Callable.new(:method), :a, :b)
20
+ # AsyncCall.new(context, Callable.new(:method), :a, :b)
32
21
  #
33
22
  # @return [self]
34
23
  #
35
24
  # @api public
36
- def self.build(context, callable, *args, &block)
37
- instance = new
38
- instance.context = context
39
- instance.callable = callable
40
- instance.arguments = *args
41
- instance.block = block
42
- instance
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
- event_queue << AsyncCall.build(context, Callable.new(method_name), *args, &block)
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
@@ -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.lock
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
- raise EventQueueDeadError, "event queue already dead" if @dead
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
@@ -3,6 +3,13 @@
3
3
  module FiniteMachine
4
4
  # A generic listener interface
5
5
  class Listener
6
+ # Initialize a listener
7
+ #
8
+ # @api private
9
+ def initialize(*args)
10
+ @name = args.unshift
11
+ end
12
+
6
13
  # Define event delivery handler
7
14
  #
8
15
  # @api public
@@ -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.build(machine, callable, trans_event, *data)
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.build(event.transition, *data)
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
- def_delegators :@dsl, :initial, :terminal, :target, :trigger_init
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(target: self)
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(TransitionError, "#(#{error.class}): #{error.message}\n" \
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) || super
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
- attr_accessor :from
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
- attr_accessor :to
24
+ attr_threadsafe :to
23
25
 
24
26
  # This event name
25
27
  #
26
28
  # @api public
27
- attr_accessor :name
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 self.build(transition, *data)
37
- instance = new
38
- instance.name = transition.name
39
- instance.from = transition.latest_from_state
40
- instance.to = transition.to_state(*data)
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
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
- VERSION = "0.9.2"
4
+ VERSION = "0.10.0"
5
5
  end
@@ -78,7 +78,7 @@ module FiniteMachine
78
78
  # Raised when argument is already defined
79
79
  AlreadyDefinedError = Class.new(::ArgumentError)
80
80
 
81
- Environment = Struct.new(:target)
81
+ Environment = Struct.new(:target, :aliases)
82
82
 
83
83
  class << self
84
84
  attr_accessor :logger
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, :Logger, :Bug, :User, :Engine].each do |class_name|
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, :green => :yellow
14
- event :stop, :yellow => :red
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
@@ -5,7 +5,7 @@ require 'spec_helper'
5
5
  describe FiniteMachine, 'handlers' do
6
6
 
7
7
  before(:each) {
8
- Logger = Class.new do
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 = Logger.new
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 = Logger.new
70
+ logger = DummyLogger.new
71
71
  called = []
72
72
  fsm = FiniteMachine.define do
73
73
  initial :green
@@ -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
- Logger = Class.new do
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 = Logger.new
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
@@ -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.9.2
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-09-27 00:00:00.000000000 Z
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