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
@@ -7,127 +7,16 @@ RSpec.describe Stoplight::DataStore::Memory do
7
7
  let(:light) { Stoplight::Light.new(name) {} }
8
8
  let(:name) { ('a'..'z').to_a.shuffle.join }
9
9
  let(:failure) { Stoplight::Failure.new('class', 'message', Time.new) }
10
-
11
- it 'is a class' do
12
- expect(described_class).to be_a(Class)
13
- end
14
-
15
- it 'is a subclass of Base' do
16
- expect(described_class).to be < Stoplight::DataStore::Base
17
- end
18
-
19
- describe '#names' do
20
- it 'is initially empty' do
21
- expect(data_store.names).to eql([])
22
- end
23
-
24
- it 'contains the name of a light with a failure' do
25
- data_store.record_failure(light, failure)
26
- expect(data_store.names).to eql([light.name])
27
- end
28
-
29
- it 'contains the name of a light with a set state' do
30
- data_store.set_state(light, Stoplight::State::UNLOCKED)
31
- expect(data_store.names).to eql([light.name])
32
- end
33
-
34
- it 'does not duplicate names' do
35
- data_store.record_failure(light, failure)
36
- data_store.set_state(light, Stoplight::State::UNLOCKED)
37
- expect(data_store.names).to eql([light.name])
38
- end
39
-
40
- it 'supports names containing colons' do
41
- light = Stoplight::Light.new('http://api.example.com/some/action')
42
- data_store.record_failure(light, failure)
43
- expect(data_store.names).to eql([light.name])
44
- end
45
- end
46
-
47
- describe '#get_all' do
48
- it 'returns the failures and the state' do
49
- failures, state = data_store.get_all(light)
50
- expect(failures).to eql([])
51
- expect(state).to eql(Stoplight::State::UNLOCKED)
52
- end
53
- end
54
-
55
- describe '#get_failures' do
56
- it 'is initially empty' do
57
- expect(data_store.get_failures(light)).to eql([])
58
- end
59
- end
60
-
61
- describe '#record_failure' do
62
- it 'returns the number of failures' do
63
- expect(data_store.record_failure(light, failure)).to eql(1)
64
- end
65
-
66
- it 'persists the failure' do
67
- data_store.record_failure(light, failure)
68
- expect(data_store.get_failures(light)).to eql([failure])
69
- end
70
-
71
- it 'stores more recent failures at the front' do
72
- data_store.record_failure(light, failure)
73
- other = Stoplight::Failure.new('class', 'message 2', Time.new)
74
- data_store.record_failure(light, other)
75
- expect(data_store.get_failures(light)).to eql([other, failure])
76
- end
77
-
78
- it 'limits the number of stored failures' do
79
- light.with_threshold(1)
80
- data_store.record_failure(light, failure)
81
- other = Stoplight::Failure.new('class', 'message 2', Time.new)
82
- data_store.record_failure(light, other)
83
- expect(data_store.get_failures(light)).to eql([other])
84
- end
85
- end
86
-
87
- describe '#clear_failures' do
88
- it 'returns the failures' do
89
- data_store.record_failure(light, failure)
90
- expect(data_store.clear_failures(light)).to eql([failure])
91
- end
92
-
93
- it 'clears the failures' do
94
- data_store.record_failure(light, failure)
95
- data_store.clear_failures(light)
96
- expect(data_store.get_failures(light)).to eql([])
97
- end
98
- end
99
-
100
- describe '#get_state' do
101
- it 'is initially unlocked' do
102
- expect(data_store.get_state(light)).to eql(Stoplight::State::UNLOCKED)
103
- end
104
- end
105
-
106
- describe '#set_state' do
107
- it 'returns the state' do
108
- state = 'state'
109
- expect(data_store.set_state(light, state)).to eql(state)
110
- end
111
-
112
- it 'persists the state' do
113
- state = 'state'
114
- data_store.set_state(light, state)
115
- expect(data_store.get_state(light)).to eql(state)
116
- end
117
- end
118
-
119
- describe '#clear_state' do
120
- it 'returns the state' do
121
- state = 'state'
122
- data_store.set_state(light, state)
123
- expect(data_store.clear_state(light)).to eql(state)
124
- end
125
-
126
- it 'clears the state' do
127
- state = 'state'
128
- data_store.set_state(light, state)
129
- data_store.clear_state(light)
130
- expect(data_store.get_state(light)).to eql(Stoplight::State::UNLOCKED)
131
- end
132
- end
10
+ let(:other) { Stoplight::Failure.new('class', 'message 2', Time.new) }
11
+
12
+ it_behaves_like 'Stoplight::DataStore::Base'
13
+ it_behaves_like 'Stoplight::DataStore::Base#names'
14
+ it_behaves_like 'Stoplight::DataStore::Base#get_failures'
15
+ it_behaves_like 'Stoplight::DataStore::Base#get_all'
16
+ it_behaves_like 'Stoplight::DataStore::Base#record_failure'
17
+ it_behaves_like 'Stoplight::DataStore::Base#clear_failures'
18
+ it_behaves_like 'Stoplight::DataStore::Base#get_state'
19
+ it_behaves_like 'Stoplight::DataStore::Base#set_state'
20
+ it_behaves_like 'Stoplight::DataStore::Base#clear_state'
21
+ it_behaves_like 'Stoplight::DataStore::Base#with_notification_lock'
133
22
  end
