state_machine_rspec 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.rvmrc +52 -0
- data/Gemfile +8 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +56 -0
- data/Rakefile +6 -0
- data/lib/matchers/events/handle_event.rb +29 -0
- data/lib/matchers/events/matcher.rb +55 -0
- data/lib/matchers/events/reject_event.rb +29 -0
- data/lib/matchers/states/have_state.rb +40 -0
- data/lib/matchers/states/matcher.rb +42 -0
- data/lib/matchers/states/reject_state.rb +28 -0
- data/lib/state_machine_rspec/state_machine_introspector.rb +70 -0
- data/lib/state_machine_rspec/version.rb +3 -0
- data/lib/state_machine_rspec.rb +11 -0
- data/spec/integration/integration_spec.rb +327 -0
- data/spec/integration/models/vehicle.rb +119 -0
- data/spec/matchers/events/handle_event_spec.rb +109 -0
- data/spec/matchers/events/reject_event_spec.rb +109 -0
- data/spec/matchers/states/have_state_spec.rb +110 -0
- data/spec/matchers/states/reject_state_spec.rb +81 -0
- data/spec/spec_helper.rb +12 -0
- data/state_machine_rspec.gemspec +31 -0
- metadata +228 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# This is an RVM Project .rvmrc file, used to automatically load the ruby
|
4
|
+
# development environment upon cd'ing into the directory
|
5
|
+
|
6
|
+
# First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
|
7
|
+
# Only full ruby name is supported here, for short names use:
|
8
|
+
# echo "rvm use 1.9.3" > .rvmrc
|
9
|
+
environment_id="ruby-1.9.3-p392@state_machine_rspec"
|
10
|
+
|
11
|
+
# Uncomment the following lines if you want to verify rvm version per project
|
12
|
+
# rvmrc_rvm_version="1.15.5 ()" # 1.10.1 seams as a safe start
|
13
|
+
# eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
|
14
|
+
# echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
|
15
|
+
# return 1
|
16
|
+
# }
|
17
|
+
|
18
|
+
# First we attempt to load the desired environment directly from the environment
|
19
|
+
# file. This is very fast and efficient compared to running through the entire
|
20
|
+
# CLI and selector. If you want feedback on which environment was used then
|
21
|
+
# insert the word 'use' after --create as this triggers verbose mode.
|
22
|
+
if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
|
23
|
+
&& -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
|
24
|
+
then
|
25
|
+
\. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
|
26
|
+
[[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
|
27
|
+
\. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
|
28
|
+
if [[ $- == *i* ]] # check for interactive shells
|
29
|
+
then echo "Using: $(tput setaf 2)$GEM_HOME$(tput sgr0)" # show the user the ruby and gemset they are using in green
|
30
|
+
else echo "Using: $GEM_HOME" # don't use colors in non-interactive shells
|
31
|
+
fi
|
32
|
+
else
|
33
|
+
# If the environment file has not yet been created, use the RVM CLI to select.
|
34
|
+
rvm --create use "$environment_id" || {
|
35
|
+
echo "Failed to create RVM environment '${environment_id}'."
|
36
|
+
return 1
|
37
|
+
}
|
38
|
+
fi
|
39
|
+
|
40
|
+
# If you use bundler, this might be useful to you:
|
41
|
+
# if [[ -s Gemfile ]] && {
|
42
|
+
# ! builtin command -v bundle >/dev/null ||
|
43
|
+
# builtin command -v bundle | GREP_OPTIONS= \grep $rvm_path/bin/bundle >/dev/null
|
44
|
+
# }
|
45
|
+
# then
|
46
|
+
# printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n"
|
47
|
+
# gem install bundler
|
48
|
+
# fi
|
49
|
+
# if [[ -s Gemfile ]] && builtin command -v bundle >/dev/null
|
50
|
+
# then
|
51
|
+
# bundle install | GREP_OPTIONS= \grep -vE '^Using|Your bundle is complete'
|
52
|
+
# fi
|
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 modocache
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# state_machine_rspec
|
2
|
+
|
3
|
+
Custom matchers for [pluginaweek/state_machine](https://github.com/pluginaweek/state_machine).
|
4
|
+
|
5
|
+
|
6
|
+
## Matchers
|
7
|
+
|
8
|
+
### `have_state` & `reject_state`
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
describe Vehicle do
|
12
|
+
it { should have_states :parked, :idling, :stalled, :first_gear,
|
13
|
+
:second_gear, :third_gear }
|
14
|
+
it { should reject_state :flying }
|
15
|
+
|
16
|
+
it { should have_states :active, :off, on: :alarm_state }
|
17
|
+
it { should have_state :active, on: :alarm_state, value: 1 }
|
18
|
+
it { should reject_states :broken, :ringing, on: :alarm_state }
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
### `handle_event` & `reject_event`
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
describe Vehicle do
|
26
|
+
it { should handle_events :shift_down, :crash, when: :third_gear }
|
27
|
+
it { should reject_events :park, :ignite, :idle, :shift_up, :repair,
|
28
|
+
when: :third_gear }
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
|
33
|
+
## Installation
|
34
|
+
|
35
|
+
Add these lines to your application's Gemfile:
|
36
|
+
|
37
|
+
group :test do
|
38
|
+
gem 'state_machine_rspec'
|
39
|
+
end
|
40
|
+
|
41
|
+
And then execute:
|
42
|
+
|
43
|
+
$ bundle
|
44
|
+
|
45
|
+
Or install it yourself as:
|
46
|
+
|
47
|
+
$ gem install state_machine_rspec
|
48
|
+
|
49
|
+
|
50
|
+
## Contributing
|
51
|
+
|
52
|
+
1. Fork it
|
53
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
54
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
55
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
56
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'matchers/events/matcher'
|
2
|
+
|
3
|
+
module StateMachineRspec
|
4
|
+
module Matchers
|
5
|
+
def handle_events(value, *values)
|
6
|
+
HandleEventMatcher.new(values.unshift(value))
|
7
|
+
end
|
8
|
+
alias_method :handle_event, :handle_events
|
9
|
+
|
10
|
+
class HandleEventMatcher < StateMachineRspec::Matchers::Events::Matcher
|
11
|
+
def matches_events?(events)
|
12
|
+
!invalid_events?
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def invalid_events?
|
18
|
+
invalid_events = @introspector.invalid_events(@events)
|
19
|
+
unless invalid_events.empty?
|
20
|
+
@failure_message = "Expected to be able to handle events: " +
|
21
|
+
"#{invalid_events.join(', ')} in state: " +
|
22
|
+
"#{@introspector.current_state_value}"
|
23
|
+
end
|
24
|
+
|
25
|
+
!invalid_events.empty?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'active_support/core_ext/array/extract_options'
|
2
|
+
|
3
|
+
module StateMachineRspec
|
4
|
+
module Matchers
|
5
|
+
module Events
|
6
|
+
class Matcher
|
7
|
+
attr_reader :failure_message
|
8
|
+
|
9
|
+
def initialize(events)
|
10
|
+
@options = events.extract_options!
|
11
|
+
@events = events
|
12
|
+
end
|
13
|
+
|
14
|
+
def matches?(subject)
|
15
|
+
@subject = subject
|
16
|
+
@introspector = StateMachineIntrospector.new(@subject,
|
17
|
+
@options.fetch(:state, nil))
|
18
|
+
enter_when_state
|
19
|
+
return false if undefined_events?
|
20
|
+
return false unless matches_events?(@events)
|
21
|
+
@failure_message.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
def matches_events?(events)
|
25
|
+
raise NotImplementedError,
|
26
|
+
"subclasses of #{self.class} must override matches_events?"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def enter_when_state
|
32
|
+
if state_name = @options.fetch(:when, nil)
|
33
|
+
unless when_state = @introspector.state(state_name)
|
34
|
+
raise StateMachineIntrospectorError,
|
35
|
+
"#{@subject.class} does not define state: #{state_name}"
|
36
|
+
end
|
37
|
+
|
38
|
+
@subject.send("#{@introspector.state_machine_attribute}=",
|
39
|
+
when_state.value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def undefined_events?
|
44
|
+
undefined_events = @introspector.undefined_events(@events)
|
45
|
+
unless undefined_events.empty?
|
46
|
+
@failure_message = "state_machine: #{@introspector.state_machine_attribute} " +
|
47
|
+
"does not define events: #{undefined_events.join(', ')}"
|
48
|
+
end
|
49
|
+
|
50
|
+
!undefined_events.empty?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'matchers/events/matcher'
|
2
|
+
|
3
|
+
module StateMachineRspec
|
4
|
+
module Matchers
|
5
|
+
def reject_events(value, *values)
|
6
|
+
RejectEventMatcher.new(values.unshift(value))
|
7
|
+
end
|
8
|
+
alias_method :reject_event, :reject_events
|
9
|
+
|
10
|
+
class RejectEventMatcher < StateMachineRspec::Matchers::Events::Matcher
|
11
|
+
def matches_events?(events)
|
12
|
+
!valid_events?
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def valid_events?
|
18
|
+
valid_events = @introspector.valid_events(@events)
|
19
|
+
unless valid_events.empty?
|
20
|
+
@failure_message = "Did not expect to be able to handle events: " +
|
21
|
+
"#{valid_events.join(', ')} in state: " +
|
22
|
+
"#{@introspector.current_state_value}"
|
23
|
+
end
|
24
|
+
|
25
|
+
!valid_events.empty?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'matchers/states/matcher'
|
2
|
+
|
3
|
+
module StateMachineRspec
|
4
|
+
module Matchers
|
5
|
+
def have_states(state, *states)
|
6
|
+
HaveStateMatcher.new(states.unshift(state))
|
7
|
+
end
|
8
|
+
alias_method :have_state, :have_states
|
9
|
+
|
10
|
+
class HaveStateMatcher < StateMachineRspec::Matchers::States::Matcher
|
11
|
+
def matches_states?(states)
|
12
|
+
return false if undefined_states?
|
13
|
+
return false if incorrect_value?
|
14
|
+
@failure_message.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def undefined_states?
|
20
|
+
undefined_states = @introspector.undefined_states(@states)
|
21
|
+
unless undefined_states.empty?
|
22
|
+
@failure_message = "Expected #{@introspector.state_machine_attribute} " +
|
23
|
+
"to allow states: #{undefined_states.join(', ')}"
|
24
|
+
end
|
25
|
+
|
26
|
+
!undefined_states.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def incorrect_value?
|
30
|
+
state_value = @options.fetch(:value, nil)
|
31
|
+
if state_value && @introspector.state(@states.first).value != state_value
|
32
|
+
@failure_message = "Expected #{@states.first} to have value #{state_value}"
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'active_support/core_ext/array/extract_options'
|
2
|
+
|
3
|
+
module StateMachineRspec
|
4
|
+
module Matchers
|
5
|
+
module States
|
6
|
+
class Matcher
|
7
|
+
attr_reader :failure_message
|
8
|
+
|
9
|
+
def initialize(states)
|
10
|
+
@options = states.extract_options!
|
11
|
+
@states = states
|
12
|
+
end
|
13
|
+
|
14
|
+
def matches?(subject)
|
15
|
+
raise_if_multiple_values
|
16
|
+
|
17
|
+
@subject = subject
|
18
|
+
@introspector = StateMachineIntrospector.new(@subject,
|
19
|
+
@options.fetch(:on, nil))
|
20
|
+
|
21
|
+
return false unless matches_states?(@states)
|
22
|
+
@failure_message.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
def matches_states?(states)
|
26
|
+
raise NotImplementedError,
|
27
|
+
"subclasses of #{self.class} must override matches_states?"
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def raise_if_multiple_values
|
33
|
+
if @states.count > 1 && @options.fetch(:value, nil)
|
34
|
+
raise ArgumentError, 'cannot make value assertions on ' +
|
35
|
+
'multiple states at once'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'matchers/states/matcher'
|
2
|
+
|
3
|
+
module StateMachineRspec
|
4
|
+
module Matchers
|
5
|
+
def reject_states(state, *states)
|
6
|
+
RejectStateMatcher.new(states.unshift(state))
|
7
|
+
end
|
8
|
+
alias_method :reject_state, :reject_states
|
9
|
+
|
10
|
+
class RejectStateMatcher < StateMachineRspec::Matchers::States::Matcher
|
11
|
+
def matches_states?(states)
|
12
|
+
no_defined_states?
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def no_defined_states?
|
18
|
+
defined_states = @introspector.defined_states(@states)
|
19
|
+
unless defined_states.empty?
|
20
|
+
@failure_message = "Did not expect #{@introspector.state_machine_attribute} " +
|
21
|
+
"to allow states: #{defined_states.join(', ')}"
|
22
|
+
end
|
23
|
+
|
24
|
+
defined_states.empty?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class StateMachineIntrospector
|
2
|
+
def initialize(subject, state_machine_name=nil)
|
3
|
+
@subject = subject
|
4
|
+
@state_machine_name = state_machine_name
|
5
|
+
end
|
6
|
+
|
7
|
+
def state_machine_attribute
|
8
|
+
state_machine.attribute
|
9
|
+
end
|
10
|
+
|
11
|
+
def current_state_value
|
12
|
+
@subject.send(state_machine_attribute)
|
13
|
+
end
|
14
|
+
|
15
|
+
def state(name)
|
16
|
+
state = state_machine.states.find { |s| s.name == name }
|
17
|
+
end
|
18
|
+
|
19
|
+
def undefined_states(states)
|
20
|
+
states.reject { |s| state_defined? s }
|
21
|
+
end
|
22
|
+
|
23
|
+
def defined_states(states)
|
24
|
+
states.keep_if { |s| state_defined? s }
|
25
|
+
end
|
26
|
+
|
27
|
+
def undefined_events(events)
|
28
|
+
events.reject { |e| event_defined? e }
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid_events(events)
|
32
|
+
events.keep_if { |e| valid_event? e }
|
33
|
+
end
|
34
|
+
|
35
|
+
def invalid_events(events)
|
36
|
+
events.reject { |e| valid_event? e }
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def state_machine
|
42
|
+
if @state_machine_name
|
43
|
+
unless machine = @subject.class.state_machines[@state_machine_name]
|
44
|
+
raise StateMachineIntrospectorError,
|
45
|
+
"#{@subject.class} does not have a state machine defined " +
|
46
|
+
"on #{@state_machine_name}"
|
47
|
+
end
|
48
|
+
else
|
49
|
+
machine = @subject.class.state_machine
|
50
|
+
end
|
51
|
+
|
52
|
+
machine
|
53
|
+
end
|
54
|
+
|
55
|
+
def state_defined?(state_name)
|
56
|
+
state(state_name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def event_defined?(event)
|
60
|
+
@subject.respond_to? "can_#{event}?"
|
61
|
+
end
|
62
|
+
|
63
|
+
def valid_event?(event)
|
64
|
+
@subject.send("can_#{event}?")
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
class StateMachineIntrospectorError < StandardError
|
70
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'state_machine_rspec/version'
|
2
|
+
require 'state_machine_rspec/state_machine_introspector'
|
3
|
+
require 'matchers/events/handle_event'
|
4
|
+
require 'matchers/events/reject_event'
|
5
|
+
require 'matchers/states/have_state'
|
6
|
+
require 'matchers/states/reject_state'
|
7
|
+
|
8
|
+
module StateMachineRspec
|
9
|
+
module Matchers
|
10
|
+
end
|
11
|
+
end
|