stoplight 3.0.2 → 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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +176 -180
  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/memory.rb +20 -5
  8. data/lib/stoplight/data_store/redis.rb +37 -5
  9. data/lib/stoplight/default.rb +2 -0
  10. data/lib/stoplight/error.rb +1 -0
  11. data/lib/stoplight/light/deprecated.rb +44 -0
  12. data/lib/stoplight/light/lockable.rb +45 -0
  13. data/lib/stoplight/light/runnable.rb +25 -24
  14. data/lib/stoplight/light.rb +69 -63
  15. data/lib/stoplight/rspec/generic_notifier.rb +42 -0
  16. data/lib/stoplight/rspec.rb +3 -0
  17. data/lib/stoplight/version.rb +1 -1
  18. data/lib/stoplight.rb +32 -8
  19. data/spec/spec_helper.rb +7 -0
  20. data/spec/stoplight/builder_spec.rb +165 -0
  21. data/spec/stoplight/circuit_breaker_spec.rb +35 -0
  22. data/spec/stoplight/configurable_spec.rb +25 -0
  23. data/spec/stoplight/data_store/memory_spec.rb +12 -149
  24. data/spec/stoplight/data_store/redis_spec.rb +26 -158
  25. data/spec/stoplight/error_spec.rb +10 -0
  26. data/spec/stoplight/light/lockable_spec.rb +93 -0
  27. data/spec/stoplight/light/runnable_spec.rb +12 -273
  28. data/spec/stoplight/light_spec.rb +4 -28
  29. data/spec/stoplight/notifier/generic_spec.rb +35 -35
  30. data/spec/stoplight/notifier/io_spec.rb +1 -0
  31. data/spec/stoplight/notifier/logger_spec.rb +3 -0
  32. data/spec/stoplight_spec.rb +17 -6
  33. data/spec/support/configurable.rb +69 -0
  34. data/spec/support/data_store/base/clear_failures.rb +18 -0
  35. data/spec/support/data_store/base/clear_state.rb +20 -0
  36. data/spec/support/data_store/base/get_all.rb +44 -0
  37. data/spec/support/data_store/base/get_failures.rb +30 -0
  38. data/spec/support/data_store/base/get_state.rb +7 -0
  39. data/spec/support/data_store/base/names.rb +29 -0
  40. data/spec/support/data_store/base/record_failures.rb +70 -0
  41. data/spec/support/data_store/base/set_state.rb +15 -0
  42. data/spec/support/data_store/base/with_notification_lock.rb +27 -0
  43. data/spec/support/data_store/base.rb +21 -0
  44. data/spec/support/database_cleaner.rb +26 -0
  45. data/spec/support/exception_helpers.rb +9 -0
  46. data/spec/support/light/runnable/color.rb +79 -0
  47. data/spec/support/light/runnable/run.rb +247 -0
  48. data/spec/support/light/runnable.rb +4 -0
  49. metadata +51 -231
  50. data/lib/stoplight/notifier/bugsnag.rb +0 -37
  51. data/lib/stoplight/notifier/honeybadger.rb +0 -44
  52. data/lib/stoplight/notifier/pagerduty.rb +0 -21
  53. data/lib/stoplight/notifier/raven.rb +0 -40
  54. data/lib/stoplight/notifier/rollbar.rb +0 -39
  55. data/lib/stoplight/notifier/slack.rb +0 -21
  56. data/spec/stoplight/notifier/bugsnag_spec.rb +0 -90
  57. data/spec/stoplight/notifier/honeybadger_spec.rb +0 -88
  58. data/spec/stoplight/notifier/pagerduty_spec.rb +0 -40
  59. data/spec/stoplight/notifier/raven_spec.rb +0 -90
  60. data/spec/stoplight/notifier/rollbar_spec.rb +0 -90
  61. data/spec/stoplight/notifier/slack_spec.rb +0 -46
@@ -7,153 +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
133
-
134
- describe '#with_notification_lock' do
135
- context 'when notification is already sent' do
136
- before do
137
- data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED) {}
138
- end
139
-
140
- it 'does not yield passed block' do
141
- expect do |b|
142
- data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED, &b)
143
- end.not_to yield_control
144
- end
145
- end
146
-
147
- context 'when notification is not already sent' do
148
- before do
149
- data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED) {}
150
- end
151
-
152
- it 'yields passed block' do
153
- expect do |b|
154
- data_store.with_notification_lock(light, Stoplight::Color::RED, Stoplight::Color::GREEN, &b)
155
- end.to yield_control
156
- end
157
- end
158
- 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'
159
22
  end
@@ -1,177 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
- require 'mock_redis'
5
4
 
6
- RSpec.describe Stoplight::DataStore::Redis do
5
+ RSpec.describe Stoplight::DataStore::Redis, :redis do
7
6
  let(:data_store) { described_class.new(redis, redlock: redlock) }
8
- let(:redis) { MockRedis.new }
9
7
  let(:redlock) { instance_double(Redlock::Client) }
10
8
  let(:light) { Stoplight::Light.new(name) {} }
11
9
  let(:name) { ('a'..'z').to_a.shuffle.join }
