falcore 0.1.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.
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ module Falcore
4
+ describe Dumper::Statsd, :integration do
5
+ let(:config) do
6
+ Config.parse <<-EOH.gsub(/^ {8}/, '')
7
+ [jenkins]
8
+ endpoint = http://master.jenkins.example.com
9
+
10
+ [statsd]
11
+ host = #{RSpec::Statsd.host}
12
+ port = #{RSpec::Statsd.port}
13
+ EOH
14
+ end
15
+
16
+ let(:aggregator) { Aggregator.new(config) }
17
+ let(:master) { aggregator.run }
18
+
19
+ let(:statsd) { RSpec::Statsd.query("gauges *") }
20
+
21
+ subject { Dumper::Statsd.new(config, master) }
22
+
23
+ it 'validates config.statsd.host is present' do
24
+ config.statsd.host = nil
25
+
26
+ expect {
27
+ subject.run
28
+ }.to raise_error(RuntimeError, "Expected 'Statsd host' to be set!")
29
+ end
30
+
31
+ it 'validates config.statsd.port is present' do
32
+ config.statsd.port = nil
33
+
34
+ expect {
35
+ subject.run
36
+ }.to raise_error(RuntimeError, "Expected 'Statsd port' to be set!")
37
+ end
38
+
39
+ it 'adds the offline status' do
40
+ subject.run
41
+ expect(statsd).to include("'jenkins.master-jenkins-example-com.offline': 0")
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ module Falcore
4
+ describe Fetcher, :integration do
5
+ describe '.get' do
6
+ context 'when given something that is not a URL' do
7
+ it 'raises a RuntimeError' do
8
+ expect {
9
+ Fetcher.get('/path/to/file')
10
+ }.to raise_error(RuntimeError)
11
+ end
12
+ end
13
+
14
+ context 'when given something that is a 404' do
15
+ it 'raises a RuntimeError' do
16
+ expect {
17
+ Fetcher.get('http://jenkins.local/foo/bar')
18
+ }.to raise_error(RuntimeError)
19
+ end
20
+ end
21
+
22
+ context 'when given something that would raise a SocketError' do
23
+ it 'raises a RuntimeError' do
24
+ expect {
25
+ Fetcher.get('http://jenkins.local:5000/foo/bar')
26
+ }.to raise_error(RuntimeError)
27
+ end
28
+ end
29
+
30
+ context 'when given invalid JSON' do
31
+ it 'raises RuntimeError' do
32
+ expect {
33
+ Fetcher.get('http://jenkins.local/bad_json')
34
+ }.to raise_error(RuntimeError)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ require 'bundler/setup'
2
+ require 'rspec'
3
+ require 'webmock/rspec'
4
+
5
+ # Require our library
6
+ require 'falcore'
7
+
8
+ Dir[File.expand_path('../support/**/*.rb', __FILE__)].each { |f| require f }
9
+
10
+ RSpec.configure do |config|
11
+ # Custom helper modules and extensions
12
+
13
+ # Prohibit using the should syntax
14
+ config.expect_with :rspec do |spec|
15
+ spec.syntax = :expect
16
+ end
17
+
18
+ # Allow tests to isolate a specific test using +focus: true+. If nothing
19
+ # is focused, then all tests are executed.
20
+ config.filter_run(focus: true)
21
+ config.run_all_when_everything_filtered = true
22
+ config.treat_symbols_as_metadata_keys_with_true_values = true
23
+
24
+ # Start our fake Jenkins endpoint when integrating
25
+ config.before(:each, :integration) do
26
+ stub_request(:any, /.+/).to_rack(RSpec::JenkinsServer)
27
+ end
28
+
29
+ # Run specs in random order to surface order dependencies. If you find an
30
+ # order dependency and want to debug it, you can fix the order by providing
31
+ # the seed, which is printed after each run.
32
+ # --seed 1234
33
+ config.order = 'random'
34
+ end
@@ -0,0 +1,14 @@
1
+ require 'sinatra/base'
2
+
3
+ module RSpec
4
+ class JenkinsServer < Sinatra::Base
5
+ get('/bad_json') do
6
+ 'This is not JSON, bro...'
7
+ end
8
+
9
+ get('/computer/api/json') do
10
+ content_type 'application/json'
11
+ File.read(File.expand_path('../../fixtures/computer.json', __FILE__))
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,129 @@
1
+ require 'socket'
2
+ require 'timeout'
3
+
4
+ module RSpec
5
+ class Statsd
6
+ class << self
7
+ #
8
+ # @private
9
+ #
10
+ # Delegate everything to the instance
11
+ #
12
+ def method_missing(m, *args, &block)
13
+ if instance.respond_to?(m)
14
+ instance.public_send(m, *args, &block)
15
+ else
16
+ super
17
+ end
18
+ end
19
+ end
20
+
21
+ require 'singleton'
22
+ include Singleton
23
+
24
+ attr_reader :pid
25
+
26
+ def initialize
27
+ clone! unless File.exist?(cache_dir)
28
+ configure!
29
+ start!
30
+ end
31
+
32
+ def start!
33
+ unless @pid
34
+ @pid = Process.spawn("node #{cache_dir}/stats.js #{config}", out: '/dev/null')
35
+ sleep(0.5)
36
+
37
+ at_exit do
38
+ if @pid
39
+ Process.kill('INT', @pid)
40
+ Process.waitpid2(@pid)
41
+ end
42
+ end
43
+ end
44
+
45
+ @pid
46
+ end
47
+
48
+ #
49
+ # Query Statsd for some information and then read the response. This is
50
+ # incredibly brittle and should be the first place you look when debugging
51
+ # tests.
52
+ #
53
+ # @param [String] message
54
+ # the message to send
55
+ #
56
+ # @return [String]
57
+ # the response
58
+ #
59
+ def query(message)
60
+ unless @connection
61
+ @connection = TCPSocket.new(host, admin_port)
62
+ end
63
+
64
+ @connection.send("#{message}\\000", 0)
65
+
66
+ # This is a really super hack way to make sure the socket actually returns
67
+ # some information.
68
+ response = []
69
+ begin
70
+ Timeout.timeout(0.1) do
71
+ while line = @connection.gets
72
+ response << line.strip
73
+ end
74
+ end
75
+ rescue Timeout::Error
76
+ end
77
+
78
+ response.join("\n")
79
+ end
80
+
81
+ def host
82
+ @host ||= '127.0.0.1'
83
+ end
84
+
85
+ def port
86
+ @port ||= random_open_port
87
+ end
88
+
89
+ def admin_port
90
+ @admin_port ||= random_open_port
91
+ end
92
+
93
+ private
94
+
95
+ def random_open_port
96
+ server = TCPServer.new('127.0.0.1', 0)
97
+ port = server.addr[1]
98
+ server.close
99
+
100
+ port
101
+ end
102
+
103
+ def clone!
104
+ %x|git clone https://github.com/etsy/statsd.git #{cache_dir} > /dev/null 2>&1|
105
+ end
106
+
107
+ def configure!
108
+ File.open(config, 'w') do |f|
109
+ f.write <<-EOH.gsub(/^ {10}/, '')
110
+ {
111
+ address: '#{host}',
112
+ port: #{port},
113
+
114
+ mgmt_address: '#{host}',
115
+ mgmt_port: #{admin_port}
116
+ }
117
+ EOH
118
+ end
119
+ end
120
+
121
+ def config
122
+ @config ||= File.join(cache_dir, 'config.js')
123
+ end
124
+
125
+ def cache_dir
126
+ @cache_dir ||= File.expand_path('../../../tmp/statsd', __FILE__)
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ module Falcore
4
+ describe Aggregator do
5
+ let(:endpoint) { 'http://jenkins.local' }
6
+ let(:config) { Config.new(endpoint: endpoint) }
7
+ let(:computer) { JSON.parse(File.read(File.expand_path('../../fixtures/computer.json', __FILE__))) }
8
+
9
+ before do
10
+ Fetcher.stub(:get).and_return(computer)
11
+ end
12
+
13
+ subject { Aggregator.new(config) }
14
+
15
+ describe '#run' do
16
+ let(:master) { subject.run }
17
+
18
+ it 'returns a master jenkins node' do
19
+ expect(master).to be_a(Node::Master)
20
+ end
21
+
22
+ it 'adds the slaves to the master' do
23
+ expect(master.slaves.size).to eq(3)
24
+ end
25
+
26
+ it 'correctly builds each slave object' do
27
+ expect(master.slaves[0].display_name).to eq('slave1.jenkins.example.com')
28
+ expect(master.slaves[1].display_name).to eq('slave2.jenkins.example.com')
29
+ expect(master.slaves[2].display_name).to eq('slave3.jenkins.example.com')
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ module Falcore
4
+ describe Config do
5
+ describe '.from_file' do
6
+ context 'when the file does not exist' do
7
+ before { File.stub(:read).and_raise(Errno::ENOENT) }
8
+
9
+ it 'raises an exception' do
10
+ expect { Config.from_file('/foo/bar') }.to raise_error(RuntimeError)
11
+ end
12
+ end
13
+
14
+ context 'when the file exists' do
15
+ let(:path) { '/config/file' }
16
+ let(:config) { Config.from_file(path) }
17
+ let(:contents) do
18
+ <<-EOH.gsub(/^ {12}/, '')
19
+ [jenkins]
20
+ endpoint = http://localhost
21
+
22
+ [statsd]
23
+ host = http://my.statsd.server
24
+ port = 8125
25
+
26
+ # This section is commented out for testing purposes
27
+ # [nrpe]
28
+ # out = STDOUT
29
+ EOH
30
+ end
31
+
32
+ before { File.stub(:read).with(path).and_return(contents) }
33
+
34
+ it 'returns a config object' do
35
+ expect(config).to be_a(Config)
36
+ end
37
+
38
+ it 'has the properly nested keys' do
39
+ expect(config.jenkins.endpoint).to eq('http://localhost')
40
+ expect(config.statsd.host).to eq('http://my.statsd.server')
41
+ expect(config.statsd.port).to eq(8125)
42
+ end
43
+
44
+ it 'returns a NullObject for ultimate awesomeness' do
45
+ expect(config.foo).to be_nil
46
+ expect(config.foo.bar.zip.zap.billy.bob).to be_nil
47
+ end
48
+
49
+ it 'ignores commented lines' do
50
+ expect(config).to_not have_key('nrpe')
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,329 @@
1
+ require 'spec_helper'
2
+
3
+ module Falcore
4
+ describe Node::Base do
5
+ subject do
6
+ Node::Base.new(
7
+ 'displayName' => 'jenkins.example.com',
8
+ 'idle' => true,
9
+ )
10
+ end
11
+
12
+ describe '#initialize' do
13
+ let(:data) { { 'this' => 'that' } }
14
+
15
+ subject { Node::Base.new(data) }
16
+
17
+ it 'saves the data to @data' do
18
+ expect(subject.instance_variable_get(:@data)).to eq(data)
19
+ end
20
+
21
+ it 'dups the data' do
22
+ expect(data).to receive(:dup).once
23
+ subject
24
+ end
25
+ end
26
+
27
+ its(:id) { should eq('jenkins.jenkins-example-com') }
28
+ its(:display_name) { should eq('jenkins.example.com') }
29
+
30
+ describe '#idle?' do
31
+ context 'when @data.idle is true' do
32
+ subject { Node::Base.new('idle' => true) }
33
+ its(:idle?) { should be_true }
34
+ end
35
+
36
+ context 'when @data.idle is false' do
37
+ subject { Node::Base.new('idle' => false) }
38
+ its(:idle?) { should be_false }
39
+ end
40
+
41
+ context 'when @data.idle is nil' do
42
+ subject { Node::Base.new('idle' => nil) }
43
+ its(:idle?) { should be_false }
44
+ end
45
+ end
46
+
47
+ describe '#executors' do
48
+ context 'when the value is nil' do
49
+ subject { Node::Base.new }
50
+ its(:executors) { should eq(0) }
51
+ end
52
+
53
+ context 'when the value is a string' do
54
+ subject { Node::Base.new('numExecutors' => '5') }
55
+ its(:executors) { should eq(5) }
56
+ end
57
+
58
+ context 'when the value is an integer' do
59
+ subject { Node::Base.new('numExecutors' => 10) }
60
+ its(:executors) { should eq(10) }
61
+ end
62
+ end
63
+
64
+ describe '#offline?' do
65
+ context 'when @data.offline is true' do
66
+ subject { Node::Base.new('offline' => true) }
67
+ its(:offline?) { should be_true }
68
+ end
69
+
70
+ context 'when @data.offline is false' do
71
+ subject { Node::Base.new('offline' => false) }
72
+ its(:offline?) { should be_false }
73
+ end
74
+
75
+ context 'when @data.offline is nil' do
76
+ subject { Node::Base.new('offline' => nil) }
77
+ its(:offline?) { should be_false }
78
+ end
79
+ end
80
+
81
+ describe '#temporarily_offline?' do
82
+ context 'when @data.temporarily_offline is true' do
83
+ subject { Node::Base.new('temporarilyOffline' => true) }
84
+ its(:temporarily_offline?) { should be_true }
85
+ end
86
+
87
+ context 'when @data.temporarily_offline is false' do
88
+ subject { Node::Base.new('temporarilyOffline' => false) }
89
+ its(:temporarily_offline?) { should be_false }
90
+ end
91
+
92
+ context 'when @data.temporarily_offline is nil' do
93
+ subject { Node::Base.new('temporarilyOffline' => nil) }
94
+ its(:temporarily_offline?) { should be_false }
95
+ end
96
+ end
97
+
98
+ describe '#response_time' do
99
+ context 'when the value is nil' do
100
+ subject { Node::Base.new }
101
+ its(:response_time) { should eq(0.0) }
102
+ end
103
+
104
+ context 'when the value is a string' do
105
+ subject do
106
+ Node::Base.new(
107
+ 'monitorData' => {
108
+ 'hudson.node_monitors.ResponseTimeMonitor' => {
109
+ 'average' => '0.332'
110
+ }
111
+ }
112
+ )
113
+ end
114
+ its(:response_time) { should eq(0.332) }
115
+ end
116
+
117
+ context 'when the value is an integer' do
118
+ subject do
119
+ Node::Base.new(
120
+ 'monitorData' => {
121
+ 'hudson.node_monitors.ResponseTimeMonitor' => {
122
+ 'average' => 0.194
123
+ }
124
+ }
125
+ )
126
+ end
127
+ its(:response_time) { should eq(0.194) }
128
+ end
129
+ end
130
+
131
+ describe '#temporary_space' do
132
+ context 'when the value is nil' do
133
+ subject { Node::Base.new }
134
+ its(:temporary_space) { should eq(0) }
135
+ end
136
+
137
+ context 'when the value is a string' do
138
+ subject do
139
+ Node::Base.new(
140
+ 'monitorData' => {
141
+ 'hudson.node_monitors.TemporarySpaceMonitor' => {
142
+ 'size' => '16421466112'
143
+ }
144
+ }
145
+ )
146
+ end
147
+ its(:temporary_space) { should eq(16421466112) }
148
+ end
149
+
150
+ context 'when the value is an integer' do
151
+ subject do
152
+ Node::Base.new(
153
+ 'monitorData' => {
154
+ 'hudson.node_monitors.TemporarySpaceMonitor' => {
155
+ 'size' => 4421461112
156
+ }
157
+ }
158
+ )
159
+ end
160
+ its(:temporary_space) { should eq(4421461112) }
161
+ end
162
+ end
163
+
164
+ describe '#disk_space' do
165
+ context 'when the value is nil' do
166
+ subject { Node::Base.new }
167
+ its(:disk_space) { should eq(0) }
168
+ end
169
+
170
+ context 'when the value is a string' do
171
+ subject do
172
+ Node::Base.new(
173
+ 'monitorData' => {
174
+ 'hudson.node_monitors.DiskSpaceMonitor' => {
175
+ 'size' => '26424266182'
176
+ }
177
+ }
178
+ )
179
+ end
180
+ its(:disk_space) { should eq(26424266182) }
181
+ end
182
+
183
+ context 'when the value is an integer' do
184
+ subject do
185
+ Node::Base.new(
186
+ 'monitorData' => {
187
+ 'hudson.node_monitors.DiskSpaceMonitor' => {
188
+ 'size' => 1490284902
189
+ }
190
+ }
191
+ )
192
+ end
193
+ its(:disk_space) { should eq(1490284902) }
194
+ end
195
+ end
196
+
197
+ describe '#free_memory' do
198
+ context 'when the value is nil' do
199
+ subject { Node::Base.new }
200
+ its(:free_memory) { should eq(0) }
201
+ end
202
+
203
+ context 'when the value is a string' do
204
+ subject do
205
+ Node::Base.new(
206
+ 'monitorData' => {
207
+ 'hudson.node_monitors.SwapSpaceMonitor' => {
208
+ 'availablePhysicalMemory' => '3719038410'
209
+ }
210
+ }
211
+ )
212
+ end
213
+ its(:free_memory) { should eq(3719038410) }
214
+ end
215
+
216
+ context 'when the value is an integer' do
217
+ subject do
218
+ Node::Base.new(
219
+ 'monitorData' => {
220
+ 'hudson.node_monitors.SwapSpaceMonitor' => {
221
+ 'availablePhysicalMemory' => 193042
222
+ }
223
+ }
224
+ )
225
+ end
226
+ its(:free_memory) { should eq(193042) }
227
+ end
228
+ end
229
+
230
+ describe '#total_memory' do
231
+ context 'when the value is nil' do
232
+ subject { Node::Base.new }
233
+ its(:total_memory) { should eq(0) }
234
+ end
235
+
236
+ context 'when the value is a string' do
237
+ subject do
238
+ Node::Base.new(
239
+ 'monitorData' => {
240
+ 'hudson.node_monitors.SwapSpaceMonitor' => {
241
+ 'totalPhysicalMemory' => '28904283'
242
+ }
243
+ }
244
+ )
245
+ end
246
+ its(:total_memory) { should eq(28904283) }
247
+ end
248
+
249
+ context 'when the value is an integer' do
250
+ subject do
251
+ Node::Base.new(
252
+ 'monitorData' => {
253
+ 'hudson.node_monitors.SwapSpaceMonitor' => {
254
+ 'totalPhysicalMemory' => 89048092
255
+ }
256
+ }
257
+ )
258
+ end
259
+ its(:total_memory) { should eq(89048092) }
260
+ end
261
+ end
262
+
263
+ describe '#free_swap' do
264
+ context 'when the value is nil' do
265
+ subject { Node::Base.new }
266
+ its(:free_swap) { should eq(0) }
267
+ end
268
+
269
+ context 'when the value is a string' do
270
+ subject do
271
+ Node::Base.new(
272
+ 'monitorData' => {
273
+ 'hudson.node_monitors.SwapSpaceMonitor' => {
274
+ 'availableSwapSpace' => '319048091'
275
+ }
276
+ }
277
+ )
278
+ end
279
+ its(:free_swap) { should eq(319048091) }
280
+ end
281
+
282
+ context 'when the value is an integer' do
283
+ subject do
284
+ Node::Base.new(
285
+ 'monitorData' => {
286
+ 'hudson.node_monitors.SwapSpaceMonitor' => {
287
+ 'availableSwapSpace' => 83903890
288
+ }
289
+ }
290
+ )
291
+ end
292
+ its(:free_swap) { should eq(83903890) }
293
+ end
294
+ end
295
+
296
+ describe '#total_swap' do
297
+ context 'when the value is nil' do
298
+ subject { Node::Base.new }
299
+ its(:total_swap) { should eq(0) }
300
+ end
301
+
302
+ context 'when the value is a string' do
303
+ subject do
304
+ Node::Base.new(
305
+ 'monitorData' => {
306
+ 'hudson.node_monitors.SwapSpaceMonitor' => {
307
+ 'totalSwapSpace' => '319894031'
308
+ }
309
+ }
310
+ )
311
+ end
312
+ its(:total_swap) { should eq(319894031) }
313
+ end
314
+
315
+ context 'when the value is an integer' do
316
+ subject do
317
+ Node::Base.new(
318
+ 'monitorData' => {
319
+ 'hudson.node_monitors.SwapSpaceMonitor' => {
320
+ 'totalSwapSpace' => 8390183190
321
+ }
322
+ }
323
+ )
324
+ end
325
+ its(:total_swap) { should eq(8390183190) }
326
+ end
327
+ end
328
+ end
329
+ end