@@ -1,146 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
- require 'fakeredis'
5
4
 
6
- RSpec.describe Stoplight::DataStore::Redis do
7
- let(:data_store) { described_class.new(redis) }
8
- let(:redis) { Redis.new }
5
+ RSpec.describe Stoplight::DataStore::Redis, :redis do
6
+ let(:data_store) { described_class.new(redis, redlock: redlock) }
7
+ let(:redlock) { instance_double(Redlock::Client) }
9
8
  let(:light) { Stoplight::Light.new(name) {} }
10
9
  let(:name) { ('a'..'z').to_a.shuffle.join }
11
- let(:failure) { Stoplight::Failure.new('class', 'message', Time.new) }
10
+ let(:failure) { Stoplight::Failure.new('class', 'message', Time.new - 60) }
11
+ let(:other) { Stoplight::Failure.new('class', 'message 2', Time.new) }
12
12
 
13
- before { Redis::Connection::Memory.reset_all_databases }
13
+ it_behaves_like 'Stoplight::DataStore::Base'
14
+ it_behaves_like 'Stoplight::DataStore::Base#names'
15
+ it_behaves_like 'Stoplight::DataStore::Base#get_all'
16
+ it_behaves_like 'Stoplight::DataStore::Base#record_failure'
17
+ it_behaves_like 'Stoplight::DataStore::Base#clear_failures'
18
+ it_behaves_like 'Stoplight::DataStore::Base#get_state'
19
+ it_behaves_like 'Stoplight::DataStore::Base#set_state'
20
+ it_behaves_like 'Stoplight::DataStore::Base#clear_state'
14
21
 
15
- it 'is a class' do
16
- expect(described_class).to be_a(Class)
17
- end
18
-
19
- it 'is a subclass of Base' do
20
- expect(described_class).to be < Stoplight::DataStore::Base
21
- end
22
+ it_behaves_like 'Stoplight::DataStore::Base#get_failures' do
23
+ context 'when JSON is invalid' do
24
+ before do
25
+ light.with_error_notifier { |_error| }
26
+ end
22
27
 
23
- describe '#names' do
24
- it 'is initially empty' do
25
- expect(data_store.names).to eql([])
26
- end
27
-
28
- it 'contains the name of a light with a failure' do
29
- data_store.record_failure(light, failure)
30
- expect(data_store.names).to eql([light.name])
31
- end
28
+ it 'handles it without an error' do
29
+ expect(failure).to receive(:to_json).and_return('invalid JSON')
32
30
 