12
- let(:failure) { Stoplight::Failure.new('class', 'message', Time.new) }
13
-
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
-
22
- describe '#names' do
23
- it 'is initially empty' do
24
- expect(data_store.names).to eql([])
25
- end
26
-
27
- it 'contains the name of a light with a failure' do
28
- data_store.record_failure(light, failure)
29
- expect(data_store.names).to eql([light.name])
30
- end
31
-
32
- it 'contains the name of a light with a set state' do
33
- data_store.set_state(light, Stoplight::State::UNLOCKED)
34
- expect(data_store.names).to eql([light.name])
35
- end
36
-
37
- it 'does not duplicate names' do
38
- data_store.record_failure(light, failure)
39
- data_store.set_state(light, Stoplight::State::UNLOCKED)
40
- expect(data_store.names).to eql([light.name])
41
- end
42
-
43
- it 'supports names containing colons' do
44
- light = Stoplight::Light.new('http://api.example.com/some/action')
45
- data_store.record_failure(light, failure)
46
- expect(data_store.names).to eql([light.name])
47
- end
48
- end
49
-
50
- describe '#get_all' do
51
- it 'returns the failures and the state' do
52
- failures, state = data_store.get_all(light)
53
- expect(failures).to eql([])
54
- expect(state).to eql(Stoplight::State::UNLOCKED)
55
- end
56
- end
57
-
58
- describe '#get_failures' do
59
- it 'is initially empty' do
60
- expect(data_store.get_failures(light)).to eql([])
61
- end
62
-
63
- it 'handles invalid JSON' do
64
- expect(redis.keys.size).to eql(0)
65
- data_store.record_failure(light, failure)
66
- expect(redis.keys.size).to eql(1)
67
- redis.lset(redis.keys.first, 0, 'invalid JSON')
68
- light.with_error_notifier { |_error| }
69
- expect(data_store.get_failures(light).size).to eql(1)
70
- end
71
- end
72
-
73
- describe '#record_failure' do
74
- it 'returns the number of failures' do
75
- expect(data_store.record_failure(light, failure)).to eql(1)
76
- end
77
-
78
- it 'persists the failure' do
79
- data_store.record_failure(light, failure)
80
- expect(data_store.get_failures(light)).to eq([failure])
81
- end
82
-
83
- it 'stores more recent failures at the head' do
84
- data_store.record_failure(light, failure)
85
- other = Stoplight::Failure.new('class', 'message 2', Time.new)
86
- data_store.record_failure(light, other)
87
- expect(data_store.get_failures(light)).to eq([other, failure])
88
- end
89
-
90
- it 'limits the number of stored failures' do
91
- light.with_threshold(1)
92
- data_store.record_failure(light, failure)
93
- other = Stoplight::Failure.new('class', 'message 2', Time.new)
94
- data_store.record_failure(light, other)
95
- expect(data_store.get_failures(light)).to eq([other])
96
- end
97
- end
98
-
99
- describe '#clear_failures' do
100
- it 'returns the failures' do
101
- data_store.record_failure(light, failure)
102
- expect(data_store.clear_failures(light)).to eq([failure])
103
- end
104
-
105
- it 'clears the failures' do
106
- data_store.record_failure(light, failure)
107
- data_store.clear_failures(light)
108
- expect(data_store.get_failures(light)).to eql([])
109
- end
110
- end
111
-
112
- describe '#get_state' do
113
- it 'is initially unlocked' do
114
- expect(data_store.get_state(light)).to eql(Stoplight::State::UNLOCKED)
115
- end
116
- end
117
-
118
- describe '#set_state' do
119
- it 'returns the state' do
120
- state = 'state'
121
- expect(data_store.set_state(light, state)).to eql(state)
122
- end
123
-
124
- it 'persists the state' do
125
- state = 'state'
126
- data_store.set_state(light, state)
127
- expect(data_store.get_state(light)).to eql(state)
128
- end
129
- end
10
+ let(:failure) { Stoplight::Failure.new('class', 'message', Time.new - 60) }
11
+ let(:other) { Stoplight::Failure.new('class', 'message 2', Time.new) }
12
+
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'
21
+
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
130
27
 
131
- describe '#clear_state' do
132
- it 'returns the state' do
133
- state = 'state'
134
- data_store.set_state(light, state)
135
- expect(data_store.clear_state(light)).to eql(state)
136
- end
28
+ it 'handles it without an error' do
29
+ expect(failure).to receive(:to_json).and_return('invalid JSON')
137
30
 
138
- it 'clears the state' do
139
- state = 'state'
140
- data_store.set_state(light, state)
141
- data_store.clear_state(light)
142
- expect(data_store.get_state(light)).to eql(Stoplight::State::UNLOCKED)
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
143
35
  end
144
36
  end
145
37
 
146
- describe '#with_notification_lock' do
147
- let(:lock_key) { "stoplight:notification_lock:#{name}" }
38
+ it_behaves_like 'Stoplight::DataStore::Base#with_notification_lock' do
39
+ let(:lock_key) { "stoplight:v4:notification_lock:#{name}" }
148
40
 
149
41
  before do
150
42
  allow(redlock).to receive(:lock).with(lock_key, 2_000).and_yield
151
43
  end
152
-
153
- context 'when notification is already sent' do
154
- before do
155
- data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED) {}
156
- end
157
-
158
- it 'does not yield passed block' do
159
- expect do |b|
160
- data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED, &b)
161
- end.not_to yield_control
162
- end
163
- end
164
-
165
- context 'when notification is not already sent' do
166
- before do
167
- data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED) {}
168
- end
169
-
170
- it 'yields passed block' do
171
- expect do |b|
172
- data_store.with_notification_lock(light, Stoplight::Color::RED, Stoplight::Color::GREEN, &b)
173
- end.to yield_control
174
- end
175
- end
176
44
  end
177
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