stoplight 3.0.0 → 4.0.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +176 -198
  3. data/lib/stoplight/builder.rb +68 -0
  4. data/lib/stoplight/circuit_breaker.rb +92 -0
  5. data/lib/stoplight/configurable.rb +95 -0
  6. data/lib/stoplight/configuration.rb +126 -0
  7. data/lib/stoplight/data_store/base.rb +9 -0
  8. data/lib/stoplight/data_store/memory.rb +46 -5
  9. data/lib/stoplight/data_store/redis.rb +75 -6
  10. data/lib/stoplight/default.rb +2 -0
  11. data/lib/stoplight/error.rb +1 -0
  12. data/lib/stoplight/light/deprecated.rb +44 -0
  13. data/lib/stoplight/light/lockable.rb +45 -0
  14. data/lib/stoplight/light/runnable.rb +34 -16
  15. data/lib/stoplight/light.rb +69 -63
  16. data/lib/stoplight/rspec/generic_notifier.rb +42 -0
  17. data/lib/stoplight/rspec.rb +3 -0
  18. data/lib/stoplight/version.rb +1 -1
  19. data/lib/stoplight.rb +33 -10
  20. data/spec/spec_helper.rb +7 -0
  21. data/spec/stoplight/builder_spec.rb +165 -0
  22. data/spec/stoplight/circuit_breaker_spec.rb +35 -0
  23. data/spec/stoplight/configurable_spec.rb +25 -0
  24. data/spec/stoplight/data_store/base_spec.rb +7 -0
  25. data/spec/stoplight/data_store/memory_spec.rb +12 -123
  26. data/spec/stoplight/data_store/redis_spec.rb +28 -129
  27. data/spec/stoplight/error_spec.rb +10 -0
  28. data/spec/stoplight/light/lockable_spec.rb +93 -0
  29. data/spec/stoplight/light/runnable_spec.rb +12 -233
  30. data/spec/stoplight/light_spec.rb +4 -28
  31. data/spec/stoplight/notifier/generic_spec.rb +35 -35
  32. data/spec/stoplight/notifier/io_spec.rb +1 -0
  33. data/spec/stoplight/notifier/logger_spec.rb +3 -0
  34. data/spec/stoplight_spec.rb +17 -6
  35. data/spec/support/configurable.rb +69 -0
  36. data/spec/support/data_store/base/clear_failures.rb +18 -0
  37. data/spec/support/data_store/base/clear_state.rb +20 -0
  38. data/spec/support/data_store/base/get_all.rb +44 -0
  39. data/spec/support/data_store/base/get_failures.rb +30 -0
  40. data/spec/support/data_store/base/get_state.rb +7 -0
  41. data/spec/support/data_store/base/names.rb +29 -0
  42. data/spec/support/data_store/base/record_failures.rb +70 -0
  43. data/spec/support/data_store/base/set_state.rb +15 -0
  44. data/spec/support/data_store/base/with_notification_lock.rb +27 -0
  45. data/spec/support/data_store/base.rb +21 -0
  46. data/spec/support/database_cleaner.rb +26 -0
  47. data/spec/support/exception_helpers.rb +9 -0
  48. data/spec/support/light/runnable/color.rb +79 -0
  49. data/spec/support/light/runnable/run.rb +247 -0
  50. data/spec/support/light/runnable.rb +4 -0
  51. metadata +56 -225
  52. data/lib/stoplight/notifier/bugsnag.rb +0 -37
  53. data/lib/stoplight/notifier/hip_chat.rb +0 -43
  54. data/lib/stoplight/notifier/honeybadger.rb +0 -44
  55. data/lib/stoplight/notifier/pagerduty.rb +0 -21
  56. data/lib/stoplight/notifier/raven.rb +0 -40
  57. data/lib/stoplight/notifier/rollbar.rb +0 -39
  58. data/lib/stoplight/notifier/slack.rb +0 -21
  59. data/spec/stoplight/notifier/bugsnag_spec.rb +0 -90
  60. data/spec/stoplight/notifier/hip_chat_spec.rb +0 -91
  61. data/spec/stoplight/notifier/honeybadger_spec.rb +0 -88
  62. data/spec/stoplight/notifier/pagerduty_spec.rb +0 -40
  63. data/spec/stoplight/notifier/raven_spec.rb +0 -90
  64. data/spec/stoplight/notifier/rollbar_spec.rb +0 -90
  65. data/spec/stoplight/notifier/slack_spec.rb +0 -46
