cloudspeq 0.0.15

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 54e96a5a8ca4132e21346042feffd9eb8682ea48
4
+ data.tar.gz: d64a3fcea08a178fb9ff76f162d347c8b6cdcc67
5
+ SHA512:
6
+ metadata.gz: 6612dbce34c281ff7cd1737b9f4e0d66723e52aff60acb0ca91f0a35c43825fbd1db4a842811f7d7de41b527efccc6c5cea5bb05f0e903a5541de928947d1eff
7
+ data.tar.gz: 1be896879bb52b6361ddeadd237cbd260e25c38cebaec59771f1d550897fad11c8618354a962a430092d46e10498ac8603a14ed808675dd075b7ec827849d278
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cloudspeq.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Russell Jennings
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # Cloudspeq (Beta)
2
+
3
+ Cloudspeq distributes your specs across machines in the cloud to dramatically reduce the time it takes to test.
4
+
5
+ ## Introduction
6
+
7
+ Fast specs are ideal in development, but are often not possible for an app due to cost or effort required, or simply because there are a lot of slow things to test. What is a developer to do, sit around while the tests run? Some test suits can take half an hour!
8
+
9
+ To address this, cloudspeq throws computing power at it.
10
+
11
+ ### Benefits
12
+
13
+ - Plug and play: works with any rails codebase without app configuration
14
+ - Fast: Can make a 5 minute test suit run in 20 seconds
15
+ - Scalable: Can work with 10 machines, or 100.
16
+ - Controlable: You control what kind of machines to use, how many, and for how long. You also control which provider to use (so long as its digital ocean)
17
+ - Load Balanced: specs are parsed for definitions and randomly distributed across machines to reduce testing hotspots
18
+ - Princesses: Some directories / files / specs need special attention. Set aside machines to focus specifically on them
19
+ - Safe: Ignores other machines on the provider that do not relate to testing
20
+ - More Safe: Machines can self-destruct to ensure you dont get charged for machines you're not using
21
+
22
+ Right now, only Digital Ocean is supported as a provider, but others providers are possible in the future.
23
+
24
+ Written to work with Rspec, but other test frameworks should more or less work too.
25
+
26
+ ### Cloudspeq Vs. CI
27
+
28
+ Cloudspeq was built to make testing in dev faster by working with the code you haven't commited to your repositiory. This is different from the role of CI, which is to test commited code, and where the test time is not as noticeable. This isn't to say Cloudspeq can't serve a CI role, only that it was not an orignal design intention.
29
+
30
+ ## Installation
31
+
32
+ Add this line to your application's Gemfile:
33
+
34
+ ```ruby
35
+ gem 'cloudspeq'
36
+ ```
37
+
38
+ And then execute:
39
+
40
+ $ bundle
41
+
42
+ Or install it yourself as:
43
+
44
+ $ gem install cloudspeq
45
+
46
+
47
+ To install the require config file, execute:
48
+
49
+ ```
50
+ cloudspeq install
51
+ ```
52
+
53
+ The `cloudspeq.yml` file contains all the defaults that cloudspeq uses. You'll need to fill in the neccesary provider information. Everything but the provider info can be removed if you want to rely on the defaults; or you can customize them to suit your needs.
54
+
55
+ ## Preperation
56
+
57
+ You'll need a machine that can run your tests. This can be achieved by either:
58
+
59
+ 1. Creating a machine image
60
+ 2. Preparing the machine after boot
61
+
62
+ It is highly recommend you create a machine image to reduce the time it takes to prepare for testing.
63
+
64
+ Things you'll need to do:
65
+ 1. Ensure user & project directory exist as defined in `cloudspeq.yml`
66
+ 2. Sync local directory to remote project directory (you can run `cloudspeq machines sync`). configure `sync_excludes` in the config file to skip any directories not needed for test; it already skips `.git/` and `tmp/`.
67
+ 3. Ensure test suite runs on remote machine
68
+ 4. Install ssh keys for root & user on remote machine
69
+ 5. Optionally, you can have the machines self-destruct after a certain ammount of uptime. This is a great way to ensure machines don't accidently linger for longer than they need to, in case you forget to delete the machines after testing. It also means if you want more time, you can just reboot. For this to work it requires cloudspeq be installed on the machine, and that it have access to the `cloudspeq.yml` file. Then you just need to define a cron entry for it. For example:
70
+
71
+ ```
72
+ */2 * * * * cd /home/tester/project/; /usr/local/bin/cloudspeq self_destruct
73
+ ```
74
+
75
+ Adjust to match your enviroment, and be sure to test it works in cron before relying on it. By default, the `server_lifetime` is 90 (minutes), but you can change this in the `cloudspeq.yml` file, or specify the number of minutes by passing it as a parameter to the `self_destruct` command.
76
+ 6. Shutdown the machine, and create a snapshot
77
+ 7. In `cloudspeq.yml`, specify `image_name` using the name of the snapshot you just created under the provider
78
+
79
+
80
+
81
+ ## Usage
82
+
83
+
84
+ ### Create some machines to test with
85
+
86
+ ```
87
+ cloudspeq machines create
88
+ ```
89
+
90
+ This creates the number of machines specified in the provder machine_count. You can run this command multiple times to create machines in multiples, or provide the number to create as a parameter to the command.
91
+
92
+
93
+ ### Check the status of the machines
94
+
95
+ Creating machines can take a few minutes, especially if you create a lot. Run the status command to see how things are coming along
96
+
97
+ ```
98
+ cloudspeq machines status
99
+ ```
100
+
101
+ ### Prepare SSH (recommended)
102
+
103
+ SSH-ing into the machines modifies `~/.ssh/known_hosts` but these entries can lead to warnings later if we create another machine with the same IP and a different public_key. So, this will backup the file for later restoration.
104
+
105
+ ```
106
+ cloudspeq ssh backup
107
+ ```
108
+
109
+ ### Sync
110
+
111
+ Sync the project files to the machines
112
+
113
+ ```
114
+ cloudspeq sync
115
+ ```
116
+
117
+ ### Prepare for testing
118
+
119
+ Executes all `remote_prepare` commands defined in the config file. Useful for starting up services, migrations, or what have you.
120
+
121
+ ```
122
+ cloudspeq machines prepare
123
+ ```
124
+
125
+ ### Run tests
126
+
127
+ Run the tests and get a report of the results.
128
+
129
+ ```
130
+ cloudspeq run
131
+ ```
132
+
133
+ ### Destroy
134
+
135
+ After testing, destroy the machines
136
+
137
+ ```
138
+ cloudspeq machines destroy
139
+ ```
140
+
141
+ This destroys the same number of machines defined in `machine_count`. You can also pass this command a number to specify how many machines to destroy.
142
+
143
+ The above only pulls from the local machines file, and might not destroy all the machines. To destroy all machines on the provider that are test related:
144
+
145
+ ```
146
+ cloudspeq machines destroy_all
147
+ ```
148
+
149
+ Machines not related to testing, such as production machines, are ignored.
150
+
151
+ ### Restore SSH
152
+
153
+ ```
154
+ cloudspeq ssh restore
155
+ ```
156
+
157
+ ### Additional Usage
158
+
159
+ - You can use the `machines execute` and `machines root_execute` to execute commands across the machines
160
+ - To run commands locally for prepare: `cloudspeq prepare` and for cleanup: `cloudspeq clean_up`
161
+
162
+ Most commands have a short-hand that is a few letters long. See `cloudspeq -h` for more info, or `cloudspeq command -h` for specific command related help.
163
+
164
+ ## Optimizations
165
+
166
+ While cloudspeq can dramtically speed things up out of the box, with some tuning you can get things running even faster.
167
+
168
+ ### Princesses
169
+
170
+ While Cloudspeq load-balancing can reduce hotspots, Some specs need to be pampered to get the most out of them. These can be directories, files, or specific specs.
171
+
172
+ By defining a princess, you can dedicate machines to focus on something in particular, in order to reduce the overall time it takes for the test suite to execute. it can take some tinkering to identify an ideal configuation for your particular app.
173
+
174
+ By default, all specs are distributed and run under a default princess "misc", where misc has a machine pool equal to the number of machines defined. When you define a princess, machines are set aside from those available, and the specs that are sent to them are removed from misc. The 'misc' group is the catch-all, and you'll get an error if there is not at least 1 machine available for it.
175
+
176
+ The order matters, so its best to be specific at the top and general at the bottom.
177
+
178
+ an example princesses definition looks like:
179
+
180
+ ```
181
+ princesses:
182
+ "controllers/search_controller_spec.rb": {servers: 2, symbol: 'G'}
183
+ "controllers/store": {servers: 4, symbol: "S", threads: 2}
184
+ "acceptance": {servers: 2, symbol: 'A', per: 1}
185
+ "models": {servers: 3, symbol: 'M', load_balance: false}
186
+ ```
187
+
188
+ Each princess consists of:
189
+
190
+ 1. a directory, file, or line number to test; as the key
191
+ 2. a hash value containing
192
+ 1. `servers` is to specify the number of machines to dedicate
193
+ 2. `symbol` is the symbol to use in output when representing (optional - defaults to '.')
194
+ 3. `load_balance` is to control if spec files should be broken up or fed in whole; useful if a spec file has expensive setup, but otherwise load_balancing is faster.
195
+ 4. `threads` controls the number of SSH connections to make to each machine, by default just 1. Useful if your tests can run in paralell without interfering with each other in the database, and you want to drive the machine harder.
196
+ 5. `per` defines how many specs each thread should receive. by default it is number of specs / number of threads, but if you specify a lower number it will cause machines to come back after finishing and be available for additional specs to work on, instead of sitting idle after they've finished.
197
+
198
+ If you define a 'misc' princess (with 'misc' as the key) as the last definition, you can adjust these parameters as they apply to any specs that fall in the misc group. This is also useful if you want to experiment with how long the specs take to run with a given number of machines.
199
+
200
+ ### Spec Tuning
201
+
202
+ To optimize your tests to run with cloudspeq, they should be fast. Load-balancing and Princesses can help a lot, but in the end you'll only be able to be as fast as your slowest spec. If you have a spec that checks 6 different things and takes 6 seconds to run, you can optimize it by breaking it up into different specs, so that load-balancing can distribute the load across multiple machines.
203
+
204
+ ## Roadmap
205
+
206
+ Cloudspeq is just getting started! Coming eventually:
207
+
208
+ - More providers (EC2, linode, raspberry-pi)
209
+ - Test coverage
210
+ - Better test profiling display
211
+ - Machine profiling
212
+ - Machine profiling test correlation
213
+ - Automatic tuning & optimizing
214
+ - Framework Atheism
215
+ - Distributed Rails Load Testing
216
+
217
+ Want to help realize one of these ideas, or have an idea of your own? Submit a PR!
218
+
219
+ ## Contributing
220
+
221
+ 1. Fork it ( https://github.com/meesterdude/cloudspeq/fork )
222
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
223
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
224
+ 4. Push to the branch (`git push origin my-new-feature`)
225
+ 5. Create a new Pull Request
226
+
227
+ ## License
228
+
229
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/cloudspeq ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'escort'
4
+ require 'cloudspeq'
5
+
6
+ Escort::App.create do |app|
7
+ app.summary "Run your rails tests in the cloud for faster exectuion"
8
+
9
+
10
+ app.options do |opts|
11
+ opts.opt :provider, "provider to use", :short => '-p', :long => '--provider', :type => :string, default: 'digitalocean'
12
+ opts.opt :cfg, "config file path", :short => '-c', :long => '--config', :type => :string, default: 'cloudspeq.yml'
13
+ end
14
+
15
+
16
+ app.command :install, aliases: [:i] do |command|
17
+ command.summary "Install config file"
18
+ command.action do |options, arguments|
19
+ Cloudspeq.install
20
+ end
21
+
22
+ end
23
+
24
+
25
+ app.command :machines, aliases: [:m] do |command|
26
+ command.summary "Wrapper for commands to create and manage machines"
27
+
28
+
29
+ command.command :create, aliases: [:c] do |cmd|
30
+ cmd.summary "create machines to use for testing"
31
+ cmd.description "Optionally takes a number of machines to create"
32
+ cmd.action do |options, arguments|
33
+ c = Cloudspeq.new(options[:global][:options][:cfg])
34
+ !arguments.empty? ? c.spool_up(arguments.first) : c.spool_up
35
+ end
36
+ end
37
+
38
+ command.command :status, aliases: [:s] do |cmd|
39
+ cmd.summary "statuses of machines"
40
+ cmd.action do |options, arguments|
41
+ c = Cloudspeq.new(options[:global][:options][:cfg])
42
+ pp c.status
43
+ end
44
+ end
45
+
46
+ command.command :destroy, aliases: [:d] do |cmd|
47
+ cmd.summary "destroy machines used for testing defined in local file"
48
+ cmd.description "Optionally takes a number of machines to destroy"
49
+ cmd.action do |options, arguments|
50
+ c = Cloudspeq.new(options[:global][:options][:cfg])
51
+ !arguments.empty? ? c.spool_down(arguments.first) : c.spool_down
52
+ end
53
+ end
54
+
55
+ command.command :destroy_all, aliases: [:da] do |cmd|
56
+ cmd.summary "destroy all machines on remote that are used for testing"
57
+ cmd.action do |options, arguments|
58
+ c = Cloudspeq.new(options[:global][:options][:cfg])
59
+ c.spool_down_all
60
+ end
61
+ end
62
+
63
+ command.command :prepare, aliases: [:p] do |cmd|
64
+ cmd.summary "Run remote_prepare commands defined in config file across remote machines"
65
+ cmd.action do |options, arguments|
66
+ c = Cloudspeq.new(options[:global][:options][:cfg])
67
+ c.remote_prepare
68
+ end
69
+ end
70
+
71
+ command.command :clean_up, aliases: [:cu] do |cmd|
72
+ cmd.summary "Run remote_clean_up commands defined in config file across remote machines"
73
+ cmd.action do |options, arguments|
74
+ c = Cloudspeq.new(options[:global][:options][:cfg])
75
+ pp c.remote_clean_up
76
+ end
77
+ end
78
+
79
+ command.command :sync, aliases: [:sy] do |cmd|
80
+ cmd.summary "sync current directory with machines"
81
+ cmd.action do |options, arguments|
82
+ c = Cloudspeq.new(options[:global][:options][:cfg])
83
+ c.sync
84
+ end
85
+ end
86
+
87
+ command.command :execute, aliases: [:x] do |cmd|
88
+ cmd.summary "Execute a command across all machines"
89
+ cmd.description "Takes an argument as a command to execute"
90
+ cmd.action do |options, arguments|
91
+ c = Cloudspeq.new(options[:global][:options][:cfg])
92
+ result = c.execute arguments.first
93
+ result.each do |k,v|
94
+ puts "\n#{k}: #{v}"
95
+ end
96
+ end
97
+ end
98
+
99
+ command.command :root_execute, aliases: [:rx] do |cmd|
100
+ cmd.summary "Execute a command across all machines as root"
101
+ cmd.description "Takes an argument as a command to execute"
102
+ cmd.action do |options, arguments|
103
+ c = Cloudspeq.new(options[:global][:options][:cfg])
104
+ result = c.root_execute arguments.first
105
+ result.each do |k,v|
106
+ puts "\n#{k}: #{v}"
107
+ end
108
+ end
109
+ end
110
+
111
+ end
112
+
113
+ app.command :ssh do |command|
114
+ command.summary "Wrapper for commands to backup, restore and reset the ssh known_hosts file"
115
+ command.command :backup, aliases: [:bu] do |cmd|
116
+ cmd.summary "backup the known_hosts file"
117
+ cmd.action do |options, arguments|
118
+ Cloudspeq.backup_ssh
119
+ end
120
+ end
121
+ command.command :restore, aliases: [:rso] do |cmd|
122
+ cmd.summary "restore the known_hosts file, removing backup"
123
+ cmd.action do |options, arguments|
124
+ Cloudspeq.restore_ssh
125
+ end
126
+ end
127
+ command.command :reset, aliases: [:rse] do |cmd|
128
+ cmd.summary "restore the known_hosts file, keeping backup"
129
+ cmd.action do |options, arguments|
130
+ Cloudspeq.reset_ssh
131
+ end
132
+ end
133
+ end
134
+
135
+ app.command :self_destruct do |command|
136
+ command.summary "run on the server to destroy after X uptime in minutes (default 90 minutes)"
137
+ command.description "Optional argument is minutes of uptime to destroy after, uses server_lifetime config option otherwise"
138
+ command.action do |options, arguments|
139
+ c = Cloudspeq.new(options[:global][:options][:cfg])
140
+ minutes = !arguments.empty? ? arguments.first.to_f : c.settings['server_lifetime'].to_f
141
+ uptime = `cat /proc/uptime`.split(" ").first.to_f / 60
142
+ if uptime > minutes
143
+ name = `hostname`.strip
144
+ puts "*** Destroying this machine"
145
+ machine = c.provider.machines.find{|m| m.name == name}
146
+ machine.destroy if machine
147
+ else
148
+ puts "not ready for self destruct"
149
+ end
150
+ end
151
+ end
152
+
153
+ app.command :prepare, aliases: [:p] do |command|
154
+ command.summary "Locally run commands defined in local_prepare"
155
+ command.action do |options, arguments|
156
+ c = Cloudspeq.new(options[:global][:options][:cfg])
157
+ c.local_prepare
158
+ end
159
+ end
160
+
161
+ app.command :clean_up, aliases: [:cu] do |command|
162
+ command.summary "Locally run commands defined in local_clean_up"
163
+ command.action do |options, arguments|
164
+ c = Cloudspeq.new(options[:global][:options][:cfg])
165
+ c.local_clean_up
166
+ end
167
+ end
168
+
169
+ app.command :run, aliases: [:r] do |command|
170
+ command.summary "Distribute the tests and display the results"
171
+ command.action do |options, arguments|
172
+ c = Cloudspeq.new(options[:global][:options][:cfg])
173
+ Cloudspeq::RspecOutputter.perform c.run_tests
174
+ end
175
+ end
176
+
177
+ end
data/cloudspeq.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cloudspeq/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cloudspeq"
8
+ spec.version = Cloudspeq::VERSION
9
+ spec.authors = ["Russell Jennings"]
10
+ spec.email = ["violentpurr@gmail.com"]
11
+ spec.summary = %q{Distribute your tests in the cloud for faster development in slow test suits}
12
+ spec.description = %q{Having a slow test suite sucks. But don't let a slow test suite slow you down! with cloudspeq you can distribute your tests and dramatically reduce the time it takes to test.}
13
+ spec.homepage = "https://github.com/meesterdude/cloudspeq"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'escort', "~> 0.4"
22
+ spec.add_dependency 'digitalocean', "~> 1.2"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.7"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "pry", "~> 0.10"
27
+ spec.executables << 'cloudspeq'
28
+ end
data/lib/cloudspeq.rb ADDED
@@ -0,0 +1,122 @@
1
+ require 'ostruct'
2
+ require 'yaml'
3
+ require 'benchmark'
4
+ require 'digitalocean'
5
+ require_relative "cloudspeq/version"
6
+ require_relative "cloudspeq/providers/base"
7
+ require_relative "cloudspeq/providers/digital_ocean"
8
+ require_relative "cloudspeq/defaults"
9
+ require_relative "cloudspeq/distributed_testing"
10
+ require_relative "cloudspeq/rspec_outputter"
11
+
12
+ class Cloudspeq
13
+ # this is the public API.
14
+
15
+ def initialize(settings_path= 'cloudspeq.yml')
16
+ @settings = Cloudspeq.settings(settings_path)
17
+ provider_key = @settings['provider_key']
18
+ provider = Object.const_get "Cloudspeq::Providers::" + provider_key.to_s.split("_").collect(&:capitalize).join
19
+
20
+ @provider = provider.new(@settings)
21
+ end
22
+
23
+ def self.install
24
+ File.open('cloudspeq.yml',"w") do |f|
25
+ f.write(DEFAULT_SETTINGS.to_yaml)
26
+ end
27
+ end
28
+
29
+ def self.settings(path=nil)
30
+ @settings ||= OpenStruct.new DEFAULT_SETTINGS.merge( YAML.load_file(path) )
31
+ end
32
+
33
+ def settings
34
+ @settings
35
+ end
36
+
37
+ def status
38
+ refresh
39
+ provider.status
40
+ end
41
+
42
+ def provider
43
+ @provider
44
+ end
45
+
46
+ def spool_up(n=provider.provider_settings['machine_count'])
47
+ provider.create n
48
+ end
49
+
50
+ def remote_prepare
51
+ output = []
52
+ @settings['remote_prepare'].each do |command|
53
+ output << provider.exec(command)
54
+ end
55
+ output
56
+ end
57
+
58
+ def remote_clean_up
59
+ @settings['remote_cleanup'].each do |command|
60
+ provider.exec(command)
61
+ end
62
+ end
63
+
64
+ def local_prepare
65
+ output = []
66
+ @settings['local_prepare'].each do |command|
67
+ `#{command}`
68
+ end
69
+ end
70
+
71
+ def local_clean_up
72
+ @settings['local_clean_up'].each do |command|
73
+ `#{command}`
74
+ end
75
+ end
76
+
77
+ def spool_down(n=provider.provider_settings['machine_count'])
78
+ provider.destroy provider.machines.first(n)
79
+ end
80
+
81
+ def spool_down_all
82
+ provider.spool_down_all
83
+ end
84
+
85
+ def refresh
86
+ provider.refresh
87
+ end
88
+
89
+ def sync
90
+ provider.sync
91
+ end
92
+
93
+ def execute(cmd)
94
+ provider.exec cmd
95
+ end
96
+
97
+ def root_execute(cmd)
98
+ provider.root_exec cmd
99
+ end
100
+
101
+ # copies current ssh known_hosts to a backup
102
+ def self.backup_ssh
103
+ `cp ~/.ssh/known_hosts ~/.ssh/known_hosts.backup`
104
+ end
105
+ # restores known_hosts file, destroying backup
106
+ def self.restore_ssh
107
+ `mv ~/.ssh/known_hosts.backup ~/.ssh/known_hosts`
108
+ end
109
+ # restores known_hosts file, keeping backup
110
+ def self.reset_ssh
111
+ `cp ~/.ssh/known_hosts.backup ~/.ssh/known_hosts`
112
+ end
113
+
114
+ def run_tests(n=1) # run 1 time, by default
115
+ output = []
116
+ n.times do
117
+ output << Cloudspeq::DistributedTesting.perform(@settings, provider.machines)
118
+ end
119
+ n == 1 ? output.first : output
120
+ end
121
+
122
+ end
@@ -0,0 +1,30 @@
1
+ DEFAULT_SETTINGS =
2
+ {'provider_key'=>:digital_ocean,
3
+ 'spec_path'=>"spec",
4
+ 'user'=>"tester",
5
+ 'project_name'=>"myapp",
6
+ 'digital_ocean'=>
7
+ {"client_id"=>"xxx",
8
+ "api_key"=>"xxx",
9
+ "image_name"=>"xxx",
10
+ "ssh_key_name"=>"xxx",
11
+ "size_slug"=>"512mb",
12
+ "region_slug"=>"nyc2",
13
+ "machine_count"=>5,
14
+ },
15
+ 'machine_prefix'=>"test",
16
+ 'server_threads' => 1,
17
+ 'load_balance' => true,
18
+ 'server_lifetime' => 90,
19
+ 'file_pattern' => '/_spec.rb(:\d+)?\z/',
20
+ 'spec_line_pattern' => '/^(\s)+it|scenario/',
21
+ 'machine_file'=>"cloudspeq_machines.yml",
22
+ 'remote_command_prefix'=>"source .profile;",
23
+ 'remote_project_directory'=>"/home/tester/project",
24
+ 'remote_prepare'=>["bundle exec bin/rspec spec/spec_helper.rb"],
25
+ 'remote_clean_up'=>[],
26
+ 'local_clean_up' => [],
27
+ 'local_prepare' => [],
28
+ 'sync_excludes'=>["doc"]
29
+ }
30
+
@@ -0,0 +1,128 @@
1
+ require 'find'
2
+
3
+ class Cloudspeq
4
+ class DistributedTesting
5
+
6
+
7
+ def self.perform(settings,machines)
8
+ @settings = settings
9
+ @machines = machines.shuffle
10
+ @threads = []
11
+ @outputs = []
12
+ @proccessed = []
13
+ @code_returns = []
14
+ time = Benchmark.measure do
15
+ test_princesses
16
+ test_remaining
17
+ @threads.each(&:join)
18
+ end
19
+
20
+ {'time' => time.real, 'outputs' => @outputs}
21
+ end
22
+
23
+ private
24
+
25
+
26
+ def self.test_princesses
27
+ if @settings.princesses
28
+ specified_servers = @settings.princesses.reject{|k,v| k == 'misc'}
29
+ if specified_servers.empty?
30
+ specified_servers = @machines.count
31
+ else
32
+ specified_servers = specified_servers.collect{|k,v| v['servers']}.inject(:+) + 1
33
+ end
34
+ if specified_servers > @machines.count
35
+ puts "ERROR: not enough servers. #{@machines.count} available, but #{specified_servers} needed"
36
+ return false
37
+ end
38
+ @settings.princesses.each do |k,v|
39
+ next if k == 'misc'
40
+ specs = parse_specs(k,v) - @proccessed
41
+ @proccessed.concat specs
42
+ issue_specs(specs.shuffle,v)
43
+ end
44
+ end
45
+ end
46
+
47
+ def self.test_remaining
48
+ remaining = parse_specs - @proccessed
49
+ if @settings.princesses && @settings.princesses['misc']
50
+ options = {'servers' => @machines.count, 'load_balance' => false, 'symbol' => '.'}.merge @settings.princesses['misc']
51
+ else
52
+ options = {'servers' => @machines.count, 'load_balance' => false}
53
+ end
54
+ issue_specs(remaining.shuffle, options)
55
+ end
56
+
57
+
58
+ # takes either a "valid" spec file, or directory.
59
+ def self.parse_specs(path="",v={})
60
+ file_lines = []
61
+ if path.match(@settings.file_pattern)
62
+ proccess_lines(path,v)
63
+ else
64
+ Find.find("#{@settings.spec_path}/#{path}") do |file|
65
+ lines = proccess_lines(file,v)
66
+ next if lines.nil?
67
+ file_lines.concat lines
68
+ end
69
+ file_lines
70
+ end
71
+ end
72
+
73
+ def self.proccess_lines(file, v)
74
+ if v['load_balance'] && (v['load_balance'] == false)
75
+ return [file]
76
+ elsif @settings.load_balance == false
77
+ return [file]
78
+ end
79
+ if file.match(eval @settings.file_pattern)
80
+ lines = `awk '#{@settings.spec_line_pattern}{print NR}' #{file}`.split("\n")
81
+ return [] if lines.empty?
82
+ lines.collect do |line|
83
+ "#{file}:#{line}"
84
+ end
85
+ end
86
+ end
87
+
88
+ def self.issue_specs(specs,v={'servers'=> 1})
89
+ threads = v['threads'].nil? ? @settings.server_threads : v['threads']
90
+ servers = v['servers'].nil? ? @machines.count : v['servers']
91
+ per = v['per'].nil? ? specs.count / [v['servers'].to_i,1].max / threads : v['per']
92
+ per = per.ceil
93
+ puts "#{v['symbol'] || '.'} - #{specs.count} specs on #{servers} servers with #{per} specs per connection and #{threads} connections per server"
94
+ servers.times do
95
+ machine = @machines.pop # take a machine from the pool
96
+ threads.times do
97
+ @threads << create_thread(machine,specs,per,v)
98
+ end
99
+ end
100
+ end
101
+
102
+ def self.create_thread(machine,specs,per,v)
103
+ Thread.new do
104
+ files = []
105
+ while !specs.empty?
106
+ spec_lines = []
107
+ per.times{spec_lines << specs.pop}
108
+ output = nil
109
+ time = Benchmark.measure do
110
+ output = machine.exec("bundle exec bin/rspec -f j #{spec_lines.join(" ")}", @settings)
111
+ end
112
+ host_report = {"output" => (JSON.parse(output) rescue output),
113
+ "hostname" => machine.name,
114
+ "ip_address" => machine.ip_address,
115
+ "time" => time.real,
116
+ "specs" => spec_lines,
117
+ "symbol" => v['symbol']
118
+ }
119
+ @outputs << host_report
120
+ print type_return = v['symbol'] || '.'
121
+ @code_returns << type_return
122
+ end
123
+ end
124
+ end
125
+
126
+
127
+ end
128
+ end
@@ -0,0 +1,151 @@
1
+ class Cloudspeq
2
+ class Providers
3
+ class Base
4
+
5
+ def initialize(settings)
6
+ @settings = settings
7
+ end
8
+
9
+ def refresh
10
+ @machines= remote_machines
11
+ write_machines @machines
12
+ end
13
+
14
+ def machines
15
+ end
16
+
17
+
18
+ def sync
19
+ machines.each do |machine|
20
+ machine.sync(@settings)
21
+ end
22
+ end
23
+
24
+ def sync
25
+ threads = []
26
+ output = {}
27
+ machines.each do |m|
28
+ threads << Thread.new{ output[m.name] = m.sync(@settings) }
29
+ end
30
+ threads.each(&:join)
31
+ output
32
+ end
33
+
34
+ # destroys machines and removes
35
+ def destroy(machs)
36
+ machs.each do |m|
37
+ machines.delete m
38
+ m.destroy
39
+ end
40
+ end
41
+
42
+ def exec(command,machs=machines)
43
+ threads = []
44
+ output = {}
45
+ machs.each do |m|
46
+ threads << Thread.new{ output[m.name] = m.exec(command,@settings) }
47
+ end
48
+ threads.each(&:join)
49
+ output
50
+ end
51
+
52
+ def exec!(command,machs=machines)
53
+ threads = []
54
+ machs.collect{|m| threads << Thread.new{m.exec! command, @settings}}
55
+ threads.each(&:join)
56
+ end
57
+
58
+ def root_exec(command,machs=machines)
59
+ threads = []
60
+ machs.collect{|m| threads << Thread.new{m.root_exec command}}
61
+ threads.each(&:join)
62
+ end
63
+
64
+
65
+ def self.find(hostname: '', ip_address: '')
66
+ return false if hostname.empty? && ip_address.empty?
67
+ if !ip_address.empty?
68
+ else
69
+ end
70
+ end
71
+
72
+
73
+
74
+ private
75
+
76
+ def generate_machine_name(settings=@settings)
77
+ characters = ("a".."z").to_a + ('0'..'9').to_a
78
+ prefix = settings.machine_prefix ? "#{settings.machine_prefix}-" : "test-"
79
+ name = prefix + settings.project_name + "-" + characters.sample(4).join
80
+ end
81
+
82
+ end
83
+ end
84
+ end
85
+
86
+
87
+
88
+ class Cloudspeq
89
+ class Providers
90
+ class Base
91
+ class Machine
92
+
93
+ def initialize(machine)
94
+ @attributes = RecursiveOpenStruct.new(machine)
95
+ end
96
+
97
+ def attributes
98
+ @attributes
99
+ end
100
+
101
+ def shutdown
102
+ end
103
+
104
+ def restart
105
+ end
106
+
107
+ def startup
108
+ end
109
+
110
+ def destroy
111
+ end
112
+
113
+ # most of the time, execution requires prefixing
114
+ def exec(command,settings)
115
+ user = settings.user
116
+ prefix = settings.command_prefix
117
+ command_exec(user,attributes.ip_address,prefix,command)
118
+ end
119
+
120
+ # execute without prefixing
121
+ def exec!(command,settings)
122
+ user = settings.user
123
+ prefix = settings.command_prefix
124
+ command_exec(user,attributes.ip_address,"",command)
125
+ end
126
+
127
+ # exec as root
128
+ def root_exec(command)
129
+ command_exec('root',attributes.ip_address,"",command)
130
+ end
131
+
132
+ def sync(settings)
133
+ command = 'rsync -avz --delete -e "ssh -o StrictHostKeyChecking=no -x " '
134
+ command += "--exclude='.git/' --exclude='log/' --exclude='tmp/' "
135
+ settings.sync_excludes.each do |exc|
136
+ command += "--include='#{exc}' "
137
+ end
138
+ command += ". #{settings.user}@#{attributes.ip_address}:#{settings.remote_project_directory}"
139
+ `#{command}`
140
+ end
141
+
142
+ private
143
+
144
+ def command_exec(user,ip_address,prefix,command)
145
+ `ssh -o StrictHostKeyChecking=no -x -C #{user}@#{attributes.ip_address} "#{prefix} #{command}"`
146
+ end
147
+
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,125 @@
1
+ class Cloudspeq
2
+ class Providers
3
+ class DigitalOcean < Base
4
+
5
+ def initialize(settings)
6
+ ::Digitalocean.client_id = settings.digital_ocean['client_id']
7
+ ::Digitalocean.api_key = settings.digital_ocean['api_key']
8
+ @settings = settings
9
+ @digital = settings.digital_ocean
10
+ @machines = machines
11
+ end
12
+
13
+ def provider_settings
14
+ @digital
15
+ end
16
+
17
+ def create(count=@digital['machine_count'])
18
+ created = []
19
+ count.to_i.times do
20
+ response = Digitalocean::Droplet.create({name: generate_machine_name,
21
+ size_id: size.id,
22
+ image_id: image.id,
23
+ region_id: region.id,
24
+ ssh_key_ids: ssh_key.id})
25
+ if response.status == "OK"
26
+ created << response.droplet
27
+ else
28
+ puts response
29
+ end
30
+ end
31
+ created.each do |machine|
32
+ @machines << Machine.new(Digitalocean::Droplet.find(machine.id).droplet.to_h)
33
+ end
34
+
35
+ end
36
+
37
+ def machines(file='cloudspeq_machines.yml')
38
+ File.write(file, '') unless File.exist?(file)
39
+ yaml = YAML.load_file(file)
40
+ yaml = [] if yaml == false
41
+ yaml.class == Array ? yaml : [yaml]
42
+ end
43
+
44
+ # by default, only grab machines that look like they're related to testing
45
+ # be careful if you set this to true and have other machines on your account
46
+ def remote_machines(all=false)
47
+ if all
48
+ machines = ::Digitalocean::Droplet.all
49
+ else
50
+ string = @settings.machine_prefix ? "#{@settings.machine_prefix}-" : "test-"
51
+ machines = ::Digitalocean::Droplet.all.droplets.select{|d| d.name.start_with?(string)}
52
+ end
53
+ machines.collect{|m| Machine.new(m)}
54
+ end
55
+
56
+ def write_machines(machs,file=@digital['machine_file'])
57
+ @machines= machs
58
+ File.open(file,"w") do |f|
59
+ f.write(machs.to_yaml)
60
+ end
61
+
62
+ end
63
+
64
+ def image
65
+ ::Digitalocean::Image.all.images.select{|i| i.name == @digital['image_name']}.first
66
+ end
67
+
68
+ def ssh_key
69
+ ::Digitalocean::SshKey.all.ssh_keys.select{|i| i.name == @digital['ssh_key_name']}.first
70
+ end
71
+
72
+ def size
73
+ ::Digitalocean::Size.all.sizes.select{|s| s.slug == @digital['size_slug']}.first
74
+ end
75
+
76
+ def region
77
+ ::Digitalocean::Region.all.regions.select{|r| r.slug == @digital['region_slug']}.first
78
+ end
79
+
80
+ def status
81
+ status_hashes = machines.collect {|m| {"#{m.attributes.name}" => m.attributes.status} }
82
+ statuses = status_hashes.collect{|m| m.values}.flatten
83
+ new_machines = statuses.select{|m| m == 'new'}
84
+ active_machines = statuses.select{|m| m == 'active'}
85
+ {'total' => statuses.count, 'new' => new_machines.count, 'active' => active_machines.count, 'machines' => status_hashes}
86
+ end
87
+
88
+ def spool_down_all
89
+ destroy remote_machines
90
+ end
91
+
92
+
93
+
94
+
95
+ class Machine < Base::Machine
96
+
97
+ def name
98
+ attributes.name
99
+ end
100
+
101
+ def ip_address
102
+ attributes.ip_address
103
+ end
104
+
105
+ def power_off
106
+ ::Digitalocean::Droplet.power_off(attributes.id)
107
+ end
108
+
109
+ def power_cycle
110
+ ::Digitalocean::Droplet.power_cycle(attributes.id)
111
+ end
112
+
113
+ def power_on
114
+ ::Digitalocean::Droplet.power_on(attributes.id)
115
+ end
116
+
117
+ def destroy
118
+ ::Digitalocean::Droplet.destroy(attributes.id)
119
+ end
120
+
121
+ end
122
+ end
123
+ end
124
+ end
125
+
@@ -0,0 +1,59 @@
1
+ class Cloudspeq
2
+ class RspecOutputter
3
+
4
+ def self.color(color, text)
5
+ colors = {red: "0;31",
6
+ blue: '0;34',
7
+ green: '0;32',
8
+ yellow: '1;33',
9
+ light_green: '1;32',
10
+ light_red:'1;31',
11
+ light_blue: '1;34',
12
+ purple: '0;35',
13
+ light_purple: '1;35'
14
+ }
15
+ "\033[#{colors[color]}m #{text} \033[0m"
16
+ end
17
+
18
+
19
+ def self.perform(outputs)
20
+ puts "\n\n ***** Spec Report *****\n\n"
21
+ @failures = []
22
+ @outputs = outputs['outputs']
23
+ output_summary_lines
24
+ output_failures
25
+ puts "Total Time: #{outputs['time']}"
26
+ end
27
+
28
+ private
29
+
30
+ def self.output_summary_lines
31
+ @outputs.each do |o|
32
+ puts "#{o['symbol'] || '.'} - #{o['hostname']}: #{o['time'].to_s} #{o['output']['summary_line'] rescue 'unexpected output'}"
33
+ end
34
+ end
35
+
36
+ def self.collect_failures
37
+ @outputs.each do |o|
38
+ if o['output']['examples']
39
+ fails = o['output']['examples'].select{|t| t['status'] == 'failed'}
40
+ fails.each do |f|
41
+ @failures << "#{o['symbol'] || '.'} - #{o['hostname']}: #{f['file_path']}:#{f['line_number']} #{f['exception']['message']}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def self.output_failures
48
+ collect_failures
49
+ if !@failures.empty?
50
+ puts "\n\nFailures:"
51
+ @failures.each{|f| puts color(:red, f)}
52
+ puts "Total Failures: #{@failures.size.to_s}"
53
+ else
54
+ puts color(:green, "No Failures!")
55
+ end
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ class Cloudspeq
2
+ VERSION = "0.0.15"
3
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloudspeq
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.15
5
+ platform: ruby
6
+ authors:
7
+ - Russell Jennings
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: escort
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: digitalocean
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.7'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.10'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.10'
83
+ description: Having a slow test suite sucks. But don't let a slow test suite slow
84
+ you down! with cloudspeq you can distribute your tests and dramatically reduce the
85
+ time it takes to test.
86
+ email:
87
+ - violentpurr@gmail.com
88
+ executables:
89
+ - cloudspeq
90
+ extensions: []
91
+ extra_rdoc_files: []
92
+ files:
93
+ - ".gitignore"
94
+ - Gemfile
95
+ - LICENSE.txt
96
+ - README.md
97
+ - Rakefile
98
+ - bin/cloudspeq
99
+ - cloudspeq.gemspec
100
+ - lib/cloudspeq.rb
101
+ - lib/cloudspeq/defaults.rb
102
+ - lib/cloudspeq/distributed_testing.rb
103
+ - lib/cloudspeq/providers/base.rb
104
+ - lib/cloudspeq/providers/digital_ocean.rb
105
+ - lib/cloudspeq/rspec_outputter.rb
106
+ - lib/cloudspeq/version.rb
107
+ homepage: https://github.com/meesterdude/cloudspeq
108
+ licenses:
109
+ - MIT
110
+ metadata: {}
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 2.2.2
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Distribute your tests in the cloud for faster development in slow test suits
131
+ test_files: []