33
- it 'contains the name of a light with a set state' do
34
- data_store.set_state(light, Stoplight::State::UNLOCKED)
35
- expect(data_store.names).to eql([light.name])
36
- end
37
-
38
- it 'does not duplicate names' do
39
- data_store.record_failure(light, failure)
40
- data_store.set_state(light, Stoplight::State::UNLOCKED)
41
- expect(data_store.names).to eql([light.name])
42
- end
43
-
44
- it 'supports names containing colons' do
45
- light = Stoplight::Light.new('http://api.example.com/some/action')
46
- data_store.record_failure(light, failure)
47
- expect(data_store.names).to eql([light.name])
31
+ expect { data_store.record_failure(light, failure) }
32
+ .to change { data_store.get_failures(light) }
33
+ .to([have_attributes(error_class: 'JSON::ParserError')])
34
+ end
48
35
  end
49
36
  end
50
37
 
51
- describe '#get_all' do
52
- it 'returns the failures and the state' do
53
- failures, state = data_store.get_all(light)
54
- expect(failures).to eql([])
55
- expect(state).to eql(Stoplight::State::UNLOCKED)
56
- end
57
- end
58
-
59
- describe '#get_failures' do
60
- it 'is initially empty' do
61
- expect(data_store.get_failures(light)).to eql([])
62
- end
63
-
64
- it 'handles invalid JSON' do
65
- expect(redis.keys.size).to eql(0)
66
- data_store.record_failure(light, failure)
67
- expect(redis.keys.size).to eql(1)
68
- redis.lset(redis.keys.first, 0, 'invalid JSON')
69
- light.with_error_notifier { |_error| }
70
- expect(data_store.get_failures(light).size).to eql(1)
71
- end
72
- end
73
-
74
- describe '#record_failure' do
75
- it 'returns the number of failures' do
76
- expect(data_store.record_failure(light, failure)).to eql(1)
77
- end
78
-
79
- it 'persists the failure' do
80
- data_store.record_failure(light, failure)
81
- expect(data_store.get_failures(light)).to eq([failure])
82
- end
83
-
84
- it 'stores more recent failures at the head' do
85
- data_store.record_failure(light, failure)
86
- other = Stoplight::Failure.new('class', 'message 2', Time.new)
87
- data_store.record_failure(light, other)
88
- expect(data_store.get_failures(light)).to eq([other, failure])
89
- end
90
-
91
- it 'limits the number of stored failures' do
92
- light.with_threshold(1)
93
- data_store.record_failure(light, failure)
94
- other = Stoplight::Failure.new('class', 'message 2', Time.new)
95
- data_store.record_failure(light, other)
96
- expect(data_store.get_failures(light)).to eq([other])
97
- end
98
- end
99
-
100
- describe '#clear_failures' do
101
- it 'returns the failures' do
102
- data_store.record_failure(light, failure)
103
- expect(data_store.clear_failures(light)).to eq([failure])
104
- end
105
-
106
- it 'clears the failures' do
107
- data_store.record_failure(light, failure)
108
- data_store.clear_failures(light)
109
- expect(data_store.get_failures(light)).to eql([])
110
- end
111
- end
112
-
113
- describe '#get_state' do
114
- it 'is initially unlocked' do
115
- expect(data_store.get_state(light)).to eql(Stoplight::State::UNLOCKED)
116
- end
117
- end
118
-
119
- describe '#set_state' do
120
- it 'returns the state' do
121
- state = 'state'
122
- expect(data_store.set_state(light, state)).to eql(state)
123
- end
124
-
125
- it 'persists the state' do
126
- state = 'state'
127
- data_store.set_state(light, state)
128
- expect(data_store.get_state(light)).to eql(state)
129
- end
130
- end
131
-
132
- describe '#clear_state' do
133
- it 'returns the state' do
134
- state = 'state'
135
- data_store.set_state(light, state)
136
- expect(data_store.clear_state(light)).to eql(state)
137
- end
38
+ it_behaves_like 'Stoplight::DataStore::Base#with_notification_lock' do
39
+ let(:lock_key) { "stoplight:v4:notification_lock:#{name}" }
138
40
 
139
- it 'clears the state' do
140
- state = 'state'
141
- data_store.set_state(light, state)
142
- data_store.clear_state(light)
143
- expect(data_store.get_state(light)).to eql(Stoplight::State::UNLOCKED)
41
+ before do
42
+ allow(redlock).to receive(:lock).with(lock_key, 2_000).and_yield
144
43
  end
