stoplight 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +66 -63
  5. data/lib/stoplight.rb +10 -15
  6. data/lib/stoplight/color.rb +9 -0
  7. data/lib/stoplight/data_store.rb +0 -146
  8. data/lib/stoplight/data_store/base.rb +7 -130
  9. data/lib/stoplight/data_store/memory.rb +25 -100
  10. data/lib/stoplight/data_store/redis.rb +61 -119
  11. data/lib/stoplight/default.rb +34 -0
  12. data/lib/stoplight/error.rb +0 -42
  13. data/lib/stoplight/failure.rb +21 -25
  14. data/lib/stoplight/light.rb +42 -127
  15. data/lib/stoplight/light/runnable.rb +97 -0
  16. data/lib/stoplight/notifier/base.rb +1 -4
  17. data/lib/stoplight/notifier/hip_chat.rb +17 -32
  18. data/lib/stoplight/notifier/io.rb +9 -9
  19. data/lib/stoplight/state.rb +9 -0
  20. data/spec/spec_helper.rb +2 -3
  21. data/spec/stoplight/color_spec.rb +39 -0
  22. data/spec/stoplight/data_store/base_spec.rb +56 -36
  23. data/spec/stoplight/data_store/memory_spec.rb +120 -2
  24. data/spec/stoplight/data_store/redis_spec.rb +123 -24
  25. data/spec/stoplight/data_store_spec.rb +2 -69
  26. data/spec/stoplight/default_spec.rb +86 -0
  27. data/spec/stoplight/error_spec.rb +29 -0
  28. data/spec/stoplight/failure_spec.rb +61 -51
  29. data/spec/stoplight/light/runnable_spec.rb +234 -0
  30. data/spec/stoplight/light_spec.rb +143 -191
  31. data/spec/stoplight/notifier/base_spec.rb +8 -11
  32. data/spec/stoplight/notifier/hip_chat_spec.rb +66 -55
  33. data/spec/stoplight/notifier/io_spec.rb +49 -21
  34. data/spec/stoplight/notifier_spec.rb +3 -0
  35. data/spec/stoplight/state_spec.rb +39 -0
  36. data/spec/stoplight_spec.rb +2 -65
  37. metadata +55 -19
  38. data/spec/support/data_store.rb +0 -36
  39. data/spec/support/fakeredis.rb +0 -3
  40. data/spec/support/hipchat.rb +0 -3
@@ -3,74 +3,7 @@
3
3
  require 'spec_helper'
4
4
 
5
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
-
49
- context 'with a negative threshold' do
50
- let(:threshold) { -1 }
51
-
52
- it 'raises an error' do
53
- expect { result }.to raise_error(Stoplight::Error::InvalidThreshold)
54
- end
55
- end
56
-
57
- context 'with a zero threshold' do
58
- let(:threshold) { 0 }
59
-
60
- it 'raises an error' do
61
- expect { result }.to raise_error(Stoplight::Error::InvalidThreshold)
62
- end
63
- end
64
- end
65
-
66
- describe '.validate_timeout!' do
67
- subject(:result) { described_class.validate_timeout!(timeout) }
68
- let(:timeout) { nil }
69
-
70
- context 'with an invalid timeout' do
71
- it 'raises an error' do
72
- expect { result }.to raise_error(Stoplight::Error::InvalidTimeout)
73
- end
74
- end
6
+ it 'is a module' do
7
+ expect(described_class).to be_a(Module)
75
8
  end
76
9
  end
