stoplight 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.md +18 -0
- data/README.md +208 -0
- data/lib/stoplight.rb +65 -0
- data/lib/stoplight/data_store.rb +15 -0
- data/lib/stoplight/data_store/base.rb +96 -0
- data/lib/stoplight/data_store/memory.rb +67 -0
- data/lib/stoplight/data_store/redis.rb +72 -0
- data/lib/stoplight/error.rb +12 -0
- data/lib/stoplight/failure.rb +24 -0
- data/lib/stoplight/light.rb +115 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/stoplight/data_store/base_spec.rb +31 -0
- data/spec/stoplight/data_store/memory_spec.rb +189 -0
- data/spec/stoplight/data_store/redis_spec.rb +190 -0
- data/spec/stoplight/failure_spec.rb +23 -0
- data/spec/stoplight/light_spec.rb +222 -0
- data/spec/stoplight_spec.rb +128 -0
- metadata +156 -0
@@ -0,0 +1,190 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'fakeredis'
|
5
|
+
|
6
|
+
describe Stoplight::DataStore::Redis do
|
7
|
+
let(:error) { error_class.new }
|
8
|
+
let(:error_class) { Class.new(StandardError) }
|
9
|
+
let(:name) { SecureRandom.hex }
|
10
|
+
let(:state) { Stoplight::DataStore::STATES.to_a.sample }
|
11
|
+
let(:threshold) { rand(10) }
|
12
|
+
|
13
|
+
subject(:data_store) { described_class.new }
|
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
|
190
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Stoplight::Failure do
|
6
|
+
let(:error) { double }
|
7
|
+
|
8
|
+
subject(:failure) { described_class.new(error) }
|
9
|
+
|
10
|
+
describe '#to_json' do
|
11
|
+
let(:json) { JSON.parse(result) }
|
12
|
+
|
13
|
+
subject(:result) { failure.to_json }
|
14
|
+
|
15
|
+
it 'includes the error' do
|
16
|
+
expect(json['error']).to eql(error.inspect)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'includes the time' do
|
20
|
+
expect(json['time']).to eql(Time.now.to_s)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Stoplight::Light do
|
6
|
+
let(:code) { proc { code_result } }
|
7
|
+
let(:code_result) { double }
|
8
|
+
let(:name) { SecureRandom.hex }
|
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
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#run' do
|
27
|
+
subject(:result) { light.run }
|
28
|
+
|
29
|
+
it 'syncs settings' do
|
30
|
+
expect(Stoplight.data_store.threshold(name)).to be nil
|
31
|
+
result
|
32
|
+
expect(Stoplight.data_store.threshold(name)).to eql(
|
33
|
+
light.threshold)
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'green' do
|
37
|
+
before { allow(light).to receive(:green?).and_return(true) }
|
38
|
+
|
39
|
+
it 'runs the code' do
|
40
|
+
expect(result).to eql(code_result)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'clears failures' do
|
44
|
+
Stoplight.record_failure(name, nil)
|
45
|
+
expect(Stoplight.failures(name)).to_not be_empty
|
46
|
+
result
|
47
|
+
expect(Stoplight.failures(name)).to be_empty
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'with failing code' do
|
51
|
+
let(:code_result) { fail error }
|
52
|
+
let(:error) { klass.new }
|
53
|
+
let(:klass) { Class.new(StandardError) }
|
54
|
+
let(:safe_result) do
|
55
|
+
begin
|
56
|
+
result
|
57
|
+
rescue klass
|
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
|
+
end
|
93
|
+
|
94
|
+
it 'runs the fallback' do
|
95
|
+
expect(result).to eql(fallback_result)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'records the attempt' do
|
99
|
+
result
|
100
|
+
expect(Stoplight.data_store.attempts(name)).to eql(1)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe '#with_allowed_errors' do
|
106
|
+
let(:allowed_errors) { [double] }
|
107
|
+
|
108
|
+
subject(:result) { light.with_allowed_errors(allowed_errors) }
|
109
|
+
|
110
|
+
it 'returns self' do
|
111
|
+
expect(result).to be light
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'sets the allowed errors' do
|
115
|
+
expect(result.allowed_errors).to eql(allowed_errors)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe '#with_fallback' do
|
120
|
+
let(:fallback) { proc { fallback_result } }
|
121
|
+
let(:fallback_result) { double }
|
122
|
+
|
123
|
+
subject(:result) { light.with_fallback(&fallback) }
|
124
|
+
|
125
|
+
it 'returns self' do
|
126
|
+
expect(result).to be light
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'sets the fallback' do
|
130
|
+
expect(result.fallback).to eql(fallback)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe '#with_threshold' do
|
135
|
+
let(:threshold) { rand(10) }
|
136
|
+
|
137
|
+
subject(:result) { light.with_threshold(threshold) }
|
138
|
+
|
139
|
+
it 'returns self' do
|
140
|
+
expect(result).to be light
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'sets the threshold' do
|
144
|
+
expect(result.threshold).to eql(threshold)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe '#fallback' do
|
149
|
+
subject(:result) { light.fallback }
|
150
|
+
|
151
|
+
it 'uses the default fallback' do
|
152
|
+
expect { result }.to raise_error(Stoplight::Error::NoFallback)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
describe '#green?' do
|
157
|
+
subject(:result) { light.green? }
|
158
|
+
|
159
|
+
it 'is true' do
|
160
|
+
expect(result).to be true
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'locked green' do
|
164
|
+
before do
|
165
|
+
Stoplight.set_state(name, Stoplight::DataStore::STATE_LOCKED_GREEN)
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'is true' do
|
169
|
+
expect(result).to be true
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
context 'locked red' do
|
174
|
+
before do
|
175
|
+
Stoplight.set_state(name, Stoplight::DataStore::STATE_LOCKED_RED)
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'is false' do
|
179
|
+
expect(result).to be false
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
context 'with failures' do
|
184
|
+
before do
|
185
|
+
light.threshold.times { Stoplight.record_failure(name, nil) }
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'is false' do
|
189
|
+
expect(result).to be false
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
describe '#red?' do
|
195
|
+
subject(:result) { light.red? }
|
196
|
+
|
197
|
+
context 'green' do
|
198
|
+
before { allow(light).to receive(:green?).and_return(true) }
|
199
|
+
|
200
|
+
it 'is false' do
|
201
|
+
expect(result).to be false
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
context 'not green' do
|
206
|
+
before { allow(light).to receive(:green?).and_return(false) }
|
207
|
+
|
208
|
+
it 'is true' do
|
209
|
+
expect(result).to be true
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
describe '#threshold' do
|
215
|
+
subject(:result) { light.threshold }
|
216
|
+
|
217
|
+
it 'uses the default threshold' do
|
218
|
+
expect(result).to eql(
|
219
|
+
described_class.const_get(:DEFAULT_THRESHOLD))
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Stoplight do
|
6
|
+
let(:name) { SecureRandom.hex }
|
7
|
+
|
8
|
+
it 'forwards all data store methods' do
|
9
|
+
(Stoplight::DataStore::Base.new.methods - Object.methods).each do |method|
|
10
|
+
expect(Stoplight).to respond_to(method)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '::VERSION' do
|
15
|
+
subject(:result) { described_class.const_get(:VERSION) }
|
16
|
+
|
17
|
+
it 'is a Gem::Version' do
|
18
|
+
expect(result).to be_a(Gem::Version)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '.data_store' do
|
23
|
+
subject(:result) { described_class.data_store }
|
24
|
+
|
25
|
+
it 'uses the default data store' do
|
26
|
+
expect(result).to be_a(Stoplight::DataStore::Memory)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'memoizes the result' do
|
30
|
+
expect(result).to be described_class.data_store
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'with a custom data store' do
|
34
|
+
let(:data_store) { double }
|
35
|
+
|
36
|
+
before do
|
37
|
+
@data_store = described_class.data_store
|
38
|
+
described_class.data_store(data_store)
|
39
|
+
end
|
40
|
+
|
41
|
+
after { described_class.data_store(@data_store) }
|
42
|
+
|
43
|
+
it 'returns the data store' do
|
44
|
+
expect(result).to eql(data_store)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '.green?' do
|
50
|
+
subject(:result) { described_class.green?(name) }
|
51
|
+
|
52
|
+
it 'is true' do
|
53
|
+
expect(result).to be true
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'locked green' do
|
57
|
+
before do
|
58
|
+
described_class.set_state(
|
59
|
+
name, Stoplight::DataStore::STATE_LOCKED_GREEN)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'is true' do
|
63
|
+
expect(result).to be true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'locked red' do
|
68
|
+
before do
|
69
|
+
described_class.set_state(
|
70
|
+
name, Stoplight::DataStore::STATE_LOCKED_RED)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'is false' do
|
74
|
+
expect(result).to be false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'with failures' do
|
79
|
+
before do
|
80
|
+
described_class.threshold(name).times do
|
81
|
+
described_class.record_failure(name, nil)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'is false' do
|
86
|
+
expect(result).to be false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe '.red?' do
|
92
|
+
subject(:result) { described_class.red?(name) }
|
93
|
+
|
94
|
+
context 'green' do
|
95
|
+
before { allow(described_class).to receive(:green?).and_return(true) }
|
96
|
+
|
97
|
+
it 'is false' do
|
98
|
+
expect(result).to be false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'not green' do
|
103
|
+
before { allow(described_class).to receive(:green?).and_return(false) }
|
104
|
+
|
105
|
+
it 'is true' do
|
106
|
+
expect(result).to be true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe '.threshold' do
|
112
|
+
subject(:result) { described_class.threshold(name) }
|
113
|
+
|
114
|
+
it 'uses the default threshold' do
|
115
|
+
expect(result).to eql(Stoplight::Light::DEFAULT_THRESHOLD)
|
116
|
+
end
|
117
|
+
|
118
|
+
context 'with a custom threshold' do
|
119
|
+
let(:threshold) { rand(10) }
|
120
|
+
|
121
|
+
before { described_class.set_threshold(name, threshold) }
|
122
|
+
|
123
|
+
it 'uses the threshold' do
|
124
|
+
expect(result).to eql(threshold)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|