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
@@ -1,228 +1,180 @@
1
1
  # coding: utf-8
2
- # rubocop:disable Metrics/LineLength
3
2
 
4
3
  require 'spec_helper'
5
4
 
6
5
  describe Stoplight::Light do
7
- before do
8
- @notifiers = Stoplight.notifiers
9
- Stoplight.notifiers = [Stoplight::Notifier::IO.new(StringIO.new)]
10
- end
11
- after { Stoplight.notifiers = @notifiers }
12
-
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) { 1 + 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)
6
+ let(:light) { described_class.new(name, &code) }
7
+ let(:name) { ('a'..'z').to_a.shuffle.join }
8
+ let(:code) { -> {} }
9
+
10
+ it 'is a class' do
11
+ expect(described_class).to be_a(Class)
12
+ end
13
+
14
+ describe '.default_data_store' do
15
+ it 'is initially the default' do
16
+ expect(described_class.default_data_store)
17
+ .to eql(Stoplight::Default::DATA_STORE)
18
+ end
19
+ end
20
+
21
+ describe '.default_data_store=' do
22
+ before { @default_data_store = described_class.default_data_store }
23
+ after { described_class.default_data_store = @default_data_store }
24
+
25
+ it 'sets the data store' do
26
+ data_store = Stoplight::DataStore::Memory.new
27
+ described_class.default_data_store = data_store
28
+ expect(described_class.default_data_store).to eql(data_store)
29
+ end
45
30
  end
46
-
47
- it 'sets the fallback' do
48
- light.with_fallback(&fallback)
49
- expect(light.fallback).to eql(fallback)
50
- end
51
-
52
- it 'sets the threshold' do
53
- light.with_threshold(threshold)
54
- expect(light.threshold).to eql(threshold)
31
+
32
+ describe '.default_error_notifier' do
33
+ it 'is initially the default' do
34
+ expect(described_class.default_error_notifier)
35
+ .to eql(Stoplight::Default::ERROR_NOTIFIER)
36
+ end
55
37
  end
56
38
 
57
- it 'sets the timeout' do
58
- light.with_timeout(timeout)
59
- expect(light.timeout).to eql(timeout)
39
+ describe '.default_error_notifier=' do
40
+ before { @default_error_notifier = described_class.default_error_notifier }
41
+ after { described_class.default_error_notifier = @default_error_notifier }
42
+
43
+ it 'sets the error notifier' do
44
+ default_error_notifier = -> _ {}
45
+ described_class.default_error_notifier = default_error_notifier
46
+ expect(described_class.default_error_notifier)
47
+ .to eql(default_error_notifier)
48
+ end
60
49
  end
61
50
 
62
- context 'failing' do
63
- let(:code_result) { fail error }
64
-
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)
69
- end
70
- expect(light.red?).to eql(true)
71
- expect { light.run }.to raise_error(Stoplight::Error::RedLight)
72
- end
73
-
74
- context 'with allowed errors' do
75
- before { light.with_allowed_errors(allowed_errors) }
76
-
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)
84
- end
51
+ describe '.default_notifiers' do
52
+ it 'is initially the default' do
53
+ expect(described_class.default_notifiers)
54
+ .to eql(Stoplight::Default::NOTIFIERS)
85
55
  end
56
+ end
86
57
 
87
- context 'with fallback' do
88
- before { light.with_fallback(&fallback) }
89
-
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)
97
- end
98
- end
99
-
100
- context 'with timeout' do
101
- before { light.with_timeout(-1) }
102
-
103
- it 'switch to yellow' do
104
- light.threshold.times do
105
- expect(light.green?).to eql(true)
106
- expect { light.run }.to raise_error(error_class)
107
- end
108
- expect(light.yellow?).to eql(true)
109
- expect { light.run }.to raise_error(error_class)
110
- end
111
- end
112
- end
113
-
114
- context 'conditionally failing' do
115
- let(:code_result) { fail error if @fail }
116
- let(:name) { 'failing' }
117
-
118
- it 'clears the attempts' do
119
- @fail = true
120
- light.threshold.succ.times do
121
- begin
122
- light.run
123
- rescue error_class, Stoplight::Error::RedLight
124
- nil
125
- end
126
- end
58
+ describe '.default_notifiers=' do
59
+ before { @default_notifiers = described_class.default_notifiers }
60
+ after { described_class.default_notifiers = @default_notifiers }
127
61
 
