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
@@ -3,10 +3,7 @@
3
3
  module Stoplight
4
4
  module Notifier
5
5
  class Base
6
- # @param _light [Light]
7
- # @param _from_color [String]
8
- # @param _to_color [String]
9
- def notify(_light, _from_color, _to_color)
6
+ def notify(_light, _from_color, _to_color, _error)
10
7
  fail NotImplementedError
11
8
  end
12
9
  end
@@ -2,44 +2,29 @@
2
2
 
3
3
  module Stoplight
4
4
  module Notifier
5
- # @note hipchat ~> 1.3.0
6
5
  class HipChat < Base
7
- DEFAULT_FORMATTER = lambda do |light, from_color, to_color|
8
- "@all Switching #{light.name} from #{from_color} to #{to_color}"
9
- end
10
- DEFAULT_OPTIONS = { color: 'red', message_format: 'text', notify: true }
6
+ DEFAULT_OPTIONS = {
7
+ color: 'purple',
8
+ message_format: 'text',
9
+ notify: true
10
+ }.freeze
11
+
12
+ attr_reader :formatter
13
+ attr_reader :hip_chat
14
+ attr_reader :options
15
+ attr_reader :room
11
16
 
12
- # @param client [HipChat::Client]
13
- # @param room [String]
14
- # @param formatter [Proc, nil]
15
- # @param options [Hash]
16
- def initialize(client, room, formatter = nil, options = {})
17
- @client = client
17
+ def initialize(hip_chat, room, formatter = nil, options = {})
18
+ @hip_chat = hip_chat
18
19
  @room = room
19
- @formatter = formatter || DEFAULT_FORMATTER
20
+ @formatter = formatter || Default::FORMATTER
20
21
  @options = DEFAULT_OPTIONS.merge(options)
21
22
  end
22
23
 
23
- def notify(light, from_color, to_color)
24
- message = @formatter.call(light, from_color, to_color)
25
- @client[@room].send('Stoplight', message, @options)
26
- rescue *errors => error
27
- raise Error::BadNotifier, error
28
- end
29
-
30
- private
31
-
32
- def errors
33
- [
34
- ::HipChat::InvalidApiVersion,
35
- ::HipChat::RoomMissingOwnerUserId,
36
- ::HipChat::RoomNameTooLong,
37
- ::HipChat::Unauthorized,
38
- ::HipChat::UnknownResponseCode,
39
- ::HipChat::UnknownRoom,
40
- ::HipChat::UnknownUser,
41
- ::HipChat::UsernameTooLong
42
- ]
24
+ def notify(light, from_color, to_color, error)
25
+ message = formatter.call(light, from_color, to_color, error)
26
+ hip_chat[room].send('Stoplight', message, options)
27
+ message
43
28
  end
44
29
  end
45
30
  end
@@ -1,22 +1,22 @@
1
1
  # coding: utf-8
2
2
 
3
+ require 'stringio'
4
+
3
5
  module Stoplight
4
6
  module Notifier
5
7
  class IO < Base
6
- DEFAULT_FORMATTER = lambda do |light, from_color, to_color|
7
- "Switching #{light.name} from #{from_color} to #{to_color}"
8
- end
8
+ attr_reader :formatter
9
+ attr_reader :io
9
10
 
10
- # @param io [IO]
11
- # @param formatter [Proc, nil]
12
11
  def initialize(io, formatter = nil)
13
12
  @io = io
14
- @formatter = formatter || DEFAULT_FORMATTER
13
+ @formatter = formatter || Default::FORMATTER
15
14
  end
16
15
 
17
- def notify(light, from_color, to_color)
18
- message = @formatter.call(light, from_color, to_color)
19
- @io.puts(message)
16
+ def notify(light, from_color, to_color, error)
17
+ message = formatter.call(light, from_color, to_color, error)
18
+ io.puts(message)
19
+ message
20
20
  end
21
21
  end
22
22
  end
@@ -0,0 +1,9 @@
1
+ # coding: utf-8
2
+
3
+ module Stoplight
4
+ module State
5
+ UNLOCKED = 'unlocked'.freeze
6
+ LOCKED_GREEN = 'locked_green'.freeze
7
+ LOCKED_RED = 'locked_red'.freeze
8
+ end
9
+ end
data/spec/spec_helper.rb CHANGED
@@ -4,7 +4,6 @@ require 'coveralls'
4
4
  Coveralls.wear!