145
44
  end
146
45
  end
@@ -17,6 +17,16 @@ RSpec.describe Stoplight::Error do
17
17
  end
18
18
  end
19
19
 
20
+ describe '::IncorrectColor' do
21
+ it 'is a class' do
22
+ expect(Stoplight::Error::IncorrectColor).to be_a(Class)
23
+ end
24
+
25
+ it 'is a subclass of StandardError' do
26
+ expect(Stoplight::Error::IncorrectColor).to be < StandardError
27
+ end
28
+ end
29
+
20
30
  describe '::RedLight' do
21
31
  it 'is a class' do
22
32
  expect(Stoplight::Error::RedLight).to be_a(Class)
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Stoplight::Light::Lockable do
6
+ subject(:light) { Stoplight::Light.new(name, &code) }
7
+
8
+ let(:code) { -> { code_result } }
9
+ let(:code_result) { random_string }
10
+ let(:name) { random_string }
11
+
12
+ def random_string
13
+ ('a'..'z').to_a.sample(8).join
14
+ end
15
+
16
+ describe '#lock' do
17
+ let(:color) { Stoplight::Color::GREEN }
18
+
19
+ context 'with correct color' do
20
+ it 'returns the light' do
21
+ expect(light.lock(color)).to be_a Stoplight::Light
22
+ end
23
+
24
+ context 'with green color' do
25
+ let(:color) { Stoplight::Color::GREEN }
26
+
27
+ it 'locks green color' do
28
+ expect(light.data_store).to receive(:set_state).with(light, Stoplight::State::LOCKED_GREEN)
29
+
30
+ light.lock(color)
31
+ end
32
+ end
33
+
34
+ context 'with red color' do
35
+ let(:color) { Stoplight::Color::RED }
36
+
37
+ it 'locks red color' do
38
+ expect(light.data_store).to receive(:set_state).with(light, Stoplight::State::LOCKED_RED)
39
+
40
+ light.lock(color)
41
+ end
42
+ end
43
+ end
44
+
45
+ context 'with incorrect color' do
46
+ let(:color) { 'incorrect-color' }
47
+
48
+ it 'raises Error::IncorrectColor error' do
49
+ expect { light.lock(color) }.to raise_error(Stoplight::Error::IncorrectColor)
50
+ end
51
+
52
+ it 'does not lock color' do
53
+ expect(light.data_store).to_not receive(:set_state)
54
+
55
+ suppress(Stoplight::Error::IncorrectColor) { light.lock(color) }
56
+ end
57
+ end
58
+ end
59
+
60
+ describe '#unlock' do
61
+ it 'returns the light' do
62
+ expect(light.unlock).to be_a Stoplight::Light
63
+ end
64
+
65
+ context 'with locked green light' do
66
+ before { light.lock(Stoplight::Color::GREEN) }
67
+
68
+ it 'unlocks light' do
69
+ expect(light.data_store).to receive(:set_state).with(light, Stoplight::State::UNLOCKED)
70
+
71
+ light.unlock
72
+ end
73
+ end
74
+
75
+ context 'with locked red light' do
76
+ before { light.lock(Stoplight::Color::RED) }
77
+
78
+ it 'unlocks light' do
79
+ expect(light.data_store).to receive(:set_state).with(light, Stoplight::State::UNLOCKED)
80
+
81
+ light.unlock
82
+ end
83
+ end
84
+
85
+ context 'with unlocked light' do
86
+ it 'unlocks light' do
87
+ expect(light.data_store).to receive(:set_state).with(light, Stoplight::State::UNLOCKED)
88
+
89
+ light.unlock
90
+ end
91
+ end
92
+ end
93
+ end
@@ -3,15 +3,7 @@
3
3
  require 'spec_helper'
4
4
  require 'stringio'
5
5
 
6
- RSpec.describe Stoplight::Light::Runnable do
7
- subject { Stoplight::Light.new(name, &code) }
8
-
9
- let(:code) { -> { code_result } }
10
- let(:code_result) { random_string }
11
- let(:fallback) { ->(_) { fallback_result } }
12
- let(:fallback_result) { random_string }
13
- let(:name) { random_string }
14
-
6
+ RSpec.describe Stoplight::Light::Runnable, :redis do
15
7
  let(:failure) do