@@ -0,0 +1,86 @@
1
+ # coding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Stoplight::Default do
6
+ it 'is a module' do
7
+ expect(described_class).to be_a(Module)
8
+ end
9
+
10
+ describe '::ALLOWED_ERRORS' do
11
+ it 'is an array' do
12
+ expect(Stoplight::Default::ALLOWED_ERRORS).to be_an(Array)
13
+ end
14
+
15
+ it 'contains exception classes' do
16
+ Stoplight::Default::ALLOWED_ERRORS.each do |allowed_error|
17
+ expect(allowed_error).to be < Exception
18
+ end
19
+ end
20
+
21
+ it 'is frozen' do
22
+ expect(Stoplight::Default::ALLOWED_ERRORS).to be_frozen
23
+ end
24
+ end
25
+
26
+ describe '::DATA_STORE' do
27
+ it 'is a data store' do
28
+ expect(Stoplight::Default::DATA_STORE).to be_a(Stoplight::DataStore::Base)
29
+ end
30
+ end
31
+
32
+ describe '::ERROR_NOTIFIER' do
33
+ it 'is a proc' do
34
+ expect(Stoplight::Default::ERROR_NOTIFIER).to be_a(Proc)
35
+ end
36
+
37
+ it 'has an arity of 1' do
38
+ expect(Stoplight::Default::ERROR_NOTIFIER.arity).to eql(1)
39
+ end
40
+ end
41
+
42
+ describe '::FALLBACK' do
43
+ it 'is nil' do
44
+ expect(Stoplight::Default::FALLBACK).to eql(nil)
45
+ end
46
+ end
47
+
48
+ describe '::FORMATTER' do
49
+ it 'is a proc' do
50
+ expect(Stoplight::Default::FORMATTER).to be_a(Proc)
51
+ end
52
+
53
+ it 'has the same arity as #notify' do
54
+ notify = Stoplight::Notifier::Base.new.method(:notify)
55
+ expect(Stoplight::Default::FORMATTER.arity).to eql(notify.arity)
56
+ end
57
+ end
58
+
59
+ describe '::NOTIFIERS' do
60
+ it 'is an array' do
61
+ expect(Stoplight::Default::NOTIFIERS).to be_an(Array)
62
+ end
63
+
64
+ it 'contains notifiers' do
65
+ Stoplight::Default::NOTIFIERS.each do |notifier|
66
+ expect(notifier).to be_a(Stoplight::Notifier::Base)
67
+ end
68
+ end
69
+
70
+ it 'is frozen' do
71
+ expect(Stoplight::Default::NOTIFIERS).to be_frozen
72
+ end
73
+ end
74
+
75
+ describe '::THRESHOLD' do
76
+ it 'is an integer' do
77
+ expect(Stoplight::Default::THRESHOLD).to be_a(Fixnum)
78
+ end
79
+ end
80
+
81
+ describe '::TIMEOUT' do
82
+ it 'is a float' do
83
+ expect(Stoplight::Default::TIMEOUT).to be_a(Float)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Stoplight::Error do
6
+ it 'is a module' do
7
+ expect(described_class).to be_a(Module)
8
+ end
9
+
10
+ describe '::Base' do
11
+ it 'is a class' do
12
+ expect(Stoplight::Error::Base).to be_a(Class)
13
+ end
14
+
15
+ it 'is a subclass of StandardError' do
16
+ expect(Stoplight::Error::Base).to be < StandardError
17
+ end
18
+ end
19
+
20
+ describe '::RedLight' do
21
+ it 'is a class' do
22
+ expect(Stoplight::Error::RedLight).to be_a(Class)
23
+ end
24
+
25
+ it 'is a subclass of StandardError' do
26
+ expect(Stoplight::Error::RedLight).to be < Stoplight::Error::Base
27
+ end
28
+ end
29
+ end
@@ -3,80 +3,90 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe Stoplight::Failure do
6
- subject(:failure) { described_class.new(error_class, error_message, time) }
7
- let(:error_class) { SecureRandom.hex }
8
- let(:error_message) { SecureRandom.hex }
9
- let(:time) { Time.now }
6
+ let(:error) { ZeroDivisionError.new('divided by 0') }
7
+ let(:error_class) { error.class.name }
8
+ let(:error_message) { error.message }
9
+ let(:time) { Time.new(2001, 2, 3, 4, 5, 6, '+07:08') }
10
+ let(:json) do
11
+ JSON.generate(
12
+ error: { class: error_class, message: error_message },
13
+ time: time.strftime('%Y-%m-%dT%H:%M:%S.%N%:z'))
14
+ end
10
15
 
