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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/LICENSE.md +1 -1
- data/README.md +66 -63
- data/lib/stoplight.rb +10 -15
- data/lib/stoplight/color.rb +9 -0
- data/lib/stoplight/data_store.rb +0 -146
- data/lib/stoplight/data_store/base.rb +7 -130
- data/lib/stoplight/data_store/memory.rb +25 -100
- data/lib/stoplight/data_store/redis.rb +61 -119
- data/lib/stoplight/default.rb +34 -0
- data/lib/stoplight/error.rb +0 -42
- data/lib/stoplight/failure.rb +21 -25
- data/lib/stoplight/light.rb +42 -127
- data/lib/stoplight/light/runnable.rb +97 -0
- data/lib/stoplight/notifier/base.rb +1 -4
- data/lib/stoplight/notifier/hip_chat.rb +17 -32
- data/lib/stoplight/notifier/io.rb +9 -9
- data/lib/stoplight/state.rb +9 -0
- data/spec/spec_helper.rb +2 -3
- data/spec/stoplight/color_spec.rb +39 -0
- data/spec/stoplight/data_store/base_spec.rb +56 -36
- data/spec/stoplight/data_store/memory_spec.rb +120 -2
- data/spec/stoplight/data_store/redis_spec.rb +123 -24
- data/spec/stoplight/data_store_spec.rb +2 -69
- data/spec/stoplight/default_spec.rb +86 -0
- data/spec/stoplight/error_spec.rb +29 -0
- data/spec/stoplight/failure_spec.rb +61 -51
- data/spec/stoplight/light/runnable_spec.rb +234 -0
- data/spec/stoplight/light_spec.rb +143 -191
- data/spec/stoplight/notifier/base_spec.rb +8 -11
- data/spec/stoplight/notifier/hip_chat_spec.rb +66 -55
- data/spec/stoplight/notifier/io_spec.rb +49 -21
- data/spec/stoplight/notifier_spec.rb +3 -0
- data/spec/stoplight/state_spec.rb +39 -0
- data/spec/stoplight_spec.rb +2 -65
- metadata +55 -19
- data/spec/support/data_store.rb +0 -36
- data/spec/support/fakeredis.rb +0 -3
- data/spec/support/hipchat.rb +0 -3
@@ -3,10 +3,7 @@
|
|
3
3
|
module Stoplight
|
4
4
|
module Notifier
|
5
5
|
class Base
|
6
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
13
|
-
|
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 ||
|
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 =
|
25
|
-
|
26
|
-
|
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
|
-
|
7
|
-
|
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 ||
|
13
|
+
@formatter = formatter || Default::FORMATTER
|
15
14
|
end
|
16
15
|
|
17
|
-
def notify(light, from_color, to_color)
|
18
|
-
message =
|
19
|
-
|
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
|
data/spec/spec_helper.rb
CHANGED
@@ -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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
expect { data_store.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
13
|
+
before { Redis::Connection::Memory.reset_all_databases }
|
10
14
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
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 '
|
19
|
-
expect
|
20
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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 '
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|