tddium-old 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.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
|
+
|