@@ -122,19 +122,9 @@ RSpec.describe Stoplight::Light do
122
122
  end
123
123
  end
124
124
 
125
- describe '#with_cool_off_time' do
126
- it 'sets the cool off time' do
127
- cool_off_time = 1.2
128
- light.with_cool_off_time(cool_off_time)
129
- expect(light.cool_off_time).to eql(cool_off_time)
130
- end
131
- end
132
-
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)
125
+ describe '#window_size' do
126
+ it 'is initially the default' do
127
+ expect(light.window_size).to eql(Stoplight::Default::WINDOW_SIZE)
138
128
  end
139
129
  end
140
130
 
@@ -162,19 +152,5 @@ RSpec.describe Stoplight::Light do
162
152
  end
163
153
  end
164
154
 
165
- describe '#with_notifiers' do
166
- it 'sets the notifiers' do
167
- notifiers = [Stoplight::Notifier::IO.new(StringIO.new)]
168
- light.with_notifiers(notifiers)
169
- expect(light.notifiers).to eql(notifiers)
170
- end
171
- end
172
-
173
- describe '#with_threshold' do
174
- it 'sets the threshold' do
175
- threshold = 12
176
- light.with_threshold(threshold)
177
- expect(light.threshold).to eql(threshold)
178
- end
179
- end
155
+ it_behaves_like Stoplight::Configurable
180
156
  end
@@ -2,49 +2,49 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- RSpec.shared_examples_for 'a generic notifier' do
6
- it 'includes Generic' do
7
- expect(described_class).to include(Stoplight::Notifier::Generic)
5
+ RSpec.describe Stoplight::Notifier::Generic do
6
+ let(:light) { Stoplight::Light.new(name) {} }
7
+ let(:name) { ('a'..'z').to_a.shuffle.join }
8
+ let(:from_color) { Stoplight::Color::GREEN }
9
+ let(:to_color) { Stoplight::Color::RED }
10
+ let(:error) { nil }
11
+
12
+ it 'is a module' do
13
+ expect(described_class).to be_a(Module)
8
14
  end
9
15
 
10
- describe '#formatter' do
11
- it 'is initially the default' do
12
- formatter = nil
13
- expect(described_class.new(nil, formatter).formatter)
14
- .to eql(Stoplight::Default::FORMATTER)
16
+ describe '#put' do
17
+ let(:notifier) { notifier_class.new(double.as_null_object) }
18
+ let(:notifier_class) do
19
+ Class.new do
20
+ include Stoplight::Notifier::Generic
21
+ end
15
22
  end
16
23
 
17
- it 'reads the formatter' do
18
- formatter = proc {}
19
- expect(described_class.new(nil, formatter).formatter)
20
- .to eql(formatter)
24
+ it 'has to implement the #put method' do
25
+ expect do
26
+ notifier.notify(light, from_color, to_color, error)
27
+ end.to raise_error(NotImplementedError)
21
28
  end
22
29
  end
23
30
 
24
31
  describe '#notify' do
25
- let(:light) { Stoplight::Light.new(name, &code) }
26
- let(:name) { ('a'..'z').to_a.shuffle.join }
27
- let(:code) { -> {} }
28
- let(:from_color) { Stoplight::Color::GREEN }
29
- let(:to_color) { Stoplight::Color::RED }
30
- let(:notifier) { described_class.new(double.as_null_object) }
31
-
32
- it 'returns the message' do
33
- error = nil
34
- expect(notifier.notify(light, from_color, to_color, error))
35
- .to eql(notifier.formatter.call(light, from_color, to_color, error))
36
- end
37
-
38
- it 'returns the message with an error' do
39
- error = ZeroDivisionError.new('divided by 0')
40
- expect(notifier.notify(light, from_color, to_color, error))
41
- .to eql(notifier.formatter.call(light, from_color, to_color, error))
32
+ let(:formatted_message) { 'formatted message' }
33
+ let(:notifier) { notifier_class.new(object) }
34
+ let(:notifier_class) do
35
+ Class.new do
36
+ include Stoplight::Notifier::Generic
37
+ def put(message)
38
+ object.put(message)
39
+ end
40
+ end
41
+
42
+ it 'puts formatted message' do
43
+ expect(formatter).to receive(:call).with(light, from_color, to_color, error) { formatted_message }
44
+ expect(object).to receive(:put).with(formatted_message)
45
+
46
+ expect(notifier.notify(light, from_color, to_color, error)).to eq(formatted_message)
47
+ end
42
48
  end
