stoplight 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +73 -27
- data/lib/stoplight.rb +19 -8
- data/lib/stoplight/data_store/base.rb +22 -8
- data/lib/stoplight/data_store/memory.rb +54 -29
- data/lib/stoplight/data_store/redis.rb +46 -36
- data/lib/stoplight/error.rb +1 -3
- data/lib/stoplight/light.rb +14 -12
- data/lib/stoplight/mixin.rb +9 -0
- data/lib/stoplight/notifier.rb +6 -0
- data/lib/stoplight/notifier/base.rb +11 -0
- data/lib/stoplight/notifier/hip_chat.rb +27 -0
- data/lib/stoplight/notifier/standard_error.rb +11 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/stoplight/data_store/base_spec.rb +2 -0
- data/spec/stoplight/data_store/memory_spec.rb +1 -181
- data/spec/stoplight/data_store/redis_spec.rb +3 -182
- data/spec/stoplight/light_spec.rb +23 -3
- data/spec/stoplight/mixin_spec.rb +35 -0
- data/spec/stoplight/notifier/base_spec.rb +21 -0
- data/spec/stoplight/notifier/hip_chat_spec.rb +21 -0
- data/spec/stoplight/notifier/standard_error_spec.rb +18 -0
- data/spec/stoplight/notifier_spec.rb +6 -0
- data/spec/stoplight_spec.rb +33 -3
- data/spec/support/data_store.rb +178 -0
- metadata +23 -6
@@ -4,187 +4,8 @@ require 'spec_helper'
|
|
4
4
|
require 'fakeredis'
|
5
5
|
|
6
6
|
describe Stoplight::DataStore::Redis do
|
7
|
-
|
8
|
-
let(:
|
9
|
-
let(:name) { SecureRandom.hex }
|
10
|
-
let(:state) { Stoplight::DataStore::STATES.to_a.sample }
|
11
|
-
let(:threshold) { rand(10) }
|
7
|
+
subject(:data_store) { described_class.new(redis) }
|
8
|
+
let(:redis) { Redis.new }
|
12
9
|
|
13
|
-
|
14
|
-
|
15
|
-
describe '#attempts' do
|
16
|
-
subject(:result) { data_store.attempts(name) }
|
17
|
-
|
18
|
-
it 'returns 0' do
|
19
|
-
expect(result).to eql(0)
|
20
|
-
end
|
21
|
-
|
22
|
-
context 'with an attempt' do
|
23
|
-
before { data_store.record_attempt(name) }
|
24
|
-
|
25
|
-
it 'returns 1' do
|
26
|
-
expect(result).to eql(1)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
describe '#clear_attempts' do
|
32
|
-
subject(:result) { data_store.clear_attempts(name) }
|
33
|
-
|
34
|
-
it 'returns 0' do
|
35
|
-
expect(result).to eql(0)
|
36
|
-
end
|
37
|
-
|
38
|
-
context 'with an attempt' do
|
39
|
-
before { data_store.record_attempt(name) }
|
40
|
-
|
41
|
-
it 'returns 1' do
|
42
|
-
expect(result).to eql(1)
|
43
|
-
end
|
44
|
-
|
45
|
-
it 'clears the attempts' do
|
46
|
-
result
|
47
|
-
expect(data_store.attempts(name)).to eql(0)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
describe '#clear_failures' do
|
53
|
-
subject(:result) { data_store.clear_failures(name) }
|
54
|
-
|
55
|
-
it 'returns 0' do
|
56
|
-
expect(result).to eql(0)
|
57
|
-
end
|
58
|
-
|
59
|
-
context 'with a failure' do
|
60
|
-
before { data_store.record_failure(name, error) }
|
61
|
-
|
62
|
-
it 'returns 1' do
|
63
|
-
expect(result).to eql(1)
|
64
|
-
end
|
65
|
-
|
66
|
-
it 'clears the failures' do
|
67
|
-
result
|
68
|
-
expect(data_store.failures(name)).to be_empty
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
describe '#failures' do
|
74
|
-
subject(:result) { data_store.failures(name) }
|
75
|
-
|
76
|
-
it 'returns an empty array' do
|
77
|
-
expect(result).to be_an(Array)
|
78
|
-
expect(result).to be_empty
|
79
|
-
end
|
80
|
-
|
81
|
-
context 'with a failure' do
|
82
|
-
before { data_store.record_failure(name, error) }
|
83
|
-
|
84
|
-
it 'returns a non-empty array' do
|
85
|
-
expect(result).to be_an(Array)
|
86
|
-
expect(result).to_not be_empty
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
describe '#names' do
|
92
|
-
subject(:result) { data_store.names }
|
93
|
-
|
94
|
-
it 'returns an array' do
|
95
|
-
expect(result).to be_an(Array)
|
96
|
-
end
|
97
|
-
|
98
|
-
context 'with a name' do
|
99
|
-
before { data_store.set_threshold(name, threshold) }
|
100
|
-
|
101
|
-
it 'includes the name' do
|
102
|
-
expect(result).to include(name)
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
describe '#record_attempt' do
|
108
|
-
subject(:result) { data_store.record_attempt(name) }
|
109
|
-
|
110
|
-
it 'returns 1' do
|
111
|
-
expect(result).to eql(1)
|
112
|
-
end
|
113
|
-
|
114
|
-
it 'records the attempt' do
|
115
|
-
result
|
116
|
-
expect(data_store.attempts(name)).to eql(1)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
describe '#record_failure' do
|
121
|
-
subject(:result) { data_store.record_failure(name, error) }
|
122
|
-
|
123
|
-
it 'returns 1' do
|
124
|
-
expect(result).to eql(1)
|
125
|
-
end
|
126
|
-
|
127
|
-
it 'records the failure' do
|
128
|
-
result
|
129
|
-
expect(data_store.failures(name)).to_not be_empty
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
describe '#set_state' do
|
134
|
-
subject(:result) { data_store.set_state(name, state) }
|
135
|
-
|
136
|
-
it 'returns the state' do
|
137
|
-
expect(result).to eql(state)
|
138
|
-
end
|
139
|
-
|
140
|
-
it 'sets the state' do
|
141
|
-
result
|
142
|
-
expect(data_store.state(name)).to eql(state)
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
describe '#set_threshold' do
|
147
|
-
subject(:result) { data_store.set_threshold(name, threshold) }
|
148
|
-
|
149
|
-
it 'returns the threshold' do
|
150
|
-
expect(result).to eql(threshold)
|
151
|
-
end
|
152
|
-
|
153
|
-
it 'sets the threshold' do
|
154
|
-
result
|
155
|
-
expect(data_store.threshold(name)).to eql(threshold)
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
describe '#state' do
|
160
|
-
subject(:result) { data_store.state(name) }
|
161
|
-
|
162
|
-
it 'returns the default state' do
|
163
|
-
expect(result).to eql(Stoplight::DataStore::STATE_UNLOCKED)
|
164
|
-
end
|
165
|
-
|
166
|
-
context 'with a state' do
|
167
|
-
before { data_store.set_state(name, state) }
|
168
|
-
|
169
|
-
it 'returns the state' do
|
170
|
-
expect(result).to eql(state)
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
describe '#threshold' do
|
176
|
-
subject(:result) { data_store.threshold(name) }
|
177
|
-
|
178
|
-
it 'returns nil' do
|
179
|
-
expect(result).to be(nil)
|
180
|
-
end
|
181
|
-
|
182
|
-
context 'with a threshold' do
|
183
|
-
before { data_store.set_threshold(name, threshold) }
|
184
|
-
|
185
|
-
it 'returns the threshold' do
|
186
|
-
expect(result).to eql(threshold)
|
187
|
-
end
|
188
|
-
end
|
189
|
-
end
|
10
|
+
it_behaves_like 'a data store'
|
190
11
|
end
|
@@ -89,6 +89,9 @@ describe Stoplight::Light do
|
|
89
89
|
before do
|
90
90
|
light.with_fallback(&fallback)
|
91
91
|
allow(light).to receive(:green?).and_return(false)
|
92
|
+
Stoplight.notifiers.each do |notifier|
|
93
|
+
allow(notifier).to receive(:notify)
|
94
|
+
end
|
92
95
|
end
|
93
96
|
|
94
97
|
it 'runs the fallback' do
|
@@ -99,6 +102,24 @@ describe Stoplight::Light do
|
|
99
102
|
result
|
100
103
|
expect(Stoplight.data_store.attempts(name)).to eql(1)
|
101
104
|
end
|
105
|
+
|
106
|
+
it 'notifies' do
|
107
|
+
result
|
108
|
+
Stoplight.notifiers.each do |notifier|
|
109
|
+
expect(notifier).to have_received(:notify)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'with an attempt' do
|
114
|
+
before { allow(Stoplight).to receive(:attempts).and_return(1) }
|
115
|
+
|
116
|
+
it 'does not notify' do
|
117
|
+
result
|
118
|
+
Stoplight.notifiers.each do |notifier|
|
119
|
+
expect(notifier).to_not have_received(:notify)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
102
123
|
end
|
103
124
|
end
|
104
125
|
|
@@ -149,7 +170,7 @@ describe Stoplight::Light do
|
|
149
170
|
subject(:result) { light.fallback }
|
150
171
|
|
151
172
|
it 'uses the default fallback' do
|
152
|
-
expect { result }.to raise_error(Stoplight::Error::
|
173
|
+
expect { result }.to raise_error(Stoplight::Error::RedLight)
|
153
174
|
end
|
154
175
|
end
|
155
176
|
|
@@ -215,8 +236,7 @@ describe Stoplight::Light do
|
|
215
236
|
subject(:result) { light.threshold }
|
216
237
|
|
217
238
|
it 'uses the default threshold' do
|
218
|
-
expect(result).to eql(
|
219
|
-
described_class.const_get(:DEFAULT_THRESHOLD))
|
239
|
+
expect(result).to eql(Stoplight::DEFAULT_THRESHOLD)
|
220
240
|
end
|
221
241
|
end
|
222
242
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Stoplight::Mixin do
|
6
|
+
subject(:klass) { Class.new.extend(described_class) }
|
7
|
+
|
8
|
+
describe '#stoplight' do
|
9
|
+
subject(:result) { klass.stoplight(name, &code) }
|
10
|
+
let(:name) { SecureRandom.hex }
|
11
|
+
let(:code) { proc { code_result } }
|
12
|
+
let(:code_result) { double }
|
13
|
+
|
14
|
+
let(:light) { double }
|
15
|
+
|
16
|
+
before do
|
17
|
+
allow(Stoplight::Light).to receive(:new).and_return(light)
|
18
|
+
allow(light).to receive(:run).and_return(code.call)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'calls .new' do
|
22
|
+
expect(Stoplight::Light).to receive(:new)
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'calls #run' do
|
27
|
+
expect(light).to receive(:run)
|
28
|
+
result
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'returns the result of #run' do
|
32
|
+
expect(result).to eql(code_result)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Stoplight::Notifier::Base do
|
6
|
+
subject(:notifier) { described_class.new }
|
7
|
+
|
8
|
+
%w(
|
9
|
+
notify
|
10
|
+
).each do |method|
|
11
|
+
it "responds to #{method}" do
|
12
|
+
expect(notifier).to respond_to(method)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "does not implement #{method}" do
|
16
|
+
args = [nil] * notifier.method(method).arity
|
17
|
+
expect { notifier.public_send(method, *args) }.to raise_error(
|
18
|
+
NotImplementedError)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Stoplight::Notifier::HipChat do
|
6
|
+
subject(:notifier) { described_class.new(client, room, options) }
|
7
|
+
let(:client) { double }
|
8
|
+
let(:room) { SecureRandom.hex }
|
9
|
+
let(:options) { {} }
|
10
|
+
|
11
|
+
describe '#notify' do
|
12
|
+
subject(:result) { notifier.notify(message) }
|
13
|
+
let(:message) { SecureRandom.hex }
|
14
|
+
|
15
|
+
it 'sends the message to HipChat' do
|
16
|
+
expect(client).to receive(:[]).with(room).and_return(client)
|
17
|
+
expect(client).to receive(:send)
|
18
|
+
result
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Stoplight::Notifier::StandardError do
|
6
|
+
subject(:notifier) { described_class.new }
|
7
|
+
|
8
|
+
describe '#notify' do
|
9
|
+
let(:message) { SecureRandom.hex }
|
10
|
+
|
11
|
+
subject(:result) { notifier.notify(message) }
|
12
|
+
|
13
|
+
it 'emits the message as a warning' do
|
14
|
+
expect(notifier).to receive(:warn).with(message)
|
15
|
+
result
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/spec/stoplight_spec.rb
CHANGED
@@ -35,10 +35,10 @@ describe Stoplight do
|
|
35
35
|
|
36
36
|
before do
|
37
37
|
@data_store = described_class.data_store
|
38
|
-
described_class.data_store
|
38
|
+
described_class.data_store = data_store
|
39
39
|
end
|
40
40
|
|
41
|
-
after { described_class.data_store
|
41
|
+
after { described_class.data_store = @data_store }
|
42
42
|
|
43
43
|
it 'returns the data store' do
|
44
44
|
expect(result).to eql(data_store)
|
@@ -46,6 +46,36 @@ describe Stoplight do
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
+
describe '.notifiers' do
|
50
|
+
subject(:result) { described_class.notifiers }
|
51
|
+
|
52
|
+
it 'uses the default notifier' do
|
53
|
+
expect(result).to be_an(Array)
|
54
|
+
expect(result.size).to eql(1)
|
55
|
+
expect(result.first).to be_a(Stoplight::Notifier::StandardError)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'memoizes the result' do
|
59
|
+
expect(result).to be described_class.notifiers
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'with custom notifiers' do
|
63
|
+
let(:notifiers) { [notifier] }
|
64
|
+
let(:notifier) { double }
|
65
|
+
|
66
|
+
before do
|
67
|
+
@notifiers = described_class.notifiers
|
68
|
+
described_class.notifiers = notifiers
|
69
|
+
end
|
70
|
+
|
71
|
+
after { described_class.notifiers = @notifiers }
|
72
|
+
|
73
|
+
it 'returns the notifiers' do
|
74
|
+
expect(result).to eql(notifiers)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
49
79
|
describe '.green?' do
|
50
80
|
subject(:result) { described_class.green?(name) }
|
51
81
|
|
@@ -112,7 +142,7 @@ describe Stoplight do
|
|
112
142
|
subject(:result) { described_class.threshold(name) }
|
113
143
|
|
114
144
|
it 'uses the default threshold' do
|
115
|
-
expect(result).to eql(Stoplight::
|
145
|
+
expect(result).to eql(Stoplight::DEFAULT_THRESHOLD)
|
116
146
|
end
|
117
147
|
|
118
148
|
context 'with a custom threshold' do
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
shared_examples_for 'a data store' do
|
4
|
+
let(:error) { error_class.new }
|
5
|
+
let(:error_class) { Class.new(StandardError) }
|
6
|
+
let(:name) { SecureRandom.hex }
|
7
|
+
let(:state) { Stoplight::DataStore::STATES.to_a.sample }
|
8
|
+
let(:threshold) { rand(10) }
|
9
|
+
|
10
|
+
it 'is a DataStore::Base' do
|
11
|
+
expect(data_store).to be_a(Stoplight::DataStore::Base)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#names' do
|
15
|
+
subject(:result) { data_store.names }
|
16
|
+
|
17
|
+
it 'returns an array' do
|
18
|
+
expect(result).to be_an(Array)
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'with a name' do
|
22
|
+
before do
|
23
|
+
@data_store = Stoplight.data_store
|
24
|
+
Stoplight.data_store = data_store
|
25
|
+
Stoplight::Light.new(name) {}.run
|
26
|
+
end
|
27
|
+
after { Stoplight.data_store = @data_store }
|
28
|
+
|
29
|
+
it 'includes the name' do
|
30
|
+
expect(result).to include(name)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'attempts' do
|
36
|
+
describe '#attempts' do
|
37
|
+
subject(:result) { data_store.attempts(name) }
|
38
|
+
|
39
|
+
it 'returns an integer' do
|
40
|
+
expect(result).to be_an(Integer)
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'with an attempt' do
|
44
|
+
it 'includes the attempt' do
|
45
|
+
attempts = data_store.attempts(name)
|
46
|
+
data_store.record_attempt(name)
|
47
|
+
expect(result).to be > attempts
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#clear_attempts' do
|
53
|
+
subject(:result) { data_store.clear_attempts(name) }
|
54
|
+
|
55
|
+
context 'with an attempt' do
|
56
|
+
before { data_store.record_attempt(name) }
|
57
|
+
|
58
|
+
it 'clears the attempts' do
|
59
|
+
result
|
60
|
+
expect(data_store.attempts(name)).to eql(0)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe '#record_attempt' do
|
66
|
+
subject(:result) { data_store.record_attempt(name) }
|
67
|
+
|
68
|
+
it 'records the attempt' do
|
69
|
+
attempts = data_store.attempts(name)
|
70
|
+
result
|
71
|
+
expect(data_store.attempts(name)).to eql(attempts + 1)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'failures' do
|
77
|
+
describe '#clear_failures' do
|
78
|
+
subject(:result) { data_store.clear_failures(name) }
|
79
|
+
|
80
|
+
context 'with a failure' do
|
81
|
+
before { data_store.record_failure(name, error) }
|
82
|
+
|
83
|
+
it 'clears the failures' do
|
84
|
+
result
|
85
|
+
expect(data_store.failures(name)).to be_empty
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#failures' do
|
91
|
+
subject(:result) { data_store.failures(name) }
|
92
|
+
|
93
|
+
it 'returns an array' do
|
94
|
+
expect(result).to be_an(Array)
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'with a failure' do
|
98
|
+
it 'includes the failure' do
|
99
|
+
failures = data_store.failures(name)
|
100
|
+
data_store.record_failure(name, error)
|
101
|
+
expect(result.size).to be > failures.size
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe '#record_failure' do
|
107
|
+
subject(:result) { data_store.record_failure(name, error) }
|
108
|
+
|
109
|
+
it 'records the failure' do
|
110
|
+
failures = data_store.failures(name)
|
111
|
+
result
|
112
|
+
expect(data_store.failures(name).size).to eql(failures.size + 1)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'state' do
|
118
|
+
describe '#set_state' do
|
119
|
+
subject(:result) { data_store.set_state(name, state) }
|
120
|
+
|
121
|
+
it 'returns the state' do
|
122
|
+
expect(result).to eql(state)
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'sets the state' do
|
126
|
+
result
|
127
|
+
expect(data_store.state(name)).to eql(state)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe '#state' do
|
132
|
+
subject(:result) { data_store.state(name) }
|
133
|
+
|
134
|
+
it 'returns the default state' do
|
135
|
+
expect(result).to eql(Stoplight::DataStore::STATE_UNLOCKED)
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'with a state' do
|
139
|
+
before { data_store.set_state(name, state) }
|
140
|
+
|
141
|
+
it 'returns the state' do
|
142
|
+
expect(result).to eql(state)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context 'threshold' do
|
149
|
+
describe '#set_threshold' do
|
150
|
+
subject(:result) { data_store.set_threshold(name, threshold) }
|
151
|
+
|
152
|
+
it 'returns the threshold' do
|
153
|
+
expect(result).to eql(threshold)
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'sets the threshold' do
|
157
|
+
result
|
158
|
+
expect(data_store.threshold(name)).to eql(threshold)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
describe '#threshold' do
|
163
|
+
subject(:result) { data_store.threshold(name) }
|
164
|
+
|
165
|
+
it 'returns nil' do
|
166
|
+
expect(result).to eql(nil)
|
167
|
+
end
|
168
|
+
|
169
|
+
context 'with a threshold' do
|
170
|
+
before { data_store.set_threshold(name, threshold) }
|
171
|
+
|
172
|
+
it 'returns the threshold' do
|
173
|
+
expect(result).to eql(threshold)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|