state_machines-rspec 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,119 @@
1
+ require 'spec_helper'
2
+
3
+ class Vehicle
4
+ attr_accessor :seatbelt_on, :time_used, :auto_shop_busy
5
+
6
+ state_machine :state, :initial => :parked do
7
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
8
+
9
+ after_transition :on => :crash, :do => :tow
10
+ after_transition :on => :repair, :do => :fix
11
+ after_transition any => :parked do |vehicle, transition|
12
+ vehicle.seatbelt_on = false
13
+ end
14
+
15
+ after_failure :on => :ignite, :do => :log_start_failure
16
+
17
+ around_transition do |vehicle, transition, block|
18
+ start = Time.now
19
+ block.call
20
+ vehicle.time_used += Time.now - start
21
+ end
22
+
23
+ event :park do
24
+ transition [:idling, :first_gear] => :parked
25
+ end
26
+
27
+ event :ignite do
28
+ transition :stalled => same, :parked => :idling
29
+ end
30
+
31
+ event :idle do
32
+ transition :first_gear => :idling
33
+ end
34
+
35
+ event :shift_up do
36
+ transition :idling => :first_gear, :first_gear => :second_gear, :second_gear => :third_gear
37
+ end
38
+
39
+ event :shift_down do
40
+ transition :third_gear => :second_gear, :second_gear => :first_gear
41
+ end
42
+
43
+ event :crash do
44
+ transition all - [:parked, :stalled] => :stalled, :if => lambda {|vehicle| !vehicle.passed_inspection?}
45
+ end
46
+
47
+ event :repair do
48
+ # The first transition that matches the state and passes its conditions
49
+ # will be used
50
+ transition :stalled => :parked, :unless => :auto_shop_busy
51
+ transition :stalled => same
52
+ end
53
+
54
+ state :parked do
55
+ def speed
56
+ 0
57
+ end
58
+ end
59
+
60
+ state :idling, :first_gear do
61
+ def speed
62
+ 10
63
+ end
64
+ end
65
+
66
+ state all - [:parked, :stalled, :idling] do
67
+ def moving?
68
+ true
69
+ end
70
+ end
71
+
72
+ state :parked, :stalled, :idling do
73
+ def moving?
74
+ false
75
+ end
76
+ end
77
+ end
78
+
79
+ state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
80
+ event :enable do
81
+ transition all => :active
82
+ end
83
+
84
+ event :disable do
85
+ transition all => :off
86
+ end
87
+
88
+ state :active, :value => 1
89
+ state :off, :value => 0
90
+ end
91
+
92
+ def initialize
93
+ @seatbelt_on = false
94
+ @time_used = 0
95
+ @auto_shop_busy = true
96
+ super() # NOTE: This *must* be called, otherwise states won't get initialized
97
+ end
98
+
99
+ def put_on_seatbelt
100
+ @seatbelt_on = true
101
+ end
102
+
103
+ def passed_inspection?
104
+ false
105
+ end
106
+
107
+ def tow
108
+ # tow the vehicle
109
+ end
110
+
111
+ def fix
112
+ # get the vehicle fixed by a mechanic
113
+ end
114
+
115
+ def log_start_failure
116
+ # log a failed attempt to start the vehicle
117
+ end
118
+ end
119
+
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+
3
+ describe StateMachinesRspec::Matchers::HandleEventMatcher do
4
+ describe '#matches?' do
5
+ context 'when :when state is specified' do
6
+ context 'but the state doesn\'t exist' do
7
+ before do
8
+ matcher_class = Class.new do
9
+ state_machine :state, initial: :mathy
10
+ end
11
+ @matcher_subject = matcher_class.new
12
+ @matcher = described_class.new([when: :artsy])
13
+ end
14
+
15
+ it 'raises' do
16
+ expect { @matcher.matches? @matcher_subject }.
17
+ to raise_error StateMachinesIntrospectorError
18
+ end
19
+ end
20
+
21
+ context 'and that state exists' do
22
+ before do
23
+ matcher_class = Class.new do
24
+ state_machine :state, initial: :mathy do
25
+ state :artsy
26
+ end
27
+ end
28
+ @matcher_subject = matcher_class.new
29
+ @matcher = described_class.new([when: :artsy])
30
+ end
31
+
32
+ it 'sets the state' do
33
+ @matcher.matches? @matcher_subject
34
+ expect(@matcher_subject.state).to eq 'artsy'
35
+ end
36
+ end
37
+ end
38
+
39
+ context 'when subject can perform events' do
40
+ before do
41
+ matcher_class = Class.new do
42
+ state_machine :mathiness, initial: :mathy do
43
+ event(:mathematize) { transition any => same }
44
+ end
45
+ end
46
+ @matcher_subject = matcher_class.new
47
+ @matcher = described_class.new([:mathematize, on: :mathiness])
48
+ end
49
+
50
+ it 'does not set a failure message' do
51
+ @matcher.matches? @matcher_subject
52
+ expect(@matcher.failure_message).to be_nil
53
+ end
54
+ it 'returns true' do
55
+ expect(@matcher.matches?(@matcher_subject)).to be_truthy
56
+ end
57
+ end
58
+
59
+ context 'when subject cannot perform events' do
60
+ before do
61
+ matcher_class = Class.new do
62
+ state_machine :state, initial: :mathy do
63
+ state :polynomial
64
+
65
+ event(:mathematize) { transition any => same }
66
+ event(:algebraify) { transition :polynomial => same }
67
+ event(:trigonomalize) { transition :trigonomalize => same }
68
+ end
69
+ end
70
+ @matcher_subject = matcher_class.new
71
+ end
72
+
73
+ context 'because it cannot perform the transition' do
74
+ before do
75
+ @matcher = described_class.new([:mathematize, :algebraify, :trigonomalize])
76
+ end
77
+
78
+ it 'sets a failure message' do
79
+ @matcher.matches? @matcher_subject
80
+ expect(@matcher.failure_message).to eq('Expected to be able to handle events: algebraify, trigonomalize ' +
81
+ 'in state: mathy')
82
+ end
83
+ it 'returns false' do
84
+ expect(@matcher.matches?(@matcher_subject)).to be_falsey
85
+ end
86
+ end
87
+
88
+ context 'because no such events exist' do
89
+ before do
90
+ @matcher = described_class.new([:polynomialize, :eulerasterize])
91
+ end
92
+
93
+ it 'does not raise' do
94
+ expect { @matcher.matches?(@matcher_subject) }.not_to raise_error
95
+ end
96
+ it 'sets a failure message' do
97
+ @matcher.matches? @matcher_subject
98
+ expect(@matcher.failure_message).to eq('state_machine: state does not ' +
99
+ 'define events: polynomialize, eulerasterize')
100
+ end
101
+ it 'returns false' do
102
+ expect(@matcher.matches?(@matcher_subject)).to be_falsey
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ describe '#description' do
109
+ context 'with no options' do
110
+ let(:matcher) { described_class.new([:placate, :mollify]) }
111
+
112
+ it 'returns a string description' do
113
+ expect(matcher.description).to eq('handle :placate, :mollify')
114
+ end
115
+ end
116
+
117
+ context 'when :when state is specified' do
118
+ let(:matcher) { described_class.new([:destroy_food, when: :hangry]) }
119
+
120
+ it 'mentions the requisite state' do
121
+ expect(matcher.description).to eq('handle :destroy_food when :hangry')
122
+ end
123
+ end
124
+
125
+ context 'when :on is specified' do
126
+ let(:matcher) { described_class.new([:ensmarmify, on: :tired_investors]) }
127
+
128
+ it 'mentions the state machines variable' do
129
+ expect(matcher.description).to eq('handle :ensmarmify on :tired_investors')
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+
3
+ describe StateMachinesRspec::Matchers::RejectEventMatcher do
4
+ describe '#matches?' do
5
+ context 'when :when state is specified' do
6
+ context 'but that state doesn\'t exist' do
7
+ before do
8
+ matcher_class = Class.new do
9
+ state_machine :state, initial: :sleazy
10
+ end
11
+ @matcher_subject = matcher_class.new
12
+ @matcher = described_class.new([when: :sneezy])
13
+ end
14
+
15
+ it 'raises' do
16
+ expect { @matcher.matches? @matcher_subject }.
17
+ to raise_error StateMachinesIntrospectorError
18
+ end
19
+ end
20
+
21
+ context 'and that state exists' do
22
+ before do
23
+ matcher_class = Class.new do
24
+ state_machine :state, initial: :sleazy do
25
+ state :sneezy
26
+ end
27
+ end
28
+ @matcher_subject = matcher_class.new
29
+ @matcher = described_class.new([when: :sneezy])
30
+ end
31
+
32
+ it 'sets the state' do
33
+ @matcher.matches? @matcher_subject
34
+ expect(@matcher_subject.state).to eq('sneezy')
35
+ end
36
+ end
37
+ end
38
+
39
+ context 'when an expectation is made on an event that is undefined' do
40
+ before do
41
+ matcher_class = Class.new do
42
+ state_machine :state, initial: :snarky do
43
+ event(:primmadonnalize) { transition any => same }
44
+ end
45
+ end
46
+ @matcher_subject = matcher_class.new
47
+ @matcher = described_class.new([:primmadonnalize, :martinilunchitize])
48
+ end
49
+
50
+ it 'does not raise' do
51
+ expect { @matcher.matches?(@matcher_subject) }.not_to raise_error
52
+ end
53
+ it 'sets a failure message' do
54
+ @matcher.matches? @matcher_subject
55
+ expect(@matcher.failure_message).to eq('state_machine: state does not ' +
56
+ 'define events: martinilunchitize')
57
+ end
58
+ it 'returns false' do
59
+ expect(@matcher.matches?(@matcher_subject)).to be_falsey
60
+ end
61
+ end
62
+
63
+ context 'when subject cannot perform any of the specified events' do
64
+ before do
65
+ matcher_class = Class.new do
66
+ state_machine :state, initial: :snarky do
67
+ state :haughty
68
+ event(:primmadonnalize) { transition :haughty => same }
69
+ end
70
+ end
71
+ @matcher_subject = matcher_class.new
72
+ @matcher = described_class.new([:primmadonnalize])
73
+ end
74
+
75
+ it 'does not set a failure message' do
76
+ @matcher.matches? @matcher_subject
77
+ expect(@matcher.failure_message).to be_nil
78
+ end
79
+ it 'returns true' do
80
+ expect(@matcher.matches?(@matcher_subject)).to be_truthy
81
+ end
82
+ end
83
+
84
+ context 'when subject can perform any one of the specified events' do
85
+ before do
86
+ matcher_class = Class.new do
87
+ state_machine :state, initial: :snarky do
88
+ state :haughty
89
+ event(:primmadonnalize) { transition :haughty => same }
90
+ event(:defer_to_management) { transition any => same }
91
+ end
92
+ end
93
+ @matcher_subject = matcher_class.new
94
+ @matcher = described_class.new([:primmadonnalize, :defer_to_management])
95
+ end
96
+
97
+ it 'sets a failure message' do
98
+ @matcher.matches? @matcher_subject
99
+ expect(@matcher.failure_message).to eq('Did not expect to be able to handle events: defer_to_management ' +
100
+ 'in state: snarky')
101
+ end
102
+ it 'returns false' do
103
+ expect(@matcher.matches?(@matcher_subject)).to be_falsey
104
+ end
105
+ end
106
+ end
107
+
108
+ describe '#description' do
109
+ context 'with no options' do
110
+ let(:matcher) { described_class.new([:makeadealify, :hustlinate]) }
111
+
112
+ it 'returns a string description' do
113
+ expect(matcher.description).to eq('reject :makeadealify, :hustlinate')
114
+ end
115
+ end
116
+
117
+ context 'when :when state is specified' do
118
+ let(:matcher) { described_class.new([:begargle, when: :sleep_encrusted]) }
119
+
120
+ it 'mentions the requisite state' do
121
+ expect(matcher.description).to eq('reject :begargle when :sleep_encrusted')
122
+ end
123
+ end
124
+
125
+ context 'when :on is specified' do
126
+ let(:matcher) { described_class.new([:harrangue, on: :suspicious_crowd]) }
127
+
128
+ it 'mentions the state machines variable' do
129
+ expect(matcher.description).to eq('reject :harrangue on :suspicious_crowd')
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,136 @@
1
+ require 'spec_helper'
2
+
3
+ describe StateMachinesRspec::Matchers::HaveStateMatcher do
4
+ describe '#matches?' do
5
+ before { @matcher = described_class.new([:rad, :not_so_rad, { on: :radical_state }]) }
6
+
7
+ context 'when values are asserted on multiple states' do
8
+ before do
9
+ @matcher = described_class.new([:rad, :not_so_rad, { value: 'rad' }])
10
+ end
11
+ it 'raises an ArgumentError' do
12
+ expect { @matcher.matches? nil }.to raise_error ArgumentError,
13
+ 'cannot make value assertions on multiple states at once'
14
+ end
15
+ end
16
+
17
+ context 'when class does not have a matching state attribute' do
18
+ before do
19
+ @class = Class.new do
20
+ state_machine :bodacious_state, initial: :super_bodacious
21
+ end
22
+ end
23
+
24
+ it 'raises' do
25
+ expect { @matcher.matches? @class.new }.
26
+ to raise_error StateMachinesIntrospectorError,
27
+ /.+? does not have a state machine defined on radical_state/
28
+ end
29
+ end
30
+
31
+ context 'when class has a matching state attribute' do
32
+ context 'but is missing some of the specified states' do
33
+ before do
34
+ @class = Class.new do
35
+ state_machine :radical_state do
36
+ state :not_so_rad
37
+ end
38
+ end
39
+ end
40
+
41
+ it 'sets a failure message indicating a state is missing' do
42
+ @matcher.matches? @class.new
43
+ expect(@matcher.failure_message).to eq 'Expected radical_state to allow states: rad'
44
+ end
45
+ it 'returns false' do
46
+ expect(@matcher.matches?(@class.new)).to be_falsey
47
+ end
48
+ end
49
+
50
+ context 'and has all states specified' do
51
+ before do
52
+ @class = Class.new do
53
+ state_machine :radical_state do
54
+ state :rad, value: 'totes rad'
55
+ state :not_so_rad, value: 'meh'
56
+ end
57
+ end
58
+ end
59
+
60
+ context 'state values not specified' do
61
+ it 'does not set a failure message' do
62
+ @matcher.matches? @class.new
63
+ expect(@matcher.failure_message).to be_nil
64
+ end
65
+ it 'returns true' do
66
+ expect(@matcher.matches?(@class.new)).to be_truthy
67
+ end
68
+ end
69
+
70
+ context 'state value matches specified value' do
71
+ before do
72
+ @matcher = described_class.new([:rad, { on: :radical_state, value: 'uber-rad' }])
73
+ @class = Class.new do
74
+ state_machine :radical_state do
75
+ state :rad, value: 'uber-rad'
76
+ end
77
+ end
78
+ end
79
+
80
+ it 'does not set a failure message' do
81
+ @matcher.matches? @class.new
82
+ expect(@matcher.failure_message).to be_nil
83
+ end
84
+ it 'returns true' do
85
+ expect(@matcher.matches?(@class.new)).to be_truthy
86
+ end
87
+ end
88
+
89
+ context 'state value does not match specified value' do
90
+ before do
91
+ @matcher = described_class.new([:rad, { on: :radical_state, value: 'uber-rad' }])
92
+ @class = Class.new do
93
+ state_machine :radical_state do
94
+ state :rad, value: 'kinda rad'
95
+ end
96
+ end
97
+ end
98
+
99
+ it 'does not set a failure message' do
100
+ @matcher.matches? @class.new
101
+ expect(@matcher.failure_message).to eq 'Expected rad to have value uber-rad'
102
+ end
103
+ it 'returns true' do
104
+ expect(@matcher.matches?(@class.new)).to be_falsey
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ describe '#description' do
112
+ context 'with no options' do
113
+ let(:matcher) { described_class.new([:fancy_shirt, :cracked_toenail]) }
114
+
115
+ it 'returns a string description' do
116
+ expect(matcher.description).to eq('have :fancy_shirt, :cracked_toenail')
117
+ end
118
+ end
119
+
120
+ context 'when :value is specified' do
121
+ let(:matcher) { described_class.new([:mustache, value: :really_shady]) }
122
+
123
+ it 'mentions the requisite state' do
124
+ expect(matcher.description).to eq('have :mustache == :really_shady')
125
+ end
126
+ end
127
+
128
+ context 'when :on state machines is specified' do
129
+ let(:matcher) { described_class.new([:lunch, on: :tuesday]) }
130
+
131
+ it 'mentions the state machines variable' do
132
+ expect(matcher.description).to eq('have :lunch on :tuesday')
133
+ end
134
+ end
135
+ end
136
+ end