5
5
 
6
6
  require 'stoplight'
7
+ require 'timecop'
7
8
 
8
- Dir.glob(File.join('.', 'spec', 'support', '**', '*.rb')).each do |filename|
9
- require filename
10
- end
9
+ Timecop.safe_mode = true
@@ -0,0 +1,39 @@
1
+ # coding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Stoplight::Color do
6
+ it 'is a module' do
7
+ expect(described_class).to be_a(Module)
8
+ end
9
+
10
+ describe '::GREEN' do
11
+ it 'is a string' do
12
+ expect(Stoplight::Color::GREEN).to be_a(String)
13
+ end
14
+
15
+ it 'is frozen' do
16
+ expect(Stoplight::Color::GREEN).to be_frozen
17
+ end
18
+ end
19
+
20
+ describe '::YELLOW' do
21
+ it 'is a string' do
22
+ expect(Stoplight::Color::YELLOW).to be_a(String)
23
+ end
24
+
25
+ it 'is frozen' do
26
+ expect(Stoplight::Color::YELLOW).to be_frozen
27
+ end
28
+ end
29
+
30
+ describe '::RED' do
31
+ it 'is a string' do
32
+ expect(Stoplight::Color::RED).to be_a(String)
33
+ end
34
+
35
+ it 'is frozen' do
36
+ expect(Stoplight::Color::RED).to be_frozen
37
+ end
38
+ end
39
+ end
@@ -3,42 +3,62 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe Stoplight::DataStore::Base do
6
- subject(:data_store) { described_class.new }
7
-
8
- %w(
9
- names
10
- clear_stale
11
- clear
12
- sync
13
- greenify
14
- green?
15
- yellow?
16
- red?
17
- get_color
18
- get_attempts
19
- record_attempt
20
- clear_attempts
21
- get_failures
22
- record_failure
23
- clear_failures
24
- get_state
25
- set_state
26
- clear_state
27
- get_threshold
28
- set_threshold
29
- clear_threshold
30
- get_timeout
31
- set_timeout
32
- clear_timeout
33
- ).each do |method|
34
- it "responds to #{method}" do
35
- expect(data_store).to respond_to(method)
36
- end
37
-
38
- it "does not implement #{method}" do
39
- args = [nil] * data_store.method(method).arity
40
- expect { data_store.public_send(method, *args) }.to raise_error(
41
- NotImplementedError)
6
+ let(:data_store) { described_class.new }
7
+
8
+ it 'is a class' do
9
+ expect(described_class).to be_a(Class)
10
+ end
11
+
12
+ describe '#names' do
13
+ it 'is not implemented' do
14
+ expect { data_store.names }.to raise_error(NotImplementedError)
15
+ end
16
+ end
17
+
18
+ describe '#get_all' do
19
+ it 'is not implemented' do
20
+ expect { data_store.get_all(nil) }.to raise_error(NotImplementedError)
21
+ end
22
+ end
23
+
24
+ describe '#get_failures' do
25
+ it 'is not implemented' do
26
+ expect { data_store.get_failures(nil) }
27
+ .to raise_error(NotImplementedError)
28
+ end
29
+ end
30
+
31
+ describe '#record_failure' do
32
+ it 'is not implemented' do
33
+ expect { data_store.record_failure(nil, nil) }
34
+ .to raise_error(NotImplementedError)
35
+ end
36
+ end
37
+
38
+ describe '#clear_failures' do
39
+ it 'is not implemented' do
40
+ expect { data_store.clear_failures(nil) }
41
+ .to raise_error(NotImplementedError)
42
+ end
43
+ end
44
+
45
+ describe '#get_state' do
46
+ it 'is not implemented' do
47
+ expect { data_store.get_state(nil) }.to raise_error(NotImplementedError)
48
+ end
49
+ end
50
+
51
+ describe '#set_state' do
52
+ it 'is not implemented' do
53
+ expect { data_store.set_state(nil, nil) }
54
+ .to raise_error(NotImplementedError)
55
+ end
56
+ end
57
+
58
+ describe '#clear_state' do
59
+ it 'is not implemented' do
60
+ expect { data_store.clear_state(nil) }
61
+ .to raise_error(NotImplementedError)
42
62
  end
43
63
  end
44
64
  end