43
49
  end
44
50
  end
45
-
46
- RSpec.describe Stoplight::Notifier::Generic do
47
- it 'is a module' do
48
- expect(described_class).to be_a(Module)
49
- end
50
- end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
  require 'stringio'
5
+ require 'stoplight/rspec'
5
6
 
6
7
  RSpec.describe Stoplight::Notifier::IO do
7
8
  it_behaves_like 'a generic notifier'
@@ -3,8 +3,11 @@
3
3
  require 'spec_helper'
4
4
  require 'logger'
5
5
  require 'stringio'
6
+ require 'stoplight/rspec'
6
7
 
7
8
  RSpec.describe Stoplight::Notifier::Logger do
9
+ it_behaves_like 'a generic notifier'
10
+
8
11
  it 'is a class' do
9
12
  expect(described_class).to be_a(Class)
10
13
  end
@@ -9,13 +9,24 @@ RSpec.describe Stoplight do
9
9
  end
10
10
 
11
11
  RSpec.describe 'Stoplight' do
12
- subject(:light) { Stoplight(name, &code) }
13
12
  let(:name) { ('a'..'z').to_a.shuffle.join }
14
- let(:code) { -> {} }
15
13
 
16
- it 'creates a stoplight' do
17
- expect(light).to be_a(Stoplight::Light)
18
- expect(light.name).to eql(name)
19
- expect(light.code).to eql(code)
14
+ context 'with code' do
15
+ subject(:light) { Stoplight(name, &code) }
16
+ let(:code) { -> {} }
17
+
18
+ it 'creates a stoplight' do
19
+ expect(light).to be_a(Stoplight::Light)
20
+ expect(light.name).to eql(name)
21
+ expect(light.code).to eql(code)
22
+ end
23
+ end
24
+
25
+ context 'without code' do
26
+ subject(:light) { Stoplight(name) }
27
+
28
+ it 'creates a stoplight' do
29
+ expect(light).to eq(Stoplight::Builder.with(name: name))
30
+ end
20
31
  end
