cloudspeq 0.0.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +229 -0
- data/Rakefile +2 -0
- data/bin/cloudspeq +177 -0
- data/cloudspeq.gemspec +28 -0
- data/lib/cloudspeq.rb +122 -0
- data/lib/cloudspeq/defaults.rb +30 -0
- data/lib/cloudspeq/distributed_testing.rb +128 -0
- data/lib/cloudspeq/providers/base.rb +151 -0
- data/lib/cloudspeq/providers/digital_ocean.rb +125 -0
- data/lib/cloudspeq/rspec_outputter.rb +59 -0
- data/lib/cloudspeq/version.rb +3 -0
- metadata +131 -0
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
data/Gemfile
ADDED
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
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
|
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: []
|