@@ -3,7 +3,125 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe Stoplight::DataStore::Memory do
6
- subject(:data_store) { described_class.new }
6
+ let(:data_store) { described_class.new }
7
+ let(:light) { Stoplight::Light.new(name) {} }
8
+ let(:name) { ('a'..'z').to_a.shuffle.join }
9
+ let(:failure) { Stoplight::Failure.new('class', 'message', Time.new) }
7
10
 
8
- it_behaves_like 'a data store'
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
+ end
40
+
41
+ describe '#get_all' do
42
+ it 'returns the failures and the state' do
43
+ failures, state = data_store.get_all(light)
44
+ expect(failures).to eql([])
45
+ expect(state).to eql(Stoplight::State::UNLOCKED)
46
+ end
47
+ end
48
+
49
+ describe '#get_failures' do
50
+ it 'is initially empty' do
51
+ expect(data_store.get_failures(light)).to eql([])
52
+ end
53
+ end
54
+
55
+ describe '#record_failure' do
56
+ it 'returns the number of failures' do
57
+ expect(data_store.record_failure(light, failure)).to eql(1)
58
+ end
59
+
60
+ it 'persists the failure' do
61
+ data_store.record_failure(light, failure)
62
+ expect(data_store.get_failures(light)).to eql([failure])
63
+ end
64
+
65
+ it 'stores more recent failures at the front' do
66
+ data_store.record_failure(light, failure)
67
+ other = Stoplight::Failure.new('class', 'message 2', Time.new)
68
+ data_store.record_failure(light, other)
69
+ expect(data_store.get_failures(light)).to eql([other, failure])
70
+ end
71
+
72
+ it 'limits the number of stored failures' do
73
+ light.with_threshold(1)
74
+ data_store.record_failure(light, failure)
75
+ other = Stoplight::Failure.new('class', 'message 2', Time.new)
76
+ data_store.record_failure(light, other)
77
+ expect(data_store.get_failures(light)).to eql([other])
78
+ end
79
+ end
80
+
81
+ describe '#clear_failures' do
82
+ it 'returns the failures' do
83
+ data_store.record_failure(light, failure)
84
+ expect(data_store.clear_failures(light)).to eql([failure])
85
+ end
86
+
87
+ it 'clears the failures' do
88
+ data_store.record_failure(light, failure)
89
+ data_store.clear_failures(light)
90
+ expect(data_store.get_failures(light)).to eql([])
91
+ end
92
+ end
93
+
94
+ describe '#get_state' do
95
+ it 'is initially unlocked' do
96
+ expect(data_store.get_state(light)).to eql(Stoplight::State::UNLOCKED)
97
+ end
98
+ end
99
+
100
+ describe '#set_state' do
101
+ it 'returns the state' do
102
+ state = 'state'
103
+ expect(data_store.set_state(light, state)).to eql(state)
104
+ end
105
+
106
+ it 'persists the state' do
107
+ state = 'state'
108
+ data_store.set_state(light, state)
109
+ expect(data_store.get_state(light)).to eql(state)
110
+ end
111
+ end
112
+
113
+ describe '#clear_state' do
114
+ it 'returns the state' do
115
+ state = 'state'
116
+ data_store.set_state(light, state)
117
+ expect(data_store.clear_state(light)).to eql(state)
118
+ end
119
+
120
+ it 'clears the state' do
121
+ state = 'state'
122
+ data_store.set_state(light, state)
123
+ data_store.clear_state(light)
124
+ expect(data_store.get_state(light)).to eql(Stoplight::State::UNLOCKED)
125
+ end
126
+ end
9
127
  end
@@ -1,41 +1,140 @@
1
1
  # coding: utf-8
2
2
 
3
3
  require 'spec_helper'
4
+ require 'fakeredis'
4
5
 
5
6
  describe Stoplight::DataStore::Redis do
6
- subject(:data_store) { described_class.new(redis) }
7
+ let(:data_store) { described_class.new(redis) }
7
8
  let(:redis) { Redis.new }
9
+ let(:light) { Stoplight::Light.new(name) {} }
10
+ let(:name) { ('a'..'z').to_a.shuffle.join }
11
+ let(:failure) { Stoplight::Failure.new('class', 'message', Time.new) }
8
12
 
9
- it_behaves_like 'a data store'
13
+ before { Redis::Connection::Memory.reset_all_databases }
10
14
 
