stoplight 0.4.1 → 0.5.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.
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