16
8
  Stoplight::Failure.new(error.class.name, error.message, time)
17
9
  end
@@ -24,234 +16,21 @@ RSpec.describe Stoplight::Light::Runnable do
24
16
  ('a'..'z').to_a.sample(8).join
25
17
  end
26
18
 
27
- describe '#color' do
28
- it 'is initially green' do
29
- expect(subject.color).to eql(Stoplight::Color::GREEN)
30
- end
31
-
32
- it 'is green when locked green' do
33
- subject.data_store.set_state(subject, Stoplight::State::LOCKED_GREEN)
34
- expect(subject.color).to eql(Stoplight::Color::GREEN)
35
- end
36
-
37
- it 'is red when locked red' do
38
- subject.data_store.set_state(subject, Stoplight::State::LOCKED_RED)
39
- expect(subject.color).to eql(Stoplight::Color::RED)
40
- end
41
-
42
- it 'is red when there are many failures' do
43
- subject.threshold.times do
44
- subject.data_store.record_failure(subject, failure)
45
- end
46
- expect(subject.color).to eql(Stoplight::Color::RED)
47
- end
48
-
49
- it 'is yellow when the most recent failure is old' do
50
- (subject.threshold - 1).times do
51
- subject.data_store.record_failure(subject, failure)
52
- end
53
- other = Stoplight::Failure.new(
54
- error.class.name, error.message, Time.new - subject.cool_off_time
55
- )
56
- subject.data_store.record_failure(subject, other)
57
- expect(subject.color).to eql(Stoplight::Color::YELLOW)
58
- end
59
-
60
- it 'is red when the least recent failure is old' do
61
- other = Stoplight::Failure.new(
62
- error.class.name, error.message, Time.new - subject.cool_off_time
63
- )
64
- subject.data_store.record_failure(subject, other)
65
- (subject.threshold - 1).times do
66
- subject.data_store.record_failure(subject, failure)
67
- end
68
- expect(subject.color).to eql(Stoplight::Color::RED)
69
- end
19
+ before do
20
+ light.with_data_store(data_store)
70
21
  end
71
22
 
72
- describe '#run' do
73
- let(:notifiers) { [notifier] }
74
- let(:notifier) { Stoplight::Notifier::IO.new(io) }
75
- let(:io) { StringIO.new }
76
-
77
- before { subject.with_notifiers(notifiers) }
78
-
79
- context 'when the light is green' do
80
- before { subject.data_store.clear_failures(subject) }
81
-
82
- it 'runs the code' do
83
- expect(subject.run).to eql(code_result)
84
- end
85
-
86
- context 'with some failures' do
87
- before { subject.data_store.record_failure(subject, failure) }
88
-
89
- it 'clears the failures' do
90
- subject.run
91
- expect(subject.data_store.get_failures(subject).size).to eql(0)
92
- end
93
- end
94
-
95
- context 'when the code is failing' do
96
- let(:code_result) { raise error }
97
-
98
- it 're-raises the error' do
99
- expect { subject.run }.to raise_error(error.class)
100
- end
101
-
102
- it 'records the failure' do
103
- expect(subject.data_store.get_failures(subject).size).to eql(0)
104
- begin
105
- subject.run
106
- rescue error.class
107
- nil
108
- end
109
- expect(subject.data_store.get_failures(subject).size).to eql(1)
110
- end
111
-
112
- it 'notifies when transitioning to red' do
113
- subject.threshold.times do
114
- expect(io.string).to eql('')
115
- begin
116
- subject.run
117
- rescue error.class
118
- nil
119
- end
120
- end
121
- expect(io.string).to_not eql('')
122
- end
123
-
124
- context 'with an error handler' do
125
- let(:result) do
126
- subject.run
127
- expect(false).to be(true)
128
- rescue error.class
129
- expect(true).to be(true)
130
- end
131
-
132
- it 'records the failure when the handler does nothing' do
133
- subject.with_error_handler { |_error, _handler| }
134
- expect { result }
135
- .to change { subject.data_store.get_failures(subject).size }
136
- .by(1)
137
- end
23
+ context 'with memory data store' do
24
+ let(:data_store) { Stoplight::DataStore::Memory.new }
138
25
 
