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
@@ -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