128
- @fail = false
129
- Stoplight.data_store.set_timeout(light.name, 0)
130
- light.run
62
+ it 'sets the data store' do
63
+ notifiers = []
64
+ described_class.default_notifiers = notifiers
65
+ expect(described_class.default_notifiers).to eql(notifiers)
66
+ end
67
+ end
131
68
 
132
- expect(Stoplight.data_store.get_attempts(light.name)).to eq(0)
69
+ describe '#allowed_errors' do
70
+ it 'is initially the default' do
71
+ expect(light.allowed_errors).to eql(Stoplight::Default::ALLOWED_ERRORS)
133
72
  end
134
73
  end
135
74
 
136
- context 'with Redis' do
137
- let(:data_store) { Stoplight::DataStore::Redis.new(redis) }
138
- let(:redis) { Redis.new }
75
+ describe '#code' do
76
+ it 'reads the code' do
77
+ expect(light.code).to eql(code)
78
+ end
79
+ end
139
80
 
140
- before do
141
- @data_store = Stoplight.data_store
142
- Stoplight.data_store = data_store
81
+ describe '#data_store' do
82
+ it 'is initially the default' do
83
+ expect(light.data_store).to eql(described_class.default_data_store)
143
84
  end
144
- after { Stoplight.data_store = @data_store }
85
+ end
145
86
 
146
- context 'with a failing connection' do
147
- let(:error) { Stoplight::Error::BadDataStore.new(cause) }
148
- let(:cause) { Redis::BaseConnectionError.new(message) }
149
- let(:message) { SecureRandom.hex }
87
+ describe '#error_notifier' do
88
+ it 'it initially the default' do
89
+ expect(light.error_notifier)
90
+ .to eql(described_class.default_error_notifier)
91
+ end
92
+ end
150
93
 
151
- before { allow(data_store).to receive(:sync).and_raise(error) }
94
+ describe '#fallback' do
95
+ it 'is initially the default' do
96
+ expect(light.fallback).to eql(Stoplight::Default::FALLBACK)
97
+ end
98
+ end
152
99
 
153
- before { @stderr, $stderr = $stderr, StringIO.new }
154
- after { $stderr = @stderr }
100
+ describe '#name' do
101
+ it 'reads the name' do
102
+ expect(light.name).to eql(name)
103
+ end
104
+ end
155
105
 
156
- it 'does not raise an error' do
157
- expect { light.run }.to_not raise_error
158
- end
106
+ describe '#notifiers' do
107
+ it 'is initially the default' do
108
+ expect(light.notifiers).to eql(described_class.default_notifiers)
109
+ end
110
+ end
159
111
 
160
- it 'switches to an in-memory data store' do
161
- light.run
162
- expect(Stoplight.data_store).to_not eql(data_store)
163
- expect(Stoplight.data_store).to be_a(Stoplight::DataStore::Memory)
164
- end
165
-
166
- it 'syncs the light in the new data store' do
167
- expect_any_instance_of(Stoplight::DataStore::Memory)
168
- .to receive(:sync).with(light.name)
169
- light.run
170
- end
112
+ describe '#threshold' do
113
+ it 'is initially the default' do
114
+ expect(light.threshold).to eql(Stoplight::Default::THRESHOLD)
115
+ end
116
+ end
171
117
 
172
- it 'warns to STDERR' do
173
- light.run
174
- expect($stderr.string).to eql("#{cause}\n")
175
- end
118
+ describe '#timeout' do
119
+ it 'is initially the default' do
120
+ expect(light.timeout).to eql(Stoplight::Default::TIMEOUT)
176
121
  end
177
- end
178
-
179
- context 'with HipChat' do
180
- let(:notifier) { Stoplight::Notifier::HipChat.new(client, room_name) }
181
- let(:client) { double(HipChat::Client) }
182
- let(:room_name) { SecureRandom.hex }
183
- let(:room) { double(HipChat::Room) }
122
+ end
184
123
 
185
- before do
186
- @notifiers = Stoplight.notifiers
187
- Stoplight.notifiers = [notifier]
188
- allow(client).to receive(:[]).with(room_name).and_return(room)
124
+ describe '#with_allowed_errors' do
125
+ it 'adds the allowed errors to the default' do
126
+ allowed_errors = [StandardError]
127
+ light.with_allowed_errors(allowed_errors)
128
+ expect(light.allowed_errors)
129
+ .to eql(Stoplight::Default::ALLOWED_ERRORS + allowed_errors)
189
130
  end
131
+ end
190
132
 