11
- describe '.create' do
12
- subject(:result) { described_class.create(error) }
13
- let(:error) { error_class.new(error_message) }
14
- let(:error_class) { Class.new(StandardError) }
16
+ it 'is a class' do
17
+ expect(described_class).to be_a(Class)
18
+ end
15
19
 
20
+ describe '.from_error' do
16
21
  it 'creates a failure' do
17
- expect(result).to be_a(Stoplight::Failure)
18
- expect(result.error_class).to eql(error.class.name)
19
- expect(result.error_message).to eql(error.message)
20
- expect(result.time).to be_within(1).of(Time.now)
22
+ Timecop.freeze do
23
+ failure = described_class.from_error(error)
24
+ expect(failure.error_class).to eql(error_class)
25
+ expect(failure.error_message).to eql(error_message)
26
+ expect(failure.time).to eql(Time.new)
27
+ end
21
28
  end
22
29
  end
23
30
 
24
31
  describe '.from_json' do
25
- subject(:result) { described_class.from_json(json) }
26
- let(:json) { failure.to_json }
27
-
28
- it 'can be round-tripped' do
29
- expect(result.error_class).to eq(failure.error_class)
30
- expect(result.error_message).to eq(failure.error_message)
31
- expect(result.time).to be_within(1).of(failure.time)
32
+ it 'parses JSON' do
33
+ failure = described_class.from_json(json)
34
+ expect(failure.error_class).to eql(error_class)
35
+ expect(failure.error_message).to eql(error_message)
36
+ expect(failure.time).to eql(time)
32
37
  end
38
+ end
33
39
 
34
- context 'with invalid JSON' do
35
- let(:json) { nil }
36
-
37
- it 'does not raise an error' do
38
- expect { result }.to_not raise_error
39
- end
40
+ describe '#==' do
41
+ it 'is true when they are equal' do
42
+ failure = described_class.new(error_class, error_message, time)
43
+ other = described_class.new(error_class, error_message, time)
44
+ expect(failure).to eq(other)
45
+ end
40
46
 
41
- it 'returns a self-describing invalid failure' do
42
- expect(result.error_class).to eq('Stoplight::Error::InvalidFailure')
43
- expect(result.error_message).to end_with('nil into String')
44
- expect(result.time).to be_within(1).of(Time.now)
45
- end
47
+ it 'is false when they have different error classes' do
48
+ failure = described_class.new(error_class, error_message, time)
49
+ other = described_class.new(nil, error_message, time)
50
+ expect(failure).to_not eq(other)
46
51
  end
47
- end
48
52
 
49
- describe '#initialize' do
50
- it 'sets the error class' do
51
- expect(failure.error_class).to eql(error_class)
53
+ it 'is false when they have different error messages' do
54
+ failure = described_class.new(error_class, error_message, time)
55
+ other = described_class.new(error_class, nil, time)
56
+ expect(failure).to_not eq(other)
52
57
  end
53
58
 
54
- it 'sets the error message' do
55
- expect(failure.error_message).to eql(error_message)
59
+ it 'is false when they have different times' do
60
+ failure = described_class.new(error_class, error_message, time)
61
+ other = described_class.new(error_class, error_message, nil)
62
+ expect(failure).to_not eq(other)
56
63
  end
64
+ end
57
65
 
58
- it 'sets the time' do
59
- expect(failure.time).to eql(time)
66
+ describe '#error_class' do
67
+ it 'reads the error class' do
68
+ expect(described_class.new(error_class, nil, nil).error_class)
69
+ .to eql(error_class)
60
70
  end
71
+ end
61
72
 
62
- context 'without a time' do
63
- let(:time) { nil }
73
+ describe '#error_message' do
74
+ it 'reads the error message' do
75
+ expect(described_class.new(nil, error_message, nil).error_message)
76
+ .to eql(error_message)
77
+ end
78
+ end
64
79
 
65
- it 'uses the default time' do
66
- expect(failure.time).to be_within(1).of(Time.now)
67
- end
80
+ describe '#time' do
81
+ it 'reads the time' do
82
+ expect(described_class.new(nil, nil, time).time).to eql(time)
68
83
  end
69
84
  end
70
85
 