21
32
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples Stoplight::Configurable do
4
+ let(:configurable) { described_class.new(configuration) }
5
+
6
+ let(:configuration) do
7
+ Stoplight::Configuration.new(
8
+ name: name,
9
+ data_store: Stoplight.default_data_store,
10
+ notifiers: Stoplight.default_notifiers,
11
+ error_notifier: Stoplight.default_error_notifier,
12
+ cool_off_time: Stoplight::Default::COOL_OFF_TIME,
13
+ threshold: Stoplight::Default::THRESHOLD,
14
+ window_size: Stoplight::Default::WINDOW_SIZE
15
+ )
16
+ end
17
+
18
+ shared_examples 'configurable attribute' do |attribute|
19
+ subject(:with_attribute) do
20
+ configurable.__send__("with_#{attribute}", __send__(attribute))
21
+ end
22
+
23
+ it "configures #{attribute}" do
24
+ expect(with_attribute.configuration.__send__(attribute)).to eq(__send__(attribute))
25
+ end
26
+ end
27
+
28
+ describe '#with_data_store' do
29
+ let(:data_store) { instance_double(Stoplight::DataStore::Redis) }
30
+
31
+ include_examples 'configurable attribute', :data_store
32
+ end
33
+
34
+ describe '#cool_off_time' do
35
+ let(:cool_off_time) { 1_000 }
36
+
37
+ include_examples 'configurable attribute', :cool_off_time
38
+ end
39
+
40
+ describe '#with_threshold' do
41
+ let(:threshold) { 1_000 }
42
+
43
+ include_examples 'configurable attribute', :threshold
44
+ end
45
+
46
+ describe '#with_window_size' do
47
+ let(:window_size) { 1_000 }
48
+
49
+ include_examples 'configurable attribute', :window_size
50
+ end
51
+
52
+ describe '#with_notifiers' do
53
+ let(:notifiers) { 1_000 }
54
+
55
+ include_examples 'configurable attribute', :notifiers
56
+ end
57
+
58
+ describe '#with_error_notifier' do
59
+ let(:error_notifier) { ->(x) { x } }
60
+
61
+ subject(:with_attribute) do
62
+ configurable.with_error_notifier(&error_notifier)
63
+ end
64
+
65
+ it 'configures error notifier' do
66
+ expect(with_attribute.configuration.error_notifier).to eq(error_notifier)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples 'Stoplight::DataStore::Base#clear_failures' do
4
+ before do
5
+ data_store.record_failure(light, failure)
6
+ end
7
+
8
+ it 'returns the failures' do
9
+ expect(data_store.clear_failures(light)).to contain_exactly(failure)
10
+ end
11
+
12
+ it 'clears the failures' do
13
+ expect do
14
+ data_store.clear_failures(light)
15
+ end.to change { data_store.get_failures(light) }
16
+ .from([failure]).to(be_empty)
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples 'Stoplight::DataStore::Base#clear_state' do
4
+ let(:state) { 'state' }
5
+
6
+ it 'returns the state' do
7
+ data_store.set_state(light, state)
8
+
9
+ expect(data_store.clear_state(light)).to eql(state)
10
+ end
11
+
12
+ it 'clears the state' do
13
+ data_store.set_state(light, state)
14
+
15
+ expect do
16
+ data_store.clear_state(light)
17
+ end.to change { data_store.get_state(light) }
18
+ .from(state).to(Stoplight::State::UNLOCKED)
19
+ end
20
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples 'Stoplight::DataStore::Base#get_all' do
4
+ context 'when there are no errors' do
5
+ it 'returns the failures and the state' do
6
+ failures, state = data_store.get_all(light)
7
+
8
+ expect(failures).to eql([])
9
+ expect(state).to eql(Stoplight::State::UNLOCKED)
10
+ end
11
+ end
12
+
13
+ context 'when there are errors' do
14
+ before do
15
+ data_store.record_failure(light, failure)
16
+ end
17
+
18
+ it 'returns the failures and the state' do
19
+ failures, state = data_store.get_all(light)
20
+
21
+ expect(failures).to eq([failure])
22
+ expect(state).to eql(Stoplight::State::UNLOCKED)
23
+ end
24
+ end
25
+
26
+ context 'when there is a failure outside of the window' do
27
+ let(:window_size) { 3600 }
28
+ let(:older_failure) { Stoplight::Failure.new('class', 'message 3', Time.new - window_size - 1) }
29
+
30
+ before do
31
+ light.with_window_size(window_size)
32
+
33
+ data_store.record_failure(light, older_failure)
34
+ data_store.record_failure(light, failure)
35
+ end
36
+
37
+ it 'returns the failures within window and the state' do
38
+ failures, state = data_store.get_all(light)
39
+
40
+ expect(failures).to contain_exactly(failure)
41
+ expect(state).to eql(Stoplight::State::UNLOCKED)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples 'Stoplight::DataStore::Base#get_failures' do
4
+ it 'is initially empty' do
5
+ expect(data_store.get_failures(light)).to eql([])
6
+ end
7
+
8
+ it 'handles invalid JSON' do
9
+ expect { data_store.record_failure(light, failure) }
10
+ .to change { data_store.get_failures(light) }
11
+ .from(be_empty)
12
+ .to(contain_exactly(failure))
13
+ end
14
+
15
+ context 'when there is a failure outside of the window' do
16
+ let(:window_size) { 3600 }
17
+ let(:older_failure) { Stoplight::Failure.new('class', 'message 3', Time.new - window_size - 1) }
18
+
19
+ before do
20
+ light.with_window_size(window_size)
21
+
22
+ data_store.record_failure(light, failure)
23
+ data_store.record_failure(light, older_failure)
24
+ end
25
+
26
+ it 'returns failures withing given window' do
27
+ expect(data_store.get_failures(light)).to contain_exactly(failure)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples 'Stoplight::DataStore::Base#get_state' do
4
+ it 'is initially unlocked' do
5
+ expect(data_store.get_state(light)).to eql(Stoplight::State::UNLOCKED)
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples 'Stoplight::DataStore::Base#names' do
4
+ it 'is initially empty' do
5
+ expect(data_store.names).to eql([])
6
+ end
7
+
8
+ it 'contains the name of a light with a failure' do
9
+ data_store.record_failure(light, failure)
10
+ expect(data_store.names).to eql([light.name])
11
+ end
12
+
13
+ it 'contains the name of a light with a set state' do
14
+ data_store.set_state(light, Stoplight::State::UNLOCKED)
15
+ expect(data_store.names).to eql([light.name])
16
+ end
17
+
18
+ it 'does not duplicate names' do
19
+ data_store.record_failure(light, failure)
20
+ data_store.set_state(light, Stoplight::State::UNLOCKED)
21
+ expect(data_store.names).to eql([light.name])
22
+ end
23
+
24
+ it 'supports names containing colons' do
25
+ light = Stoplight::Light.new('http://api.example.com/some/action')
26
+ data_store.record_failure(light, failure)
27
+ expect(data_store.names).to eql([light.name])
28
+ end
29
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples 'Stoplight::DataStore::Base#record_failure' do
4
+ it 'returns the number of failures' do
5
+ expect(data_store.record_failure(light, failure)).to eql(1)
6
+ end
7
+
8
+ context 'when there is an error' do
9
+ before do
10
+ data_store.record_failure(light, failure)
11
+ end
12
+
13
+ it 'persists the failure' do
14
+ expect(data_store.get_failures(light)).to eq([failure])
15
+ end
16
+ end
17
+
18
+ context 'when there is are several errors' do
19
+ before do
20
+ data_store.record_failure(light, failure)
21
+ data_store.record_failure(light, other)
22
+ end
23
+
24
+ it 'stores more recent failures at the head' do
25
+ expect(data_store.get_failures(light)).to eq([other, failure])
26
+ end
27
+ end
28
+
29
+ shared_examples 'with_threshold' do
30
+ context 'when the number of errors is bigger then threshold' do
31
+ before do
32
+ light.with_threshold(1)
33
+
34
+ data_store.record_failure(light, failure)
35
+ end
36
+
37
+ it 'limits the number of stored failures' do
38
+ expect do
39
+ data_store.record_failure(light, other)
40
+ end.to change { data_store.get_failures(light) }
41
+ .from([failure])
42
+ .to([other])
43
+ end
44
+ end
45
+ end
46
+
47
+ it_behaves_like 'with_threshold'
48
+
49
+ context 'with window_size' do
50
+ let(:window_size) { 3600 }
51
+
52
+ before do
53
+ light.with_window_size(window_size)
54
+ end
55
+
56
+ context 'when error is outside of the window' do
57
+ let(:older_failure) { Stoplight::Failure.new('class', 'message 3', Time.new - window_size - 1) }
58
+
59
+ it 'stores failures only withing window length' do
60
+ data_store.record_failure(light, failure)
61
+ data_store.record_failure(light, other)
62
+ data_store.record_failure(light, older_failure)
63
+
64
+ expect(data_store.get_failures(light)).to eq([other, failure])
65
+ end
66
+ end
67
+
68
+ it_behaves_like 'with_threshold'
69
+ end
70
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples 'Stoplight::DataStore::Base#set_state' do
4
+ let(:state) { 'state' }
5
+
6
+ it 'returns the state' do
7
+ expect(data_store.set_state(light, state)).to eql(state)
8
+ end
9
+
10
+ it 'persists the state' do
11
+ data_store.set_state(light, state)
12
+
13
+ expect(data_store.get_state(light)).to eql(state)
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples 'Stoplight::DataStore::Base#with_notification_lock' do
4
+ context 'when notification is already sent' do
5
+ before do
6
+ data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED) {}
7
+ end
8
+
9
+ it 'does not yield passed block' do
10
+ expect do |b|
11
+ data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED, &b)
12
+ end.not_to yield_control
13
+ end
14
+ end
15
+
16
+ context 'when notification is not already sent' do
17
+ before do
18
+ data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED) {}
19
+ end
20
+
21
+ it 'yields passed block' do
22
+ expect do |b|
23
+ data_store.with_notification_lock(light, Stoplight::Color::RED, Stoplight::Color::GREEN, &b)
24
+ end.to yield_control
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base/names'
4
+ require_relative 'base/get_failures'
5
+ require_relative 'base/get_all'
6
+ require_relative 'base/record_failures'
7
+ require_relative 'base/clear_failures'
8
+ require_relative 'base/get_state'
9
+ require_relative 'base/set_state'
10
+ require_relative 'base/clear_state'
11
+ require_relative 'base/with_notification_lock'
12
+
13
+ RSpec.shared_examples 'Stoplight::DataStore::Base' do
14
+ it 'is a class' do
15
+ expect(described_class).to be_a(Class)
16
+ end
17
+
18
+ it 'is a subclass of Base' do
19
+ expect(described_class).to be < Stoplight::DataStore::Base
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis'
4
+ require 'database_cleaner/redis'
5
+
6
+ cleaning_strategy = DatabaseCleaner::Redis::Deletion.new(only: ["#{Stoplight::DataStore::Redis::KEY_PREFIX}*"])
7
+ DatabaseCleaner.strategy = cleaning_strategy
8
+
9
+ RSpec.shared_context :redis, :redis do
10
+ let(:redis) { Redis.new(url: ENV.fetch('STOPLIGHT_REDIS_URL', 'redis://127.0.0.1:6379/0')) }
11
+
12
+ before(:suite) do
13
+ DatabaseCleaner[:redis].db = redis
14
+ DatabaseCleaner.clean_with(:deletion)
15
+ end
16
+
17
+ around(:each) do |example|
18
+ DatabaseCleaner.cleaning do
19
+ example.run
20
+ end
21
+ end
22
+ end
23
+
24
+ RSpec.configure do |config|
25
+ config.include_context :redis, include_shared: true
26
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExceptionHelpers
4
+ def suppress(*exception_classes)
5
+ yield
6
+ rescue Exception => e # rubocop:disable Lint/RescueException
7
+ raise unless exception_classes.any? { |cls| e.is_a?(cls) }
8
+ end
9
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples 'Stoplight::Light::Runnable#color' do
4
+ subject(:light) { Stoplight::Light.new(name) }
5
+
6
+ let(:name) { random_string }
7
+
8
+ it 'is initially green' do
9
+ expect(light.color).to eql(Stoplight::Color::GREEN)
10
+ end
11
+
12
+ context 'when its locked green' do
13
+ before do
14
+ light.data_store.set_state(light, Stoplight::State::LOCKED_GREEN)
15
+ end
16
+
17
+ it 'is green' do
18
+ expect(light.color).to eql(Stoplight::Color::GREEN)
19
+ end
20
+ end
21
+
22
+ context 'when its locked red' do
23
+ before do
24
+ light.data_store.set_state(light, Stoplight::State::LOCKED_RED)
25
+ end
26
+
27
+ it 'is red' do
28
+ expect(light.color).to eql(Stoplight::Color::RED)
29
+ end
30
+ end
31
+
32
+ context 'when there are many failures' do
33
+ let(:anther) { Stoplight::Failure.new(error.class.name, error.message, time - 10) }
34
+
35
+ before do
36
+ light.with_threshold(2)
37
+ light.data_store.record_failure(light, failure)
38
+ end
39
+
40
+ it 'turns red' do
41
+ expect do
42
+ light.data_store.record_failure(light, anther)
43
+ end.to change(light, :color).to be(Stoplight::Color::RED)
44
+ end
45
+ end
46
+
47
+ context 'when the most recent failure is old' do
48
+ let(:failure) { Stoplight::Failure.new(error.class.name, error.message, Time.new - light.cool_off_time) }
49
+ let(:failure2) { Stoplight::Failure.new(error.class.name, error.message, Time.new - light.cool_off_time - 10) }
50
+
51
+ before do
52
+ light.with_threshold(2)
53
+ light.data_store.record_failure(light, failure2)
54
+ end
55
+
56
+ it 'turns yellow' do
57
+ expect do
58
+ light.data_store.record_failure(light, failure)
59
+ end.to change(light, :color).to be(Stoplight::Color::YELLOW)
60
+ end
61
+ end
62
+
63
+ context 'when the least recent failure is old' do
64
+ let(:other) do
65
+ Stoplight::Failure.new(error.class.name, error.message, Time.new - light.cool_off_time)
66
+ end
67
+
68
+ before do
69
+ light.with_threshold(2)
70
+ light.data_store.record_failure(light, other)
71
+ end
72
+
73
+ it 'is red when the least recent failure is old' do
74
+ expect do
75
+ light.data_store.record_failure(light, failure)
76
+ end.to change(light, :color).to be(Stoplight::Color::RED)
77
+ end
78
+ end
79
+ end