191
- after { Stoplight.notifiers = @notifiers }
192
-
193
- context 'with a failing client' do
194
- subject(:result) do
195
- begin
196
- light.run
197
- rescue Stoplight::Error::RedLight
198
- nil
199
- end
200
- end
133
+ describe '#with_data_store' do
134
+ it 'sets the data store' do
135
+ data_store = Stoplight::DataStore::Memory.new
136
+ light.with_data_store(data_store)
137
+ expect(light.data_store).to eql(data_store)
138
+ end
139
+ end
201
140
 
202
- let(:error_class) { HipChat::Unauthorized }
141
+ describe '#with_error_notifier' do
142
+ it 'sets the error notifier' do
143
+ error_notifier = -> _ {}
144
+ light.with_error_notifier(&error_notifier)
145
+ expect(light.error_notifier).to eql(error_notifier)
146
+ end
147
+ end
203
148
 
204
- before do
205
- Stoplight.data_store.set_state(
206
- light.name, Stoplight::DataStore::STATE_LOCKED_RED)
207
- allow(room).to receive(:send).with(
208
- 'Stoplight',
209
- /\A@all /,
210
- hash_including(color: 'red')
211
- ).and_raise(error)
212
- @stderr = $stderr
213
- $stderr = StringIO.new
214
- end
149
+ describe '#with_fallback' do
150
+ it 'sets the fallback' do
151
+ fallback = -> _ {}
152
+ light.with_fallback(&fallback)
153
+ expect(light.fallback).to eql(fallback)
154
+ end
155
+ end
215
156
 
216
- after { $stderr = @stderr }
157
+ describe '#with_notifiers' do
158
+ it 'sets the notifiers' do
159
+ notifiers = [Stoplight::Notifier::IO.new(StringIO.new)]
160
+ light.with_notifiers(notifiers)
161
+ expect(light.notifiers).to eql(notifiers)
162
+ end
163
+ end
217
164
 
218
- it 'does not raise an error' do
219
- expect { result }.to_not raise_error
220
- end
165
+ describe '#with_threshold' do
166
+ it 'sets the threshold' do
167
+ threshold = 12
168
+ light.with_threshold(threshold)
169
+ expect(light.threshold).to eql(threshold)
170
+ end
171
+ end
221
172
 
222
- it 'warns to STDERR' do
223
- result
224
- expect($stderr.string).to eql("#{error}\n")
225
- end
173
+ describe '#with_timeout' do
174
+ it 'sets the timeout' do
175
+ timeout = 1.2
176
+ light.with_timeout(timeout)
177
+ expect(light.timeout).to eql(timeout)
226
178
  end
227
179
  end
228
180
  end
@@ -3,19 +3,16 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe Stoplight::Notifier::Base do
6
- subject(:notifier) { described_class.new }
6
+ let(:notifier) { described_class.new }
7
7
 
8
- %w(
9
- notify
10
- ).each do |method|
11
- it "responds to #{method}" do
12
- expect(notifier).to respond_to(method)
13
- end
8
+ it 'is a class' do
9
+ expect(described_class).to be_a(Module)
10
+ end
14
11
 
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)
12
+ describe '#notify' do
13
+ it 'is not implemented' do
14
+ expect { notifier.notify(nil, nil, nil, nil) }
15
+ .to raise_error(NotImplementedError)
19
16
  end
20
17
  end
21
18
  end
@@ -1,74 +1,85 @@
1
1
  # coding: utf-8
2
2
 
3
3
  require 'spec_helper'
4
+ require 'hipchat'
4
5
 
5
6
  describe Stoplight::Notifier::HipChat do
6
- subject(:notifier) { described_class.new(client, room, formatter, options) }
7
- let(:client) { double }
8
- let(:room) { SecureRandom.hex }
9
- let(:formatter) { nil }
10
- let(:options) { {} }
7
+ it 'is a class' do
8
+ expect(described_class).to be_a(Module)
9
+ end
11
10
 
12
- describe '#notify' do
13
- subject(:result) { notifier.notify(light, from_color, to_color) }
14
- let(:light) { Stoplight::Light.new(light_name, &light_code) }
15
- let(:light_name) { SecureRandom.hex }
16
- let(:light_code) { -> {} }
17
- let(:from_color) { Stoplight::DataStore::COLOR_GREEN }
18
- let(:to_color) { Stoplight::DataStore::COLOR_RED }
11
+ it 'is a subclass of Base' do
12
+ expect(described_class).to be < Stoplight::Notifier::Base
13
+ end
14
+
15
+ describe '#formatter' do
16
+ it 'is initially the default' do
17
+ expect(described_class.new(nil, nil).formatter)
18
+ .to eql(Stoplight::Default::FORMATTER)
19
+ end
20
+
21
+ it 'reads the formatter' do
22
+ formatter = proc {}
23
+ expect(described_class.new(nil, nil, formatter).formatter)
24
+ .to eql(formatter)
25
+ end
26
+ end
19
27
 