139
- it 'records the failure when the handler calls handle' do
140
- subject.with_error_handler { |error, handle| handle.call(error) }
141
- expect { result }
142
- .to change { subject.data_store.get_failures(subject).size }
143
- .by(1)
144
- end
145
-
146
- it 'does not record the failure when the handler raises' do
147
- subject.with_error_handler { |error, _handle| raise error }
148
- expect { result }
149
- .to_not change { subject.data_store.get_failures(subject).size }
150
- end
151
- end
152
-
153
- context 'with a fallback' do
154
- before { subject.with_fallback(&fallback) }
155
-
156
- it 'runs the fallback' do
157
- expect(subject.run).to eql(fallback_result)
158
- end
159
-
160
- it 'passes the error to the fallback' do
161
- subject.with_fallback do |e|
162
- expect(e).to eql(error)
163
- fallback_result
164
- end
165
- expect(subject.run).to eql(fallback_result)
166
- end
167
- end
168
- end
169
-
170
- context 'when the data store is failing' do
171
- let(:data_store) { Object.new }
172
- let(:error_notifier) { ->(_) {} }
173
-
174
- before do
175
- subject
176
- .with_data_store(data_store)
177
- .with_error_notifier(&error_notifier)
178
- end
179
-
180
- it 'runs the code' do
181
- expect(subject.run).to eql(code_result)
182
- end
183
-
184
- it 'notifies about the error' do
185
- has_notified = false
186
- subject.with_error_notifier do |e|
187
- has_notified = true
188
- expect(e).to be_a(NoMethodError)
189
- end
190
- subject.run
191
- expect(has_notified).to eql(true)
192
- end
193
- end
194
- end
195
-
196
- context 'when the light is yellow' do
197
- before do
198
- (subject.threshold - 1).times do
199
- subject.data_store.record_failure(subject, failure)
200
- end
201
-
202
- other = Stoplight::Failure.new(
203
- error.class.name, error.message, time - subject.cool_off_time
204
- )
205
- subject.data_store.record_failure(subject, other)
206
- end
207
-
208
- it 'runs the code' do
209
- expect(subject.run).to eql(code_result)
210
- end
211
-
212
- it 'notifies when transitioning to green' do
213
- expect(io.string).to eql('')
214
- subject.run
215
- expect(io.string).to_not eql('')
216
- end
217
- end
218
-
219
- context 'when the light is red' do
220
- before do
221
- subject.threshold.times do
222
- subject.data_store.record_failure(subject, failure)
223
- end
224
- end
225
-
226
- it 'raises an error' do
227
- expect { subject.run }.to raise_error(Stoplight::Error::RedLight)
228
- end
229
-
230
- it 'uses the name as the error message' do
231
- e =
232
- begin
233
- subject.run
234
- rescue Stoplight::Error::RedLight => e
235
- e
236
- end
237
- expect(e.message).to eql(subject.name)
238
- end
239
-
240
- context 'with a fallback' do
241
- before { subject.with_fallback(&fallback) }
26
+ it_behaves_like 'Stoplight::Light::Runnable#color'
27
+ it_behaves_like 'Stoplight::Light::Runnable#run'
28
+ end
242
29
 
243
- it 'runs the fallback' do
244
- expect(subject.run).to eql(fallback_result)
245
- end
30
+ context 'with redis data store', :redis do
31
+ let(:data_store) { Stoplight::DataStore::Redis.new(redis) }
246
32
 
247
- it 'does not pass anything to the fallback' do
248
- subject.with_fallback do |e|
249
- expect(e).to eql(nil)
250
- fallback_result
251
- end
252
- expect(subject.run).to eql(fallback_result)
253
- end
254
- end
255
- end
33
+ it_behaves_like 'Stoplight::Light::Runnable#color'
34
+ it_behaves_like 'Stoplight::Light::Runnable#run'
256
35
  end
257
36
  end