stoplight 0.2.1 → 0.3.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 +6 -0
- data/README.md +73 -24
- data/lib/stoplight.rb +1 -47
- data/lib/stoplight/data_store.rb +144 -6
- data/lib/stoplight/data_store/base.rb +88 -33
- data/lib/stoplight/data_store/memory.rb +80 -36
- data/lib/stoplight/data_store/redis.rb +120 -30
- data/lib/stoplight/error.rb +16 -0
- data/lib/stoplight/failure.rb +24 -11
- data/lib/stoplight/light.rb +66 -37
- data/lib/stoplight/notifier/hip_chat.rb +7 -9
- data/lib/stoplight/notifier/standard_error.rb +7 -1
- data/spec/stoplight/data_store/base_spec.rb +18 -8
- data/spec/stoplight/data_store_spec.rb +60 -0
- data/spec/stoplight/failure_spec.rb +35 -9
- data/spec/stoplight/light_spec.rb +86 -206
- data/spec/stoplight/notifier/hip_chat_spec.rb +14 -1
- data/spec/stoplight/notifier/standard_error_spec.rb +16 -4
- data/spec/stoplight_spec.rb +0 -86
- data/spec/support/data_store.rb +36 -170
- metadata +29 -13
@@ -4,23 +4,21 @@ module Stoplight
|
|
4
4
|
module Notifier
|
5
5
|
# @note hipchat ~> 1.3.0
|
6
6
|
class HipChat < Base
|
7
|
+
DEFAULT_FORMAT = '@all %s'
|
8
|
+
DEFAULT_OPTIONS = { color: 'red', message_format: 'text', notify: true }
|
9
|
+
|
7
10
|
# @param client [HipChat::Client]
|
8
11
|
# @param room [String]
|
9
12
|
# @param options [Hash]
|
10
|
-
def initialize(client, room, options = {})
|
13
|
+
def initialize(client, room, format = nil, options = {})
|
11
14
|
@client = client
|
12
15
|
@room = room
|
13
|
-
@
|
16
|
+
@format = format || DEFAULT_FORMAT
|
17
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
14
18
|
end
|
15
19
|
|
16
20
|
def notify(message)
|
17
|
-
@client[@room].send('Stoplight',
|
18
|
-
end
|
19
|
-
|
20
|
-
private
|
21
|
-
|
22
|
-
def default_options
|
23
|
-
{ color: 'red', message_format: 'text', notify: true }
|
21
|
+
@client[@room].send('Stoplight', @format % message, @options)
|
24
22
|
end
|
25
23
|
end
|
26
24
|
end
|
@@ -3,8 +3,14 @@
|
|
3
3
|
module Stoplight
|
4
4
|
module Notifier
|
5
5
|
class StandardError < Base
|
6
|
+
DEFAULT_FORMAT = '%s'
|
7
|
+
|
8
|
+
def initialize(format = nil)
|
9
|
+
@format = format || DEFAULT_FORMAT
|
10
|
+
end
|
11
|
+
|
6
12
|
def notify(message)
|
7
|
-
warn(message)
|
13
|
+
warn(@format % message)
|
8
14
|
end
|
9
15
|
end
|
10
16
|
end
|
@@ -6,19 +6,29 @@ describe Stoplight::DataStore::Base do
|
|
6
6
|
subject(:data_store) { described_class.new }
|
7
7
|
|
8
8
|
%w(
|
9
|
-
attempts
|
10
|
-
clear_attempts
|
11
|
-
clear_failures
|
12
|
-
delete
|
13
|
-
failures
|
14
9
|
names
|
15
|
-
|
10
|
+
clear_stale
|
11
|
+
clear
|
12
|
+
sync
|
13
|
+
green?
|
14
|
+
yellow?
|
15
|
+
red?
|
16
|
+
get_color
|
17
|
+
get_attempts
|
16
18
|
record_attempt
|
19
|
+
clear_attempts
|
20
|
+
get_failures
|
17
21
|
record_failure
|
22
|
+
clear_failures
|
23
|
+
get_state
|
18
24
|
set_state
|
25
|
+
clear_state
|
26
|
+
get_threshold
|
19
27
|
set_threshold
|
20
|
-
|
21
|
-
|
28
|
+
clear_threshold
|
29
|
+
get_timeout
|
30
|
+
set_timeout
|
31
|
+
clear_timeout
|
22
32
|
).each do |method|
|
23
33
|
it "responds to #{method}" do
|
24
34
|
expect(data_store).to respond_to(method)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Stoplight::DataStore do
|
6
|
+
describe '.validate_color!' do
|
7
|
+
subject(:result) { described_class.validate_color!(color) }
|
8
|
+
let(:color) { nil }
|
9
|
+
|
10
|
+
context 'with an invalid color' do
|
11
|
+
it 'raises an error' do
|
12
|
+
expect { result }.to raise_error(Stoplight::Error::InvalidColor)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '.validate_failure!' do
|
18
|
+
subject(:result) { described_class.validate_failure!(failure) }
|
19
|
+
let(:failure) { nil }
|
20
|
+
|
21
|
+
context 'with an invalid failure' do
|
22
|
+
it 'raises an error' do
|
23
|
+
expect { result }.to raise_error(Stoplight::Error::InvalidFailure)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '.validate_state!' do
|
29
|
+
subject(:result) { described_class.validate_state!(state) }
|
30
|
+
let(:state) { nil }
|
31
|
+
|
32
|
+
context 'with an invalid state' do
|
33
|
+
it 'raises an error' do
|
34
|
+
expect { result }.to raise_error(Stoplight::Error::InvalidState)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '.validate_threshold!' do
|
40
|
+
subject(:result) { described_class.validate_threshold!(threshold) }
|
41
|
+
let(:threshold) { nil }
|
42
|
+
|
43
|
+
context 'with an invalid threshold' do
|
44
|
+
it 'raises an error' do
|
45
|
+
expect { result }.to raise_error(Stoplight::Error::InvalidThreshold)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '.validate_timeout!' do
|
51
|
+
subject(:result) { described_class.validate_timeout!(timeout) }
|
52
|
+
let(:timeout) { nil }
|
53
|
+
|
54
|
+
context 'with an invalid timeout' do
|
55
|
+
it 'raises an error' do
|
56
|
+
expect { result }.to raise_error(Stoplight::Error::InvalidTimeout)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -3,21 +3,47 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
5
|
describe Stoplight::Failure do
|
6
|
-
|
6
|
+
subject(:failure) { described_class.new(error, time) }
|
7
|
+
let(:error) { error_class.new(error_message) }
|
8
|
+
let(:error_class) { StandardError }
|
9
|
+
let(:error_message) { SecureRandom.hex }
|
10
|
+
let(:time) { Time.now }
|
7
11
|
|
8
|
-
|
12
|
+
describe '.from_json' do
|
13
|
+
subject(:result) { described_class.from_json(json) }
|
14
|
+
let(:json) { failure.to_json }
|
9
15
|
|
10
|
-
|
11
|
-
|
16
|
+
it do
|
17
|
+
expect(result.error).to eq(failure.error)
|
18
|
+
expect(result.time).to be_within(1).of(failure.time)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#initialize' do
|
23
|
+
it 'sets the error' do
|
24
|
+
expect(failure.error).to eql(error)
|
25
|
+
end
|
12
26
|
|
13
|
-
|
27
|
+
it 'sets the time' do
|
28
|
+
expect(failure.time).to eql(time)
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'without a time' do
|
32
|
+
let(:time) { nil }
|
14
33
|
|
15
|
-
|
16
|
-
|
34
|
+
it 'uses the default time' do
|
35
|
+
expect(failure.time).to be_within(1).of(Time.now)
|
36
|
+
end
|
17
37
|
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#to_json' do
|
41
|
+
subject(:json) { failure.to_json }
|
42
|
+
let(:data) { JSON.load(json) }
|
18
43
|
|
19
|
-
it '
|
20
|
-
expect(
|
44
|
+
it 'converts to JSON' do
|
45
|
+
expect(data['error']).to eql(error.inspect)
|
46
|
+
expect(data['time']).to eql(time.inspect)
|
21
47
|
end
|
22
48
|
end
|
23
49
|
end
|
@@ -1,242 +1,122 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
# rubocop:disable Metrics/LineLength
|
2
3
|
|
3
4
|
require 'spec_helper'
|
4
5
|
|
5
6
|
describe Stoplight::Light do
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
subject(:light) { described_class.new(name, &code) }
|
11
|
-
|
12
|
-
describe '#initialize' do
|
13
|
-
it 'uses the default allowed errors' do
|
14
|
-
expect(light.allowed_errors).to eql([])
|
15
|
-
end
|
16
|
-
|
17
|
-
it 'sets the code' do
|
18
|
-
expect(light.code).to eql(code.to_proc)
|
19
|
-
end
|
20
|
-
|
21
|
-
it 'sets the name' do
|
22
|
-
expect(light.name).to eql(name.to_s)
|
23
|
-
end
|
7
|
+
before do
|
8
|
+
@notifiers = Stoplight.notifiers
|
9
|
+
Stoplight.notifiers = []
|
24
10
|
end
|
11
|
+
after { Stoplight.notifiers = @notifiers }
|
25
12
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
nil
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
it 'raises the error' do
|
63
|
-
expect { result }.to raise_error(error)
|
64
|
-
end
|
65
|
-
|
66
|
-
it 'records the failure' do
|
67
|
-
expect(Stoplight.failures(name)).to be_empty
|
68
|
-
safe_result
|
69
|
-
expect(Stoplight.failures(name)).to_not be_empty
|
70
|
-
end
|
71
|
-
|
72
|
-
context do
|
73
|
-
before { light.with_allowed_errors([klass]) }
|
74
|
-
|
75
|
-
it 'clears failures' do
|
76
|
-
Stoplight.record_failure(name, nil)
|
77
|
-
expect(Stoplight.failures(name)).to_not be_empty
|
78
|
-
safe_result
|
79
|
-
expect(Stoplight.failures(name)).to be_empty
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
context 'not green' do
|
86
|
-
let(:fallback) { proc { fallback_result } }
|
87
|
-
let(:fallback_result) { double }
|
88
|
-
|
89
|
-
before do
|
90
|
-
light.with_fallback(&fallback)
|
91
|
-
allow(light).to receive(:green?).and_return(false)
|
92
|
-
Stoplight.notifiers.each do |notifier|
|
93
|
-
allow(notifier).to receive(:notify)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
it 'runs the fallback' do
|
98
|
-
expect(result).to eql(fallback_result)
|
99
|
-
end
|
100
|
-
|
101
|
-
it 'records the attempt' do
|
102
|
-
result
|
103
|
-
expect(Stoplight.data_store.attempts(name)).to eql(1)
|
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
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
describe '#with_allowed_errors' do
|
127
|
-
let(:allowed_errors) { [double] }
|
128
|
-
|
129
|
-
subject(:result) { light.with_allowed_errors(allowed_errors) }
|
130
|
-
|
131
|
-
it 'returns self' do
|
132
|
-
expect(result).to be light
|
133
|
-
end
|
134
|
-
|
135
|
-
it 'sets the allowed errors' do
|
136
|
-
expect(result.allowed_errors).to eql(allowed_errors)
|
137
|
-
end
|
13
|
+
subject(:light) { described_class.new(name, &code) }
|
14
|
+
let(:allowed_errors) { [error_class] }
|
15
|
+
let(:code_result) { double }
|
16
|
+
let(:code) { -> { code_result } }
|
17
|
+
let(:error_class) { Class.new(StandardError) }
|
18
|
+
let(:error) { error_class.new(message) }
|
19
|
+
let(:fallback_result) { double }
|
20
|
+
let(:fallback) { -> { fallback_result } }
|
21
|
+
let(:message) { SecureRandom.hex }
|
22
|
+
let(:name) { SecureRandom.hex }
|
23
|
+
let(:threshold) { rand(100) }
|
24
|
+
let(:timeout) { rand(100) }
|
25
|
+
|
26
|
+
it { expect(light.run).to eql(code_result) }
|
27
|
+
it { expect(light.with_allowed_errors(allowed_errors)).to equal(light) }
|
28
|
+
it { expect(light.with_fallback(&fallback)).to equal(light) }
|
29
|
+
it { expect(light.with_threshold(threshold)).to equal(light) }
|
30
|
+
it { expect(light.with_timeout(timeout)).to equal(light) }
|
31
|
+
it { expect { light.fallback }.to raise_error(Stoplight::Error::RedLight) }
|
32
|
+
it { expect(light.allowed_errors).to eql([]) }
|
33
|
+
it { expect(light.code).to eql(code) }
|
34
|
+
it { expect(light.name).to eql(name) }
|
35
|
+
it { expect(light.color).to eql(Stoplight::DataStore::COLOR_GREEN) }
|
36
|
+
it { expect(light.green?).to eql(true) }
|
37
|
+
it { expect(light.yellow?).to eql(false) }
|
38
|
+
it { expect(light.red?).to eql(false) }
|
39
|
+
it { expect(light.threshold).to eql(Stoplight::DataStore::DEFAULT_THRESHOLD) }
|
40
|
+
it { expect(light.timeout).to eql(Stoplight::DataStore::DEFAULT_TIMEOUT) }
|
41
|
+
|
42
|
+
it 'sets the allowed errors' do
|
43
|
+
light.with_allowed_errors(allowed_errors)
|
44
|
+
expect(light.allowed_errors).to eql(allowed_errors)
|
138
45
|
end
|
139
46
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
subject(:result) { light.with_fallback(&fallback) }
|
145
|
-
|
146
|
-
it 'returns self' do
|
147
|
-
expect(result).to be light
|
148
|
-
end
|
149
|
-
|
150
|
-
it 'sets the fallback' do
|
151
|
-
expect(result.fallback).to eql(fallback)
|
152
|
-
end
|
47
|
+
it 'sets the fallback' do
|
48
|
+
light.with_fallback(&fallback)
|
49
|
+
expect(light.fallback).to eql(fallback)
|
153
50
|
end
|
154
51
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
subject(:result) { light.with_threshold(threshold) }
|
159
|
-
|
160
|
-
it 'returns self' do
|
161
|
-
expect(result).to be light
|
162
|
-
end
|
163
|
-
|
164
|
-
it 'sets the threshold' do
|
165
|
-
expect(result.threshold).to eql(threshold)
|
166
|
-
end
|
52
|
+
it 'sets the threshold' do
|
53
|
+
light.with_threshold(threshold)
|
54
|
+
expect(light.threshold).to eql(threshold)
|
167
55
|
end
|
168
56
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
it 'uses the default fallback' do
|
173
|
-
expect { result }.to raise_error(Stoplight::Error::RedLight)
|
174
|
-
end
|
57
|
+
it 'sets the timeout' do
|
58
|
+
light.with_timeout(timeout)
|
59
|
+
expect(light.timeout).to eql(timeout)
|
175
60
|
end
|
176
61
|
|
177
|
-
|
178
|
-
|
62
|
+
context 'failing' do
|
63
|
+
let(:code_result) { fail error }
|
179
64
|
|
180
|
-
it '
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
context 'locked green' do
|
185
|
-
before do
|
186
|
-
Stoplight.set_state(name, Stoplight::DataStore::STATE_LOCKED_GREEN)
|
187
|
-
end
|
188
|
-
|
189
|
-
it 'is true' do
|
190
|
-
expect(result).to be true
|
65
|
+
it 'switches to red' do
|
66
|
+
light.threshold.times do
|
67
|
+
expect(light.green?).to eql(true)
|
68
|
+
expect { light.run }.to raise_error(error_class)
|
191
69
|
end
|
70
|
+
expect(light.red?).to eql(true)
|
71
|
+
expect { light.run }.to raise_error(Stoplight::Error::RedLight)
|
192
72
|
end
|
193
73
|
|
194
|
-
context '
|
195
|
-
before
|
196
|
-
Stoplight.set_state(name, Stoplight::DataStore::STATE_LOCKED_RED)
|
197
|
-
end
|
74
|
+
context 'with allowed errors' do
|
75
|
+
before { light.with_allowed_errors(allowed_errors) }
|
198
76
|
|
199
|
-
it '
|
200
|
-
|
77
|
+
it 'stays green' do
|
78
|
+
light.threshold.times do
|
79
|
+
expect(light.green?).to eql(true)
|
80
|
+
expect { light.run }.to raise_error(error_class)
|
81
|
+
end
|
82
|
+
expect(light.green?).to eql(true)
|
83
|
+
expect { light.run }.to raise_error(error_class)
|
201
84
|
end
|
202
85
|
end
|
203
86
|
|
204
|
-
context 'with
|
205
|
-
before
|
206
|
-
light.threshold.times { Stoplight.record_failure(name, nil) }
|
207
|
-
end
|
87
|
+
context 'with fallback' do
|
88
|
+
before { light.with_fallback(&fallback) }
|
208
89
|
|
209
|
-
it '
|
210
|
-
|
90
|
+
it 'calls the fallback' do
|
91
|
+
light.threshold.times do
|
92
|
+
expect(light.green?).to eql(true)
|
93
|
+
expect { light.run }.to raise_error(error_class)
|
94
|
+
end
|
95
|
+
expect(light.red?).to eql(true)
|
96
|
+
expect(light.run).to eql(fallback_result)
|
211
97
|
end
|
212
98
|
end
|
213
|
-
end
|
214
|
-
|
215
|
-
describe '#red?' do
|
216
|
-
subject(:result) { light.red? }
|
217
99
|
|
218
|
-
context '
|
219
|
-
before {
|
100
|
+
context 'with threshold' do
|
101
|
+
before { light.with_threshold(0) }
|
220
102
|
|
221
|
-
it '
|
222
|
-
expect(
|
103
|
+
it 'stays red' do
|
104
|
+
expect(light.red?).to eql(true)
|
105
|
+
expect { light.run }.to raise_error(Stoplight::Error::RedLight)
|
223
106
|
end
|
224
107
|
end
|
225
108
|
|
226
|
-
context '
|
227
|
-
before {
|
109
|
+
context 'with timeout' do
|
110
|
+
before { light.with_timeout(-1) }
|
228
111
|
|
229
|
-
it '
|
230
|
-
|
112
|
+
it 'switch to yellow' do
|
113
|
+
light.threshold.times do
|
114
|
+
expect(light.green?).to eql(true)
|
115
|
+
expect { light.run }.to raise_error(error_class)
|
116
|
+
end
|
117
|
+
expect(light.yellow?).to eql(true)
|
118
|
+
expect { light.run }.to raise_error(error_class)
|
231
119
|
end
|
232
120
|
end
|
233
121
|
end
|
234
|
-
|
235
|
-
describe '#threshold' do
|
236
|
-
subject(:result) { light.threshold }
|
237
|
-
|
238
|
-
it 'uses the default threshold' do
|
239
|
-
expect(result).to eql(Stoplight::DEFAULT_THRESHOLD)
|
240
|
-
end
|
241
|
-
end
|
242
122
|
end
|