20
- it 'sends the message to HipChat' do
21
- expect(client).to receive(:[]).with(room).and_return(client)
22
- expect(client).to receive(:send).with(
23
- 'Stoplight',
24
- "@all Switching #{light.name} from #{from_color} to #{to_color}",
25
- anything)
26
- result
28
+ describe '#hip_chat' do
29
+ it 'reads the HipChat client' do
30
+ hip_chat = HipChat::Client.new('API token')
31
+ expect(described_class.new(hip_chat, nil).hip_chat)
32
+ .to eql(hip_chat)
27
33
  end
34
+ end
28
35
 
29
- context 'with a formatter' do
30
- let(:formatter) { ->(l, f, t) { "#{l.name} #{f} #{t}" } }
36
+ describe '#options' do
37
+ it 'is initially the default' do
38
+ expect(described_class.new(nil, nil).options)
39
+ .to eql(Stoplight::Notifier::HipChat::DEFAULT_OPTIONS)
40
+ end
31
41
 
32
- it 'formats the message' do
33
- expect(client).to receive(:[]).with(room).and_return(client)
34
- expect(client).to receive(:send).with(
35
- 'Stoplight',
36
- "#{light.name} #{from_color} #{to_color}",
37
- anything)
38
- result
39
- end
42
+ it 'reads the options' do
43
+ options = { key: :value }
44
+ expect(described_class.new(nil, nil, nil, options).options)
45
+ .to eql(Stoplight::Notifier::HipChat::DEFAULT_OPTIONS.merge(options))
40
46
  end
47
+ end
41
48
 
42
- context 'failing' do
43
- let(:error) { HipChat::UnknownResponseCode.new(message) }
44
- let(:message) { SecureRandom.hex }
49
+ describe '#room' do
50
+ it 'reads the room' do
51
+ room = 'Notifications'
52
+ expect(described_class.new(nil, room).room).to eql(room)
53
+ end
54
+ end
45
55
 
46
- before do
47
- allow(client).to receive(:[]).with(room).and_return(client)
48
- allow(client).to receive(:send).and_raise(error)
49
- end
56
+ describe '#notify' do
57
+ let(:light) { Stoplight::Light.new(name, &code) }
58
+ let(:name) { ('a'..'z').to_a.shuffle.join }
59
+ let(:code) { -> {} }
60
+ let(:from_color) { Stoplight::Color::GREEN }
61
+ let(:to_color) { Stoplight::Color::RED }
62
+ let(:notifier) { described_class.new(hip_chat, room) }
63
+ let(:hip_chat) { double(HipChat::Client) }
64
+ let(:room) { ('a'..'z').to_a.shuffle.join }
50
65
 
51
- it 'reraises the error' do
52
- expect { result }.to raise_error(Stoplight::Error::BadNotifier)
53
- end
66
+ before do
67
+ tmp = double
68
+ expect(hip_chat).to receive(:[]).with(room).and_return(tmp)
69
+ expect(tmp).to receive(:send)
70
+ .with('Stoplight', kind_of(String), kind_of(Hash)).and_return(true)
71
+ end
54
72
 
55
- it 'sets the message' do
56
- begin
57
- result
58
- expect(false).to be(true)
59
- rescue Stoplight::Error::BadNotifier => e
60
- expect(e.message).to eql(message)
61
- end
62
- end
73
+ it 'returns the message' do
74
+ error = nil
75
+ expect(notifier.notify(light, from_color, to_color, error))
76
+ .to eql(notifier.formatter.call(light, from_color, to_color, error))
77
+ end
63
78
 
64
- it 'sets the cause' do
65
- begin
66
- result
67
- expect(false).to be(true)
68
- rescue Stoplight::Error::BadNotifier => e
69
- expect(e.cause).to eql(error)
70
- end
71
- end
79
+ it 'returns the message with an error' do
80
+ error = ZeroDivisionError.new('divided by 0')
81
+ expect(notifier.notify(light, from_color, to_color, error))
82
+ .to eql(notifier.formatter.call(light, from_color, to_color, error))
72
83
  end
73
84
  end
74
85
  end