11
- context 'with a failing connection' do
12
- let(:name) { SecureRandom.hex }
13
- let(:error) { Redis::BaseConnectionError.new(message) }
14
- let(:message) { SecureRandom.hex }
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
+
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
32
+
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
+ end
44
+
45
+ describe '#get_all' do
46
+ it 'returns the failures and the state' do
47
+ failures, state = data_store.get_all(light)
48
+ expect(failures).to eql([])
49
+ expect(state).to eql(Stoplight::State::UNLOCKED)
50
+ end
51
+ end
15
52
 
16
- before { allow(redis).to receive(:hget).and_raise(error) }
53
+ describe '#get_failures' do
54
+ it 'is initially empty' do
55
+ expect(data_store.get_failures(light)).to eql([])
56
+ end
17
57
 
18
- it 'reraises the error' do
19
- expect { data_store.sync(name) }
20
- .to raise_error(Stoplight::Error::BadDataStore)
58
+ it 'handles invalid JSON' do
59
+ expect(redis.keys.size).to eql(0)
60
+ data_store.record_failure(light, failure)
61
+ expect(redis.keys.size).to eql(1)
62
+ redis.lset(redis.keys.first, 0, 'invalid JSON')
63
+ light.with_error_notifier { |_error| }
64
+ expect(data_store.get_failures(light).size).to eql(1)
21
65
  end
66
+ end
67
+
68
+ describe '#record_failure' do
69
+ it 'returns the number of failures' do
70
+ expect(data_store.record_failure(light, failure)).to eql(1)
71
+ end
72
+
73
+ it 'persists the failure' do
74
+ data_store.record_failure(light, failure)
75
+ expect(data_store.get_failures(light)).to eq([failure])
76
+ end
77
+
78
+ it 'stores more recent failures at the head' do
79
+ data_store.record_failure(light, failure)
80
+ other = Stoplight::Failure.new('class', 'message 2', Time.new)
81
+ data_store.record_failure(light, other)
82
+ expect(data_store.get_failures(light)).to eq([other, failure])
83
+ end
84
+
85
+ it 'limits the number of stored failures' do
86
+ light.with_threshold(1)
87
+ data_store.record_failure(light, failure)
88
+ other = Stoplight::Failure.new('class', 'message 2', Time.new)
89
+ data_store.record_failure(light, other)
90
+ expect(data_store.get_failures(light)).to eq([other])
91
+ end
92
+ end
93
+
94
+ describe '#clear_failures' do
95
+ it 'returns the failures' do
96
+ data_store.record_failure(light, failure)
97
+ expect(data_store.clear_failures(light)).to eq([failure])
98
+ end
99
+
100
+ it 'clears the failures' do
101
+ data_store.record_failure(light, failure)
102
+ data_store.clear_failures(light)
103
+ expect(data_store.get_failures(light)).to eql([])
104
+ end
105
+ end
106
+
107
+ describe '#get_state' do
108
+ it 'is initially unlocked' do
109
+ expect(data_store.get_state(light)).to eql(Stoplight::State::UNLOCKED)
110
+ end
111
+ end
112
+
113
+ describe '#set_state' do
114
+ it 'returns the state' do
115
+ state = 'state'
116
+ expect(data_store.set_state(light, state)).to eql(state)
117
+ end
118
+
119
+ it 'persists the state' do
120
+ state = 'state'
121
+ data_store.set_state(light, state)
122
+ expect(data_store.get_state(light)).to eql(state)
123
+ end
124
+ end
22
125
 
23
- it 'sets the message' do
24
- begin
25
- data_store.sync(name)
26
- expect(false).to be(true)
27
- rescue Stoplight::Error::BadDataStore => e
28
- expect(e.message).to eql(message)
29
- end
126
+ describe '#clear_state' do
127
+ it 'returns the state' do
128
+ state = 'state'
129
+ data_store.set_state(light, state)
130
+ expect(data_store.clear_state(light)).to eql(state)
30
131
  end
31
132
 
32
- it 'sets the cause' do
33
- begin
34
- data_store.sync(name)
35
- expect(false).to be(true)
36
- rescue Stoplight::Error::BadDataStore => e
37
- expect(e.cause).to eql(error)
38
- end
133
+ it 'clears the state' do
134
+ state = 'state'
135
+ data_store.set_state(light, state)
136
+ data_store.clear_state(light)
137
+ expect(data_store.get_state(light)).to eql(Stoplight::State::UNLOCKED)
39
138
  end
40
139
  end
41
140
  end