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 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