71
86
  describe '#to_json' do
72
- subject(:json) { failure.to_json }
73
- let(:data) { JSON.load(json) }
74
- let(:time) { Time.utc(2001, 2, 3, 4, 5, 6) }
75
-
76
- it 'converts to JSON' do
77
- expect(data['error']['class']).to eql(error_class)
78
- expect(data['error']['message']).to eql(error_message)
79
- expect(data['time']).to eql('2001-02-03T04:05:06Z')
87
+ it 'generates JSON' do
88
+ expect(described_class.new(error_class, error_message, time).to_json)
89
+ .to eql(json)
80
90
  end
81
91
  end
82
92
  end
@@ -0,0 +1,234 @@
1
+ # coding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Stoplight::Light::Runnable do
6
+ subject { Stoplight::Light.new(name, &code) }
7
+
8
+ let(:code) { -> { code_result } }
9
+ let(:code_result) { random_string }
10
+ let(:fallback) { -> _ { fallback_result } }
11
+ let(:fallback_result) { random_string }
12
+ let(:name) { random_string }
13
+
14
+ let(:failure) do
15
+ Stoplight::Failure.new(error.class.name, error.message, time)
16
+ end
17
+ let(:error) { error_class.new(error_message) }
18
+ let(:error_class) { Class.new(StandardError) }
19
+ let(:error_message) { random_string }
20
+ let(:time) { Time.new }
21
+
22
+ def random_number
23
+ rand(1_000_000)
24
+ end
25
+
26
+ def random_string
27
+ ('a'..'z').to_a.shuffle.first(8).join
28
+ end
29
+
30
+ describe '#color' do
31
+ it 'is initially green' do
32
+ expect(subject.color).to eql(Stoplight::Color::GREEN)
33
+ end
34
+
35
+ it 'is green when locked green' do
36
+ subject.data_store.set_state(subject, Stoplight::State::LOCKED_GREEN)
37
+ expect(subject.color).to eql(Stoplight::Color::GREEN)
38
+ end
39
+
40
+ it 'is red when locked red' do
41
+ subject.data_store.set_state(subject, Stoplight::State::LOCKED_RED)
42
+ expect(subject.color).to eql(Stoplight::Color::RED)
43
+ end
44
+
45
+ it 'is red when there are many failures' do
46
+ subject.threshold.times do
47
+ subject.data_store.record_failure(subject, failure)
48
+ end
49
+ expect(subject.color).to eql(Stoplight::Color::RED)
50
+ end
51
+
52
+ it 'is yellow when the most recent failure is old' do
53
+ (subject.threshold - 1).times do
54
+ subject.data_store.record_failure(subject, failure)
55
+ end
56
+ other = Stoplight::Failure.new(
57
+ error.class.name, error.message, Time.new - subject.timeout)
58
+ subject.data_store.record_failure(subject, other)
59
+ expect(subject.color).to eql(Stoplight::Color::YELLOW)
60
+ end
61
+ end
62
+
63
+ describe '#run' do
64
+ let(:notifiers) { [notifier] }
65
+ let(:notifier) { Stoplight::Notifier::IO.new(io) }
66
+ let(:io) { StringIO.new }
67
+
68
+ before { subject.with_notifiers(notifiers) }
69
+
70
+ context 'when the light is green' do
71
+ before { subject.data_store.clear_failures(subject) }
72
+
73
+ it 'runs the code' do
74
+ expect(subject.run).to eql(code_result)
75
+ end
76
+
77
+ context 'with some failures' do
78
+ before { subject.data_store.record_failure(subject, failure) }
79
+
80
+ it 'clears the failures' do
81
+ subject.run
82
+ expect(subject.data_store.get_failures(subject).size).to eql(0)
83
+ end
84
+ end
85
+
86
+ context 'when the code is failing' do
87
+ let(:code_result) { fail error }
88
+
89
+ it 're-raises the error' do
90
+ expect { subject.run }.to raise_error(error.class)
91
+ end
92
+
93
+ it 'records the failure' do
94
+ expect(subject.data_store.get_failures(subject).size).to eql(0)
95
+ begin
96
+ subject.run
97
+ rescue error.class
98
+ nil
99
+ end
100
+ expect(subject.data_store.get_failures(subject).size).to eql(1)
101
+ end
102
+
103
+ it 'notifies when transitioning to red' do
104
+ subject.threshold.times do
105
+ expect(io.string).to eql('')
106
+ begin
107
+ subject.run
108
+ rescue error.class
109
+ nil
110
+ end
111
+ end
112
+ expect(io.string).to_not eql('')
113
+ end
114
+
115
+ context 'when the error is allowed' do
116
+ let(:allowed_errors) { [error.class] }
117
+
118
+ before { subject.with_allowed_errors(allowed_errors) }
119
+
120
+ it 'does not record the failure' do
121
+ expect(subject.data_store.get_failures(subject).size).to eql(0)
122
+ begin
123
+ subject.run
124
+ rescue error.class
125
+ nil
126
+ end
127
+ expect(subject.data_store.get_failures(subject).size).to eql(0)
128
+ end
129
+ end
130
+
131
+ context 'with a fallback' do
132
+ before { subject.with_fallback(&fallback) }
133
+
134
+ it 'runs the fallback' do
135
+ expect(subject.run).to eql(fallback_result)
136
+ end
137
+
138
+ it 'passes the error to the fallback' do
139
+ subject.with_fallback do |e|
140
+ expect(e).to eql(error)
141
+ fallback_result
142
+ end
143
+ expect(subject.run).to eql(fallback_result)
144
+ end
145
+ end
146
+ end
147
+
148
+ context 'when the data store is failing' do
149
+ let(:data_store) { Object.new }
150
+ let(:error_notifier) { -> _ {} }
151
+
152
+ before do
153
+ subject
154
+ .with_data_store(data_store)
155
+ .with_error_notifier(&error_notifier)
156
+ end
157
+
158
+ it 'runs the code' do
159
+ expect(subject.run).to eql(code_result)
160
+ end
161
+
162
+ it 'notifies about the error' do
163
+ has_notified = false
164
+ subject.with_error_notifier do |e|
165
+ has_notified = true
166
+ expect(e).to be_a(NoMethodError)
167
+ end
168
+ subject.run
169
+ expect(has_notified).to eql(true)
170
+ end
171
+ end
172
+ end
173
+
174
+ context 'when the light is yellow' do
175
+ before do
176
+ (subject.threshold - 1).times do
177
+ subject.data_store.record_failure(subject, failure)
178
+ end
179
+
180
+ other = Stoplight::Failure.new(
181
+ error.class.name, error.message, time - subject.timeout)
182
+ subject.data_store.record_failure(subject, other)
183
+ end
184
+
185
+ it 'runs the code' do
186
+ expect(subject.run).to eql(code_result)
187
+ end
188
+
189
+ it 'notifies when transitioning to green' do
190
+ expect(io.string).to eql('')
191
+ subject.run
192
+ expect(io.string).to_not eql('')
193
+ end
194
+ end
195
+
196
+ context 'when the light is red' do
197
+ before do
198
+ subject.threshold.times do
199
+ subject.data_store.record_failure(subject, failure)
200
+ end
201
+ end
202
+
203
+ it 'raises an error' do
204
+ expect { subject.run }.to raise_error(Stoplight::Error::RedLight)
205
+ end
206
+
207
+ it 'uses the name as the error message' do
208
+ e =
209
+ begin
210
+ subject.run
211
+ rescue Stoplight::Error::RedLight => e
212
+ e
213
+ end
214
+ expect(e.message).to eql(subject.name)
215
+ end
216
+
217
+ context 'with a fallback' do
218
+ before { subject.with_fallback(&fallback) }
219
+
220
+ it 'runs the fallback' do
221
+ expect(subject.run).to eql(fallback_result)
222
+ end
223
+
224
+ it 'does not pass anything to the fallback' do
225
+ subject.with_fallback do |e|
226
+ expect(e).to eql(nil)
227
+ fallback_result
228
+ end
229
+ expect(subject.run).to eql(fallback_result)
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end