tddium-old 0.4.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/CHANGELOG +1 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +70 -0
- data/LICENSE.txt +2 -0
- data/README.rdoc +62 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/bin/tddium-old +16 -0
- data/doc/aws-keypair-example.tiff +0 -0
- data/doc/aws-secgroup-example.tiff +0 -0
- data/lib/spec_storm/action_controller_ext.rb +68 -0
- data/lib/spec_storm/action_view_ext.rb +52 -0
- data/lib/spec_storm/active_record_ext.rb +45 -0
- data/lib/tddium.rb +10 -0
- data/lib/tddium/config.rb +146 -0
- data/lib/tddium/instance.rb +212 -0
- data/lib/tddium/parallel.rb +87 -0
- data/lib/tddium/rails.rb +71 -0
- data/lib/tddium/reporting.rb +42 -0
- data/lib/tddium/ssh.rb +49 -0
- data/lib/tddium/taskalias.rb +21 -0
- data/lib/tddium/tasks.rb +159 -0
- data/lib/tddium_helper.rb +62 -0
- data/lib/tddium_loader.rb +38 -0
- data/parallelrun +295 -0
- data/rails/init.rb +1 -0
- data/tddium-old.gemspec +110 -0
- data/test/helper.rb +31 -0
- data/test/test_config.rb +194 -0
- data/test/test_parallel.rb +47 -0
- data/test/test_tddium.rb +372 -0
- metadata +291 -0
@@ -0,0 +1,212 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright (c) 2010 tddium.com All Rights Reserved
|
3
|
+
=end
|
4
|
+
|
5
|
+
#
|
6
|
+
# tddium support methods
|
7
|
+
#
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'rubygems'
|
11
|
+
require 'fog'
|
12
|
+
require 'net/http'
|
13
|
+
require 'uri'
|
14
|
+
AMI_NAME = 'ami-da13e3b3'
|
15
|
+
DEV_SESSION_KEY='dev'
|
16
|
+
|
17
|
+
def setup_environment(server)
|
18
|
+
if !$tunnel_pid.nil?
|
19
|
+
ENV['SELENIUM_RC_HOST'] = 'localhost'
|
20
|
+
else
|
21
|
+
ENV['SELENIUM_RC_HOST'] = server.dns_name
|
22
|
+
end
|
23
|
+
ENV['TDDIUM'] = '1'
|
24
|
+
end
|
25
|
+
|
26
|
+
# Start and setup an EC2 instance to run a selenium-grid node. Set the
|
27
|
+
# tddium_session tag to session_key, if it's specified.
|
28
|
+
def start_instance(session_key=nil)
|
29
|
+
conf = read_config
|
30
|
+
|
31
|
+
if session_key.nil?
|
32
|
+
@tddium_session = rand(2**64-1).to_s(36)
|
33
|
+
else
|
34
|
+
@tddium_session = session_key
|
35
|
+
end
|
36
|
+
|
37
|
+
key_file = get_keyfile
|
38
|
+
|
39
|
+
@ec2pool = Fog::AWS::Compute.new(:aws_access_key_id => conf[:aws_key],
|
40
|
+
:aws_secret_access_key => conf[:aws_secret])
|
41
|
+
|
42
|
+
server = @ec2pool.servers.create(:flavor_id => 'm1.large',
|
43
|
+
:groups => ['selenium-grid'],
|
44
|
+
:image_id => AMI_NAME,
|
45
|
+
:name => 'sg-server',
|
46
|
+
:key_name => conf[:key_name])
|
47
|
+
|
48
|
+
server.wait_for { ready? }
|
49
|
+
server.reload
|
50
|
+
|
51
|
+
@ec2pool.tags.create(:key => 'tddium_session',
|
52
|
+
:value => @tddium_session,
|
53
|
+
:resource_id => server.id)
|
54
|
+
|
55
|
+
if conf.include?(:server_tag) then
|
56
|
+
server_tag = conf[:server_tag].split('=')
|
57
|
+
|
58
|
+
@ec2pool.tags.create(:key => server_tag[0],
|
59
|
+
:value => server_tag[1],
|
60
|
+
:resource_id => server.id)
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
puts "started instance #{server.id} #{server.dns_name} in group #{server.groups} with tags #{server.tags.inspect}"
|
65
|
+
|
66
|
+
if conf[:ssh_tunnel] == "true"
|
67
|
+
make_ssh_tunnel(key_file, server)
|
68
|
+
end
|
69
|
+
|
70
|
+
setup_environment(server)
|
71
|
+
|
72
|
+
uri = wait_for_selenium(ENV['SELENIUM_RC_HOST'])
|
73
|
+
|
74
|
+
puts "Selenium Console:"
|
75
|
+
puts "#{uri}"
|
76
|
+
|
77
|
+
if !key_file.nil?
|
78
|
+
STDERR.puts "You can login via \"ssh -i #{key_file} ec2-user@#{server.dns_name}\""
|
79
|
+
STDERR.puts "Making /var/log/messages world readable"
|
80
|
+
remote_cmd(server.dns_name, "sudo chmod 644 /var/log/messages")
|
81
|
+
else
|
82
|
+
# TODO: Remove when /var/log/messages bug is fixed
|
83
|
+
STDERR.puts "No key_file provided. /var/log/messages may not be readable by ec2-user."
|
84
|
+
end
|
85
|
+
|
86
|
+
server
|
87
|
+
end
|
88
|
+
|
89
|
+
def wait_for_selenium(hostname)
|
90
|
+
uri = URI.parse("http://#{hostname}:4444/console")
|
91
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
92
|
+
http.open_timeout = 60
|
93
|
+
http.read_timeout = 60
|
94
|
+
|
95
|
+
rc_up = false
|
96
|
+
tries = 0
|
97
|
+
while !rc_up && tries < 5
|
98
|
+
begin
|
99
|
+
http.request(Net::HTTP::Get.new(uri.request_uri))
|
100
|
+
rc_up = true
|
101
|
+
rescue Errno::ECONNREFUSED
|
102
|
+
sleep 5
|
103
|
+
rescue Timeout::Error
|
104
|
+
ensure
|
105
|
+
tries += 1
|
106
|
+
end
|
107
|
+
end
|
108
|
+
raise "Couldn't connect to #{uri.request_uri}" unless rc_up
|
109
|
+
uri
|
110
|
+
end
|
111
|
+
|
112
|
+
def recycle_server(dns_name)
|
113
|
+
conf = read_config
|
114
|
+
keyfile = get_keyfile
|
115
|
+
|
116
|
+
if keyfile.nil?
|
117
|
+
raise "No keyfile. Can't execute remote commands"
|
118
|
+
end
|
119
|
+
|
120
|
+
remote_cmd(dns_name, "sudo killall -9 java")
|
121
|
+
remote_cmd(dns_name, "bin/launch-hub.sh")
|
122
|
+
wait_for_selenium(dns_name)
|
123
|
+
remote_cmd(dns_name, "bin/launch-rc.sh")
|
124
|
+
sleep 5
|
125
|
+
STDERR.puts "Recycled Selenium on instance: #{dns_name}"
|
126
|
+
end
|
127
|
+
|
128
|
+
def recycle_servers
|
129
|
+
servers = find_instances
|
130
|
+
servers.each do |s|
|
131
|
+
recycle_server(s.dns_name)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def recycle_dev
|
136
|
+
dev_servers = session_instances(DEV_SESSION_KEY)
|
137
|
+
|
138
|
+
if dev_servers.length > 0 then
|
139
|
+
recycle_server(dev_servers[0].dns_name)
|
140
|
+
else
|
141
|
+
STDERR.puts "No dev servers found"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def checkstart_dev_instance
|
146
|
+
conf = read_config
|
147
|
+
dev_servers = session_instances(DEV_SESSION_KEY)
|
148
|
+
if dev_servers.length > 0
|
149
|
+
STDERR.puts "Using existing server #{dev_servers[0].dns_name}."
|
150
|
+
setup_environment(dev_servers[0])
|
151
|
+
return dev_servers[0]
|
152
|
+
else
|
153
|
+
STDERR.puts "Starting EC2 Instance"
|
154
|
+
server = start_instance(DEV_SESSION_KEY)
|
155
|
+
sleep 30
|
156
|
+
return server
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Find all instances running the tddium AMI
|
161
|
+
def find_instances
|
162
|
+
conf = read_config
|
163
|
+
@ec2pool = Fog::AWS::Compute.new(:aws_access_key_id => conf[:aws_key],
|
164
|
+
:aws_secret_access_key => conf[:aws_secret])
|
165
|
+
|
166
|
+
@ec2pool.servers.select do |s|
|
167
|
+
s.image_id == AMI_NAME &&
|
168
|
+
!%w{terminated stopped shutting-down}.include?(s.state)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def session_instances(session_key)
|
173
|
+
servers = find_instances
|
174
|
+
if servers.nil?
|
175
|
+
return nil
|
176
|
+
else
|
177
|
+
session_servers = []
|
178
|
+
servers.each do |s|
|
179
|
+
# in Fog 0.3.33, :filters is buggy and won't accept resourceId or resource_id
|
180
|
+
tags = @ec2pool.tags(:filters => {:key => 'tddium_session'}).select{|t| t.resource_id == s.id}
|
181
|
+
if tags.first and tags.first.value == session_key then
|
182
|
+
STDERR.puts "selecting instance #{s.id} #{s.dns_name} from our session"
|
183
|
+
session_servers << s
|
184
|
+
else
|
185
|
+
STDERR.puts "skipping instance #{s.id} #{s.dns_name} created in another session"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
return session_servers
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def stop_all_instances
|
193
|
+
servers = find_instances
|
194
|
+
servers.each do |s|
|
195
|
+
STDERR.puts "stopping instance #{s.id} #{s.dns_name}"
|
196
|
+
s.destroy
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Stop the instance created by start_instance
|
201
|
+
def stop_instance(session_key=nil)
|
202
|
+
conf = read_config
|
203
|
+
|
204
|
+
kill_tunnel
|
205
|
+
|
206
|
+
servers = session_instances(session_key ? session_key : @tddium_session)
|
207
|
+
servers.each do |s|
|
208
|
+
STDERR.puts "stopping instance #{s.id} #{s.dns_name} from our session"
|
209
|
+
s.destroy
|
210
|
+
end
|
211
|
+
nil
|
212
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright (c) 2010 tddium.com All Rights Reserved
|
3
|
+
=end
|
4
|
+
|
5
|
+
require 'parallel'
|
6
|
+
require 'tddium/config'
|
7
|
+
|
8
|
+
def make_spec_cmd(tests, environment, result_path)
|
9
|
+
if !result_path
|
10
|
+
raise "result_path must be specified"
|
11
|
+
end
|
12
|
+
|
13
|
+
environ = {
|
14
|
+
"RAILS_ENV" => environment,
|
15
|
+
'RSPEC_COLOR' => $stdout.tty? ? 1 : nil
|
16
|
+
}
|
17
|
+
|
18
|
+
env_str = environ.map{|k,v| "#{k}=#{v}" if v}.join(' ')
|
19
|
+
cmd = "env #{env_str} "
|
20
|
+
cmd << "spec "
|
21
|
+
cmd << "#{tests.map{|x| "\"#{x}\""}.join(' ')} "
|
22
|
+
cmd << spec_opts(result_path).join(' ')
|
23
|
+
cmd
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_batches(num_batches)
|
27
|
+
tests = find_test_files
|
28
|
+
STDERR.puts "\t#{tests.size} test files"
|
29
|
+
|
30
|
+
chunk_size = tests.size / num_batches
|
31
|
+
remainder = tests.size % num_batches
|
32
|
+
batches = []
|
33
|
+
num_batches.times do |c|
|
34
|
+
if c < remainder
|
35
|
+
batches << tests[((c*chunk_size)+c),(chunk_size+1)]
|
36
|
+
else
|
37
|
+
batches << tests[((c*chunk_size)+remainder),chunk_size]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
STDERR.puts batches.inspect
|
42
|
+
batches
|
43
|
+
end
|
44
|
+
|
45
|
+
def parallel_task(args)
|
46
|
+
args.with_defaults(:threads => 5, :environment => "selenium", :result_directory => '.')
|
47
|
+
threads = args.threads.to_i
|
48
|
+
|
49
|
+
STDERR.puts args.inspect
|
50
|
+
|
51
|
+
latest = args.result_directory
|
52
|
+
puts "Running tests. Results will be in #{latest}"
|
53
|
+
puts "Started at #{Time.now.inspect}"
|
54
|
+
|
55
|
+
output = {}
|
56
|
+
|
57
|
+
batches = test_batches(threads)
|
58
|
+
num_threads = batches.size
|
59
|
+
|
60
|
+
Parallel.in_threads(num_threads) do |i|
|
61
|
+
if batches[i].size > 0
|
62
|
+
result_path = File.join(latest, "#{i}-#{REPORT_FILENAME}")
|
63
|
+
cmd = make_spec_cmd(batches[i], args.environment, result_path)
|
64
|
+
output.merge!({"#{batches[i].inspect}" => execute_command( cmd )})
|
65
|
+
#puts "Running results: #{output.inspect}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
puts "Results:"
|
70
|
+
output.each do |key, value|
|
71
|
+
puts ">>>>>>>> #{key}"
|
72
|
+
puts value
|
73
|
+
end
|
74
|
+
puts "Finished at #{Time.now.inspect}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.execute_command(cmd)
|
78
|
+
STDERR.puts "Running '#{cmd}'"
|
79
|
+
f = open("|#{cmd}")
|
80
|
+
all = ''
|
81
|
+
while out = f.gets(".")
|
82
|
+
all+=out
|
83
|
+
print out
|
84
|
+
STDOUT.flush
|
85
|
+
end
|
86
|
+
all
|
87
|
+
end
|
data/lib/tddium/rails.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright (c) 2010 tddium.com All Rights Reserved
|
3
|
+
=end
|
4
|
+
|
5
|
+
require 'tddium/config'
|
6
|
+
require 'digest/sha1'
|
7
|
+
require 'tddium_helper'
|
8
|
+
require 'ftools'
|
9
|
+
|
10
|
+
# Portions of this file derived from spec_storm, under the following license:
|
11
|
+
#
|
12
|
+
# Copyright (c) 2010 Sauce Labs Inc
|
13
|
+
#
|
14
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
15
|
+
# a copy of this software and associated documentation files (the
|
16
|
+
# "Software"), to deal in the Software without restriction, including
|
17
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
18
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
19
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
20
|
+
# the following conditions:
|
21
|
+
#
|
22
|
+
# The above copyright notice and this permission notice shall be
|
23
|
+
# included in all copies or substantial portions of the Software.
|
24
|
+
#
|
25
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
26
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
27
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
28
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
29
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
30
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
31
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
32
|
+
|
33
|
+
def prepare_task(args)
|
34
|
+
args.with_defaults(:environment => "selenium")
|
35
|
+
|
36
|
+
tests = find_test_files()
|
37
|
+
puts "\t#{tests.size} test files"
|
38
|
+
first = true
|
39
|
+
|
40
|
+
tests.each do |test|
|
41
|
+
prefix = SpecStorm::db_prefix_for(test)
|
42
|
+
puts "Migrating another set of tables..."
|
43
|
+
puts "Generating DB_PREFIX: #{test} -> #{prefix}"
|
44
|
+
|
45
|
+
if first == true
|
46
|
+
["export DB_PREFIX=#{prefix}; rake db:drop RAILS_ENV=#{args.environment} --trace",
|
47
|
+
"export DB_PREFIX=#{prefix}; rake db:create RAILS_ENV=#{args.environment} --trace"].each do |command|
|
48
|
+
IO.popen( command ).close
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
["export DB_PREFIX=#{prefix}; rake db:migrate RAILS_ENV=#{args.environment} --trace"].each do |cmd|
|
53
|
+
IO.popen( cmd ).close
|
54
|
+
end
|
55
|
+
|
56
|
+
first = false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def setup_task(args)
|
61
|
+
args.with_defaults(:environment => "selenium")
|
62
|
+
|
63
|
+
File.copy(File.join(RAILS_ROOT, 'config', 'environments', 'test.rb'),
|
64
|
+
File.join(RAILS_ROOT, 'config', 'environments', "#{args.environment}.rb"))
|
65
|
+
|
66
|
+
open(File.join(RAILS_ROOT, 'config', 'environments', "#{args.environment}.rb"), 'a') do |f|
|
67
|
+
f.puts "\nmodule SpecStorm"
|
68
|
+
f.puts " USE_NAMESPACE_HACK = true"
|
69
|
+
f.puts "end"
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright (c) 2010 tddium.com All Rights Reserved
|
3
|
+
=end
|
4
|
+
#
|
5
|
+
# Prepare the result directory, as specified by config[:result_directory].
|
6
|
+
#
|
7
|
+
# If the directory doesn't exist create it, and a latest subdirectory.
|
8
|
+
#
|
9
|
+
# If the latest subdirectory exists, rotate it and create a new empty latest.
|
10
|
+
#
|
11
|
+
def result_directory
|
12
|
+
conf = read_config
|
13
|
+
latest = File.join(conf[:result_directory], 'latest')
|
14
|
+
|
15
|
+
if File.directory?(latest) then
|
16
|
+
mtime = File.stat(latest).mtime.strftime("%Y%m%d-%H%M%S")
|
17
|
+
archive = File.join(conf[:result_directory], mtime)
|
18
|
+
FileUtils.mv(latest, archive)
|
19
|
+
end
|
20
|
+
FileUtils.mkdir_p latest
|
21
|
+
latest
|
22
|
+
end
|
23
|
+
|
24
|
+
REPORT_FILENAME = "selenium_report.html"
|
25
|
+
|
26
|
+
def default_report_path
|
27
|
+
File.join(read_config[:result_directory], 'latest', REPORT_FILENAME)
|
28
|
+
end
|
29
|
+
|
30
|
+
def collect_syslog(target_directory='.')
|
31
|
+
keyfile = get_keyfile
|
32
|
+
if keyfile.nil?
|
33
|
+
raise "No ssh keyfile configured. Can't connect to remote"
|
34
|
+
end
|
35
|
+
instances = session_instances(@tddium_session ? @tddium_session : DEV_SESSION_KEY)
|
36
|
+
instances.each do |inst|
|
37
|
+
%w(selenium-hub selenium-rc).each do |log|
|
38
|
+
remote_cp(inst.dns_name, "/var/log/#{log}.log",
|
39
|
+
File.join(target_directory, "#{log}.#{inst.dns_name}"))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/tddium/ssh.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright (c) 2010 tddium.com All Rights Reserved
|
3
|
+
=end
|
4
|
+
|
5
|
+
# Subprocess main body to create an ssh tunnel to hostname for selenium, binding
|
6
|
+
# remote:4444 to local:4444. Authenticate with the private key in key_file.
|
7
|
+
#
|
8
|
+
# The ssh tunnel will auto-accept the remote host key.
|
9
|
+
def ssh_tunnel(hostname)
|
10
|
+
ssh_up = false
|
11
|
+
tries = 0
|
12
|
+
while !ssh_up && tries < 3
|
13
|
+
sleep 3
|
14
|
+
ssh_up = remote_cmd(hostname, "-L 4444:#{hostname}:4444 -N")
|
15
|
+
tries += 1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def make_ssh_tunnel(key_file, server)
|
20
|
+
$tunnel_pid = nil
|
21
|
+
if !key_file.nil? then
|
22
|
+
$tunnel_pid = Process.fork do
|
23
|
+
ssh_tunnel(server.dns_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
STDERR.puts "Created ssh tunnel to #{server.dns_name}:4444 at localhost:4444 [pid #{$tunnel_pid}]"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def kill_tunnel
|
31
|
+
if !$tunnel_pid.nil?
|
32
|
+
Process.kill("TERM", $tunnel_pid)
|
33
|
+
Process.waitpid($tunnel_pid)
|
34
|
+
$tunnel_pid = nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def remote_cmd(host, cmd)
|
40
|
+
key_file = get_keyfile
|
41
|
+
|
42
|
+
system("ssh -o 'StrictHostKeyChecking no' -i #{key_file} ec2-user@#{host} '#{cmd}'")
|
43
|
+
end
|
44
|
+
|
45
|
+
def remote_cp(host, remote_file, local_file)
|
46
|
+
key_file = get_keyfile
|
47
|
+
system("scp -o 'StrictHostKeyChecking no' -i #{key_file} ec2-user@#{host}:#{remote_file} #{local_file}")
|
48
|
+
end
|
49
|
+
|