whacamole 0.0.1 → 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.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ *.swp
2
+ whacamole*.gem
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - jruby-19mode
7
+ - rbx-19mode
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :name => 'whacamole'
4
+
data/Gemfile.lock ADDED
@@ -0,0 +1,26 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ whacamole (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.2.4)
10
+ rake (10.1.0)
11
+ rspec (2.14.1)
12
+ rspec-core (~> 2.14.0)
13
+ rspec-expectations (~> 2.14.0)
14
+ rspec-mocks (~> 2.14.0)
15
+ rspec-core (2.14.5)
16
+ rspec-expectations (2.14.2)
17
+ diff-lcs (>= 1.1.3, < 2.0)
18
+ rspec-mocks (2.14.3)
19
+
20
+ PLATFORMS
21
+ ruby
22
+
23
+ DEPENDENCIES
24
+ rake (~> 10.1)
25
+ rspec (~> 2.11)
26
+ whacamole!
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ whacamole
2
+ =========
3
+
4
+ [![Build Status](https://travis-ci.org/arches/whacamole.png)](https://travis-ci.org/arches/whacamole)
5
+
6
+ Whacamole keeps track of your Heroku dynos' memory usage and restarts large dynos before they start
7
+ swapping to disk (aka get super slow).
8
+
9
+ Here’s what Heroku says about dyno memory usage:
10
+
11
+ > Dynos are available in 1X or 2X sizes and are allocated 512MB or 1024MB respectively.
12
+ >
13
+ > Dynos whose processes exceed their memory quota are identified by an R14 error in the logs. This doesn’t terminate the process, but it does warn of deteriorating application conditions: memory used above quota will swap out to disk, which substantially degrades dyno performance.
14
+ >
15
+ > If the memory size keeps growing until it reaches three times its quota, the dyno manager will restart your dyno with an R15 error.
16
+ >
17
+ > - From https://devcenter.heroku.com/articles/dynos on 8/8/13
18
+
19
+ Heroku dynos swap to disk for up to 3GB. That is not good and that is the problem whacamole addresses.
20
+
21
+ # Usage
22
+
23
+ Enable log-runtime-metrics on your heroku app:
24
+
25
+ ```bash
26
+ $ heroku labs:enable log-runtime-metrics --app YOUR_APP_NAME
27
+ ```
28
+
29
+ Add whacamole to your gemfile:
30
+
31
+ ```ruby
32
+ gem 'whacamole'
33
+ ```
34
+
35
+ Create a config file with your app info. Personally I put it in my Rails app at config/whacamole.rb. The
36
+ most important parts are your app name and your Heroku api token (which can be found by running `heroku auth:token`
37
+ on the command line).
38
+
39
+ ```ruby
40
+ Whacamole.configure("HEROKU APP NAME") do |config|
41
+ config.api_token = ENV['HEROKU_API_TOKEN'] # you could also paste your token in here as a string
42
+ end
43
+
44
+ # you can monitor multiple apps at once, just add more configure blocks
45
+ Whacamole.configure("ANOTHER HEROKU APP") do |config|
46
+ config.api_token = ENV['HEROKU_API_TOKEN'] # you could also paste your token in here as a string
47
+ end
48
+ ```
49
+
50
+ Add whacamole to your Procfile, specifying the config file you created:
51
+
52
+ ```ruby
53
+ whacamole: bundle exec whacamole -c ./config/whacamole.rb
54
+ ```
55
+
56
+ Start foreman, and you're done!
57
+
58
+ ```bash
59
+ # locally
60
+ $ foreman start whacamole
61
+
62
+ # on heroku
63
+ $ heroku ps:scale whacamole=1 --app YOUR_APP_NAME
64
+ ```
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'rspec/core/rake_task'
13
+
14
+ desc 'Default: run specs.'
15
+ task :default => [:spec]
16
+
17
+ desc "Run specs"
18
+ RSpec::Core::RakeTask.new do |t|
19
+ end
20
+
data/bin/whacamole ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'whacamole'
4
+ $stdout.sync = true
5
+
6
+ if ARGV[0] == "-c"
7
+ require ARGV[1]
8
+
9
+ Whacamole.monitor
10
+ else
11
+ puts "No Whacamole config specified. Use `whacamole -c path/to/config`\nSee http://github.com/arches/whacamole for usage."
12
+ end
13
+
data/lib/whacamole.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'whacamole/config'
2
+ require 'whacamole/events'
3
+ require 'whacamole/heroku_wrapper'
4
+ require 'whacamole/stream'
5
+
6
+ module Whacamole
7
+
8
+ @@config = {}
9
+
10
+ def self.configure(app_name)
11
+ @@config[app_name.to_s] ||= Config.new(app_name)
12
+ yield @@config[app_name.to_s]
13
+ end
14
+
15
+ def self.monitor
16
+ threads = []
17
+ @@config.each do |app_name, config|
18
+ threads << Thread.new do
19
+ heroku = HerokuWrapper.new(app_name, config.api_token)
20
+
21
+ while true
22
+ stream_url = heroku.create_log_session
23
+ Stream.new(stream_url, heroku, &config.event_handler).watch
24
+ end
25
+ end
26
+ end
27
+ threads.collect(&:join)
28
+ end
29
+ end
30
+
@@ -0,0 +1,11 @@
1
+ module Whacamole
2
+ class Config
3
+ attr_accessor :app_name, :api_token, :event_handler
4
+
5
+ def initialize(app_name)
6
+ self.app_name = app_name
7
+ self.event_handler ||= lambda { |e| puts e.inspect.to_s }
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,26 @@
1
+ module Whacamole
2
+ module Events
3
+ class Event
4
+ attr_accessor :process
5
+
6
+ def initialize(attributes={})
7
+ attributes.each do |k,v|
8
+ self.send("#{k}=", v)
9
+ end
10
+ end
11
+ end
12
+
13
+ class DynoSize < Event
14
+ attr_accessor :units
15
+ attr_reader :size
16
+
17
+ def size=(size)
18
+ @size = size.to_f
19
+ end
20
+ end
21
+
22
+ class DynoRestart < Event
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,72 @@
1
+ require 'base64'
2
+ require 'net/http'
3
+ require 'json'
4
+
5
+ module Whacamole
6
+ class HerokuWrapper
7
+ attr_accessor :api_token, :app_name
8
+
9
+ RESTART_RATE_LIMIT = 30*60
10
+
11
+ def initialize(app_name, api_token)
12
+ self.app_name = app_name
13
+ self.api_token = api_token
14
+ end
15
+
16
+ def create_log_session
17
+ uri = URI(log_sessions_url)
18
+ req = Net::HTTP::Post.new(uri.path)
19
+ req['Authorization'] = authorization
20
+ req['Content-type'] = content_type
21
+ req['Accept'] = accept
22
+ req.set_form_data({'tail' => true})
23
+ res = Net::HTTP.start(uri.host, uri.port, :use_ssl => (uri.scheme == "https")) {|http| http.request(req)}
24
+ JSON.parse(res.body)['logplex_url']
25
+ end
26
+
27
+ def authorization
28
+ "Basic " + Base64.encode64(":#{api_token}").gsub("\n", '')
29
+ end
30
+
31
+ def restart(process)
32
+ return false if recently_restarted?(process)
33
+
34
+ uri = URI(dyno_url(process))
35
+ req = Net::HTTP::Delete.new(uri.path)
36
+ req['Authorization'] = authorization
37
+ req['Content-type'] = content_type
38
+ req['Accept'] = accept
39
+ res = Net::HTTP.start(uri.host, uri.port, :use_ssl => (uri.scheme == "https")) {|http| http.request(req)}
40
+
41
+ restarts[process] = Time.now
42
+
43
+ true
44
+ end
45
+
46
+ def recently_restarted?(process)
47
+ restarts[process] > (Time.now - RESTART_RATE_LIMIT)
48
+ end
49
+
50
+ private
51
+ def content_type
52
+ "application/json"
53
+ end
54
+
55
+ def accept
56
+ "application/vnd.heroku+json; version=3"
57
+ end
58
+
59
+ def log_sessions_url
60
+ "https://api.heroku.com/apps/#{app_name}/log-sessions"
61
+ end
62
+
63
+ def dyno_url(process)
64
+ "https://api.heroku.com/apps/#{app_name}/dynos/#{process}"
65
+ end
66
+
67
+ def restarts
68
+ @restarts ||= Hash.new { Time.now - RESTART_RATE_LIMIT*2 }
69
+ end
70
+ end
71
+ end
72
+
@@ -0,0 +1,81 @@
1
+ require 'net/http'
2
+
3
+ module Whacamole
4
+
5
+ class Stream
6
+
7
+ RESTART_THRESHOLD = 1000
8
+
9
+ def initialize(url, restart_handler, &blk)
10
+ @url = url
11
+ @restart_handler = restart_handler
12
+ @event_handler = blk
13
+ end
14
+
15
+ def watch
16
+ uri = URI(url)
17
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
18
+ request = Net::HTTP::Get.new(uri.request_uri)
19
+ http.request(request) do |response|
20
+ response.read_body do |chunk|
21
+ dispatch_handlers(chunk)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ def dispatch_handlers(chunk)
28
+ memory_size_from_chunk(chunk).each do |dyno, size|
29
+ event = Events::DynoSize.new({:process => dyno, :size => size, :units => "MB"})
30
+ event_handler.call(event)
31
+
32
+ restart(event.process) if restart_necessary?(event)
33
+ end
34
+
35
+ # TODO: handle R14 errors here also
36
+ end
37
+
38
+ private
39
+ def restart(process)
40
+ restarted = restart_handler.restart(process)
41
+
42
+ if restarted
43
+ event_handler.call( Events::DynoRestart.new({:process => process}) )
44
+ end
45
+ end
46
+
47
+ def restart_necessary?(event)
48
+ event.size > restart_threshold
49
+ end
50
+
51
+ def memory_size_from_chunk(chunk)
52
+ sizes = []
53
+ chunk.split("\n").select{|line| line.include? "sample#memory_total"}.each do |line|
54
+ dyno = line.match(/web\.\d+/)
55
+ next unless dyno
56
+ size = line.match(/sample#memory_total=([\d\.]+)/)
57
+ sizes << [dyno[0], size[1]]
58
+ end
59
+ sizes
60
+ end
61
+
62
+ def url
63
+ @url
64
+ end
65
+
66
+ def event_handler
67
+ @event_handler
68
+ end
69
+
70
+ def restart_handler
71
+ @restart_handler
72
+ end
73
+
74
+ def restart_threshold
75
+ RESTART_THRESHOLD
76
+ end
77
+ end
78
+ end
79
+
80
+
81
+
@@ -1,4 +1,4 @@
1
1
  module Whacamole
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
4
4
 
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe Whacamole::Config do
4
+ describe "initialization" do
5
+ it "sets the app name" do
6
+ c = Whacamole::Config.new("production")
7
+ c.app_name.should == "production"
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe "DynoSize" do
4
+ let (:e) { Whacamole::Events::DynoSize.new }
5
+
6
+ describe "setting size" do
7
+ it "converts strings to floats" do
8
+ e.size = "766.65"
9
+ e.size.should == 766.65
10
+ end
11
+ end
12
+
13
+ describe "setting units" do
14
+ it "stores the units on the object" do
15
+ e.units = "MB"
16
+ e.units.should == "MB"
17
+ end
18
+ end
19
+
20
+ describe "setting process" do
21
+ it "stores the process on the object" do
22
+ e.process = "web.2"
23
+ e.process.should == "web.2"
24
+ end
25
+ end
26
+
27
+ describe "initialization" do
28
+ it "sets the variables from the input hash" do
29
+ e = Whacamole::Events::DynoSize.new({:size => "766.65", :units => "MB", :process => "web.2"})
30
+ e.size.should == 766.65
31
+ e.units.should == "MB"
32
+ e.process.should == "web.2"
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ # TODO: use VCR instead of these method expectations
4
+ describe Whacamole::HerokuWrapper do
5
+ let(:h) { Whacamole::HerokuWrapper.new("staging", "foobar") }
6
+
7
+ describe "authorization" do
8
+ it "base64-encodes the api token and a preceding colon" do
9
+ h.authorization.should == "Basic OmZvb2Jhcg=="
10
+ end
11
+ end
12
+
13
+ describe "create_log_session" do
14
+ it "executes a request using the default headers" do
15
+ req = {}
16
+ Net::HTTP::Post.should_receive(:new).with("/apps/staging/log-sessions") { req }
17
+ req.should_receive(:[]=).with("Authorization", h.authorization)
18
+ req.should_receive(:[]=).with("Content-type", "application/json")
19
+ req.should_receive(:[]=).with("Accept", "application/vnd.heroku+json; version=3")
20
+ req.should_receive(:set_form_data).with({'tail' => true})
21
+ Net::HTTP.should_receive(:start).with("api.heroku.com", 443, use_ssl: true).and_return(OpenStruct.new(:body => "{\"logplex_url\": \"https://api.heroku.com/log/session/url\"}"))
22
+ h.create_log_session.should == "https://api.heroku.com/log/session/url"
23
+ end
24
+ end
25
+
26
+ describe "restart" do
27
+ it "executes a request using the default headers" do
28
+ req = {}
29
+ Net::HTTP::Delete.should_receive(:new).with("/apps/staging/dynos/web.3") { req }
30
+ req.should_receive(:[]=).with("Authorization", h.authorization)
31
+ req.should_receive(:[]=).with("Content-type", "application/json")
32
+ req.should_receive(:[]=).with("Accept", "application/vnd.heroku+json; version=3")
33
+ Net::HTTP.should_receive(:start).with("api.heroku.com", 443, use_ssl: true)
34
+ h.restart("web.3").should be_true
35
+ end
36
+
37
+ it "respects the rate limit" do
38
+ Net::HTTP::Delete.should_not_receive(:new)
39
+
40
+ h.stub(:recently_restarted? => true)
41
+ h.restart("web.2").should be_false
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,10 @@
1
+ gem 'rspec'
2
+ require 'ostruct'
3
+ require 'whacamole'
4
+
5
+ RSpec.configure do |c|
6
+ c.color = true
7
+ c.filter_run :focus => true
8
+ c.run_all_when_everything_filtered = true
9
+ end
10
+
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ class EventHandler
4
+ attr_accessor :events
5
+
6
+ def process(event)
7
+ events << event
8
+ end
9
+
10
+ def events
11
+ @events ||= []
12
+ end
13
+ end
14
+
15
+ class RestartHandler
16
+ def restart(process)
17
+ true
18
+ end
19
+ end
20
+
21
+ describe Whacamole::Stream do
22
+ let(:eh) { EventHandler.new }
23
+ let(:restart_handler) { RestartHandler.new }
24
+ let(:stream) do
25
+ Whacamole::Stream.new("https://api.heroku.com/path/to/stream/stream", restart_handler) do |event|
26
+ eh.process(event)
27
+ end
28
+ end
29
+
30
+ describe "stream" do
31
+ it "opens the url for streaming" do
32
+ stream.watch
33
+ end
34
+ end
35
+
36
+ describe "handle_chunk" do
37
+ context "when memory usage is present" do
38
+ it "surfaces the memory usage" do
39
+ stream.dispatch_handlers <<-CHUNK
40
+ 2013-08-22T16:39:22.208103+00:00 heroku[router]: at=info method=GET path=/favicon.ico host=aisle50.com fwd="205.159.94.63" dyno=web.3 connect=1ms service=20ms status=200 bytes=894
41
+ 2013-08-22T16:39:22.224847+00:00 heroku[router]: at=info method=GET path=/ host=www.aisle50.com fwd="119.63.193.132" dyno=web.3 connect=1ms service=5ms status=301 bytes=0
42
+ 2013-08-22T16:39:22.919300+00:00 heroku[web.2]: source=web.2 dyno=heroku.772639.a334caa8-736c-48b3-bac2-d366f75d7fa0 sample#load_avg_1m=0.20 sample#load_avg_5m=0.33 sample#load_avg_15m=0.38
43
+ 2013-08-22T16:39:22.919536+00:00 heroku[web.2]: source=web.2 dyno=heroku.772639.a334caa8-736c-48b3-bac2-d366f75d7fa0 sample#memory_total=581.95MB sample#memory_rss=581.75MB sample#memory_cache=0.16MB sample#memory_swap=0.03MB sample#memory_pgpgin=0pages sample#memory_pgpgout=179329pages
44
+ 2013-08-22T16:39:22.919773+00:00 heroku[web.2]: source=web.2 dyno=heroku.772639.a334caa8-736c-48b3-bac2-d366f75d7fa0 sample#diskmbytes=0MB
45
+ 2013-08-22T16:39:23.045250+00:00 heroku[web.1]: source=web.1 dyno=heroku.772639.4c9dcf54-f339-4d81-9756-8dad47f178a4 sample#load_avg_1m=0.24 sample#load_avg_5m=0.59
46
+ 2013-08-22T16:39:23.045521+00:00 heroku[web.90]: source=web.1 dyno=heroku.772639.4c9dcf54-f339-4d81-9756-8dad47f178a4 sample#memory_total=66MB sample#memory_rss=471.21MB sample#memory_cache=0.05MB sample#memory_swap=0.02MB sample#memory_pgpgin=0pages sample#memory_pgpgout=145277pages
47
+ 2013-08-22T16:39:23.045789+00:00 heroku[web.1]: source=web.1 dyno=heroku.772639.4c9dcf54-f339-4d81-9756-8dad47f178a4 sample#diskmbytes=0MB
48
+ 2013-08-22T16:39:23.364649+00:00 heroku[worker.1]: source=worker.1 dyno=heroku.772639.ae391b5d-e776-43f9-b056-360912563d61 sample#load_avg_1m=0.00 sample#load_avg_5m=0.01 sample#load_avg_15m=0.02
49
+ CHUNK
50
+
51
+ eh.events.length.should == 2
52
+
53
+ eh.events.first.should be_a Whacamole::Events::DynoSize
54
+ eh.events.first.size.should == 581.95
55
+ eh.events.first.units.should == "MB"
56
+ eh.events.first.process.should == "web.2"
57
+
58
+ eh.events.last.should be_a Whacamole::Events::DynoSize
59
+ eh.events.last.size.should == 66.0
60
+ eh.events.last.units.should == "MB"
61
+ eh.events.last.process.should == "web.90"
62
+ end
63
+ end
64
+
65
+ context "when memory usages is over the threshold" do
66
+ it "kicks off a restart" do
67
+ restart_handler.should_receive(:restart).with("web.2")
68
+ stream.dispatch_handlers <<-CHUNK
69
+ 2013-08-22T16:39:22.919536+00:00 heroku[web.2]: source=web.2 dyno=heroku.772639.a334caa8-736c-48b3-bac2-d366f75d7fa0 sample#memory_total=1001MB sample#memory_rss=581.75MB sample#memory_cache=0.16MB sample#memory_swap=0.03MB sample#memory_pgpgin=0pages sample#memory_pgpgout=179329pages
70
+ CHUNK
71
+ end
72
+
73
+ it "surfaces the restart" do
74
+ stream.dispatch_handlers <<-CHUNK
75
+ 2013-08-22T16:39:22.919536+00:00 heroku[web.2]: source=web.2 dyno=heroku.772639.a334caa8-736c-48b3-bac2-d366f75d7fa0 sample#memory_total=1001MB sample#memory_rss=581.75MB sample#memory_cache=0.16MB sample#memory_swap=0.03MB sample#memory_pgpgin=0pages sample#memory_pgpgout=179329pages
76
+ CHUNK
77
+
78
+ restart = eh.events.last
79
+ restart.should be_a Whacamole::Events::DynoRestart
80
+ restart.process.should == "web.2"
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe Whacamole do
4
+ describe "configure" do
5
+ it "returns unique config objects by app name" do
6
+ prod_config = nil
7
+ Whacamole.configure("production") do |config|
8
+ prod_config = config
9
+ end
10
+
11
+ staging_config = nil
12
+ Whacamole.configure("staging") do |config|
13
+ staging_config = config
14
+ end
15
+
16
+ staging_config.app_name.should == "staging"
17
+ prod_config.app_name.should == "production"
18
+
19
+ prod_config.should_not == staging_config
20
+ end
21
+
22
+ it "returns the same config object if asked a second time" do
23
+ Whacamole.configure("production") do |config|
24
+ config.api_token = "prod token"
25
+ end
26
+
27
+ Whacamole.configure("production") do |config|
28
+ config.api_token.should == "prod token"
29
+ end
30
+ end
31
+ end
32
+ end
33
+
data/whacamole.gemspec CHANGED
@@ -18,7 +18,8 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_development_dependency 'cucumber', '~> 1.2.1'
22
- gem.add_development_dependency 'rspec', '~> 2.11.0'
23
- gem.add_development_dependency 'rake', '~> 0.9.2'
21
+ gem.executables = ['whacamole']
22
+
23
+ gem.add_development_dependency 'rspec', '~> 2.11'
24
+ gem.add_development_dependency 'rake', '~> 10.1'
24
25
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whacamole
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,24 +9,8 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-08-21 00:00:00.000000000 Z
12
+ date: 2013-08-29 00:00:00.000000000Z
13
13
  dependencies:
14
- - !ruby/object:Gem::Dependency
15
- name: cucumber
16
- requirement: !ruby/object:Gem::Requirement
17
- none: false
18
- requirements:
19
- - - ~>
20
- - !ruby/object:Gem::Version
21
- version: 1.2.1
22
- type: :development
23
- prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ~>
28
- - !ruby/object:Gem::Version
29
- version: 1.2.1
30
14
  - !ruby/object:Gem::Dependency
31
15
  name: rspec
32
16
  requirement: !ruby/object:Gem::Requirement
@@ -34,7 +18,7 @@ dependencies:
34
18
  requirements:
35
19
  - - ~>
36
20
  - !ruby/object:Gem::Version
37
- version: 2.11.0
21
+ version: '2.11'
38
22
  type: :development
39
23
  prerelease: false
40
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -42,7 +26,7 @@ dependencies:
42
26
  requirements:
43
27
  - - ~>
44
28
  - !ruby/object:Gem::Version
45
- version: 2.11.0
29
+ version: '2.11'
46
30
  - !ruby/object:Gem::Dependency
47
31
  name: rake
48
32
  requirement: !ruby/object:Gem::Requirement
@@ -50,7 +34,7 @@ dependencies:
50
34
  requirements:
51
35
  - - ~>
52
36
  - !ruby/object:Gem::Version
53
- version: 0.9.2
37
+ version: '10.1'
54
38
  type: :development
55
39
  prerelease: false
56
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -58,14 +42,33 @@ dependencies:
58
42
  requirements:
59
43
  - - ~>
60
44
  - !ruby/object:Gem::Version
61
- version: 0.9.2
45
+ version: '10.1'
62
46
  description: Whacamole
63
47
  email: archslide@gmail.com
64
- executables: []
48
+ executables:
49
+ - whacamole
65
50
  extensions: []
66
51
  extra_rdoc_files: []
67
52
  files:
53
+ - .gitignore
54
+ - .travis.yml
55
+ - Gemfile
56
+ - Gemfile.lock
57
+ - README.md
58
+ - Rakefile
59
+ - bin/whacamole
60
+ - lib/whacamole.rb
61
+ - lib/whacamole/config.rb
62
+ - lib/whacamole/events.rb
63
+ - lib/whacamole/heroku_wrapper.rb
64
+ - lib/whacamole/stream.rb
68
65
  - lib/whacamole/version.rb
66
+ - spec/config_spec.rb
67
+ - spec/events_spec.rb
68
+ - spec/heroku_wrapper_spec.rb
69
+ - spec/spec_helper.rb
70
+ - spec/stream_spec.rb
71
+ - spec/whacamole_spec.rb
69
72
  - whacamole.gemspec
70
73
  homepage: http://github.com/arches/whacamole
71
74
  licenses:
@@ -92,4 +95,10 @@ rubygems_version: 1.8.19
92
95
  signing_key:
93
96
  specification_version: 3
94
97
  summary: ''
95
- test_files: []
98
+ test_files:
99
+ - spec/config_spec.rb
100
+ - spec/events_spec.rb
101
+ - spec/heroku_wrapper_spec.rb
102
+ - spec/spec_helper.rb
103
+ - spec/stream_spec.rb
104
+ - spec/whacamole_spec.rb