stoplight 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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