Top Level Namespace

Defined Under Namespace

Modules: Rake, SpecStorm

Constant Summary collapse

ALREADY_CONFIGURED =
<<'EOF'

tddium has already been initialized.

(settings are in %s)

Use 'tddium config:reset' to clear configuration, and then run 'tddium config:init' again.
EOF
CONFIG_DEFAULTS =
{
  :aws_key => nil,
  :aws_secret => nil,
  :test_pattern => '**/*_test.rb',
  :key_name => nil,
  :key_directory => nil,
  :result_directory => 'results',
  :ssh_tunnel => false,
  :require_files => nil,
}
AMI_NAME =
'ami-da13e3b3'
DEV_SESSION_KEY =
'dev'
REPORT_FILENAME =
"selenium_report.html"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.execute_command(cmd) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
# File 'lib/tddium/parallel.rb', line 77

def self.execute_command(cmd)
  STDERR.puts "Running '#{cmd}'"
  f = open("|#{cmd}")
  all = ''
  while out = f.gets(".")
    all+=out
    print out
    STDOUT.flush
  end
  all
end

Instance Method Details

#checkstart_dev_instanceObject



145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/tddium/instance.rb', line 145

def checkstart_dev_instance
  conf = read_config
  dev_servers = session_instances(DEV_SESSION_KEY)
  if dev_servers.length > 0
    STDERR.puts "Using existing server #{dev_servers[0].dns_name}."
    setup_environment(dev_servers[0])
    return dev_servers[0]
  else
    STDERR.puts "Starting EC2 Instance"
    server = start_instance(DEV_SESSION_KEY)
    sleep 30
    return server
  end
end

#collect_syslog(target_directory = '.') ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/tddium/reporting.rb', line 30

def collect_syslog(target_directory='.')
  keyfile = get_keyfile
  if keyfile.nil?
    raise "No ssh keyfile configured.  Can't connect to remote"
  end
  instances = session_instances(@tddium_session ? @tddium_session : DEV_SESSION_KEY)
  instances.each do |inst|
    %w(selenium-hub selenium-rc).each do |log|
      remote_cp(inst.dns_name, "/var/log/#{log}.log",
                File.join(target_directory, "#{log}.#{inst.dns_name}"))
    end
  end
end

#default_report_pathObject



26
27
28
# File 'lib/tddium/reporting.rb', line 26

def default_report_path
  File.join(read_config[:result_directory], 'latest', REPORT_FILENAME)
end

#find_configObject



40
41
42
43
# File 'lib/tddium/config.rb', line 40

def find_config
  get_config_paths.each {|f| return f if File.exists?(f)}
  nil
end

#find_instancesObject

Find all instances running the tddium AMI



161
162
163
164
165
166
167
168
169
170
# File 'lib/tddium/instance.rb', line 161

def find_instances
  conf = read_config
  @ec2pool = Fog::AWS::Compute.new(:aws_access_key_id => conf[:aws_key],
                              :aws_secret_access_key => conf[:aws_secret])

  @ec2pool.servers.select do |s|
    s.image_id == AMI_NAME &&
      !%w{terminated stopped shutting-down}.include?(s.state)
  end
end

#find_test_files(pattern = nil) ⇒ Object



130
131
132
133
134
# File 'lib/tddium/config.rb', line 130

def find_test_files(pattern=nil)
  conf = read_config
  pattern ||= conf[:test_pattern]
  Dir[pattern].sort
end

#get_config_pathsObject



24
25
26
27
28
29
30
31
32
# File 'lib/tddium/config.rb', line 24

def get_config_paths
  paths = []
  if ENV['RAILS_ROOT']
    paths << File.join(ENV['RAILS_ROOT'], '.tddium')
  end
  paths << File.expand_path('~/.tddium')
  paths << '.tddium'
  paths
end

#get_keyfileObject



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/tddium/config.rb', line 113

def get_keyfile
  conf = read_config
  key_file = key_file_name(conf)
  if key_file.nil?
    return nil
  elsif !File.exists?(key_file)
    STDERR.puts "No key file #{key_file} present"
    return nil
  elsif (File.stat(key_file).mode & "77".to_i(8) != 0)
    mode =File.stat(key_file).mode 
    STDERR.puts "Keyfile has wrong perms: #{mode.to_s}. should be x00"
    return nil
  else
    return key_file
  end
end

#init_taskObject



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/tddium/config.rb', line 57

def init_task
  path = find_config
  if path
    puts ALREADY_CONFIGURED % path
  else
    conf = {}
    conf[:aws_key] = ask('Enter AWS Access Key: ')
    conf[:aws_secret] = ask('Enter AWS Secret: ')
    conf[:test_pattern] = ask('Enter filepattern for tests: ') { |q|
      q.default='**/*_spec.rb'
    }
    conf[:key_directory] = ask('Enter directory for secret key(s): ') { |q|
      q.default='spec/secret'
    }
    conf[:key_name] = ask('Enter secret key name (excluding .pem suffix): ') { |q|
      q.default='sg-keypair'
    }
    conf[:result_directory] = ask('Enter directory for result reports: ') { |q|
      q.default='results'
    }
    conf[:server_tag] = ask("(optional) Enter tag=value to give instances: ") 
    conf[:ssh_tunnel] = ask("Create ssh tunnel to hub at localhost:4444:") { |q|
      q.default=false
    }
    conf[:require_files] = ask("(optional) Filenames (comma-separated) for spec to require:") 

    write_config conf
  end
end

#key_file_name(config) ⇒ Object

Compute the name of the ssh private key file from configured parameters



105
106
107
108
109
110
111
# File 'lib/tddium/config.rb', line 105

def key_file_name(config)
  if config[:key_name].nil? || config[:key_directory].nil?
    return nil
  end

  File.join(config[:key_directory], "#{config[:key_name]}.pem")
end

#kill_tunnelObject



30
31
32
33
34
35
36
# File 'lib/tddium/ssh.rb', line 30

def kill_tunnel
  if !$tunnel_pid.nil?
    Process.kill("TERM", $tunnel_pid)
    Process.waitpid($tunnel_pid)
    $tunnel_pid = nil
  end
end

#make_spec_cmd(tests, environment, result_path) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/tddium/parallel.rb', line 8

def make_spec_cmd(tests, environment, result_path)
  if !result_path
    raise "result_path must be specified"
  end

  environ = { 
    "RAILS_ENV" => environment,
    'RSPEC_COLOR' => $stdout.tty? ? 1 : nil
  }

  env_str = environ.map{|k,v| "#{k}=#{v}" if v}.join(' ')
  cmd = "env #{env_str} "
  cmd << "spec "
  cmd << "#{tests.map{|x| "\"#{x}\""}.join(' ')} "
  cmd << spec_opts(result_path).join(' ')
  cmd
end

#make_ssh_tunnel(key_file, server) ⇒ Object



19
20
21
22
23
24
25
26
27
28
# File 'lib/tddium/ssh.rb', line 19

def make_ssh_tunnel(key_file, server)
  $tunnel_pid = nil
  if !key_file.nil? then
    $tunnel_pid = Process.fork do
      ssh_tunnel(server.dns_name)
    end

    STDERR.puts "Created ssh tunnel to #{server.dns_name}:4444 at localhost:4444 [pid #{$tunnel_pid}]"
  end
end

#parallel_task(args) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/tddium/parallel.rb', line 45

def parallel_task(args)
  args.with_defaults(:threads => 5, :environment => "selenium", :result_directory => '.')
  threads = args.threads.to_i

  STDERR.puts args.inspect

  latest = args.result_directory
  puts "Running tests. Results will be in #{latest}"
  puts "Started at #{Time.now.inspect}"

  output = {}

  batches = test_batches(threads)
  num_threads = batches.size

  Parallel.in_threads(num_threads) do |i|
    if batches[i].size > 0
      result_path = File.join(latest, "#{i}-#{REPORT_FILENAME}")
      cmd = make_spec_cmd(batches[i], args.environment, result_path)
      output.merge!({"#{batches[i].inspect}" => execute_command( cmd )})
      #puts "Running results: #{output.inspect}"
    end
  end

  puts "Results:"
  output.each do |key, value|
    puts ">>>>>>>> #{key}"
    puts value
  end
  puts "Finished at #{Time.now.inspect}"
end

#prepare_task(args) ⇒ Object

Portions of this file derived from spec_storm, under the following license:

Copyright © 2010 Sauce Labs Inc

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/tddium/rails.rb', line 33

def prepare_task(args)
  args.with_defaults(:environment => "selenium")

  tests = find_test_files()
  puts "\t#{tests.size} test files"
  first = true

  tests.each do |test|
    prefix = SpecStorm::db_prefix_for(test)
    puts "Migrating another set of tables..."
    puts "Generating DB_PREFIX: #{test} -> #{prefix}"

    if first == true
      ["export DB_PREFIX=#{prefix}; rake db:drop RAILS_ENV=#{args.environment} --trace",
       "export DB_PREFIX=#{prefix}; rake db:create RAILS_ENV=#{args.environment} --trace"].each do |command|
        IO.popen( command ).close 
      end
    end
    
    ["export DB_PREFIX=#{prefix}; rake db:migrate RAILS_ENV=#{args.environment} --trace"].each do |cmd|
      IO.popen( cmd ).close
    end
    
    first = false
  end
end

#read_configObject



98
99
100
101
102
# File 'lib/tddium/config.rb', line 98

def read_config
  path = find_config
  file_conf = path ? YAML.load(File.read(path)) : {}
  CONFIG_DEFAULTS.merge(file_conf)
end

#recycle_devObject



135
136
137
138
139
140
141
142
143
# File 'lib/tddium/instance.rb', line 135

def recycle_dev
  dev_servers = session_instances(DEV_SESSION_KEY)
  
  if dev_servers.length > 0 then
    recycle_server(dev_servers[0].dns_name)
  else
    STDERR.puts "No dev servers found"
  end
end

#recycle_server(dns_name) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/tddium/instance.rb', line 112

def recycle_server(dns_name)
  conf = read_config
  keyfile = get_keyfile

  if keyfile.nil?
    raise "No keyfile.  Can't execute remote commands"
  end

  remote_cmd(dns_name, "sudo killall -9 java")
  remote_cmd(dns_name, "bin/launch-hub.sh")
  wait_for_selenium(dns_name)
  remote_cmd(dns_name, "bin/launch-rc.sh")
  sleep 5
  STDERR.puts "Recycled Selenium on instance: #{dns_name}"
end

#recycle_serversObject



128
129
130
131
132
133
# File 'lib/tddium/instance.rb', line 128

def recycle_servers
  servers = find_instances
  servers.each do |s|
    recycle_server(s.dns_name)
  end
end

#remote_cmd(host, cmd) ⇒ Object



39
40
41
42
43
# File 'lib/tddium/ssh.rb', line 39

def remote_cmd(host, cmd)
  key_file = get_keyfile

  system("ssh -o 'StrictHostKeyChecking no' -i #{key_file} ec2-user@#{host} '#{cmd}'")
end

#remote_cp(host, remote_file, local_file) ⇒ Object



45
46
47
48
# File 'lib/tddium/ssh.rb', line 45

def remote_cp(host, remote_file, local_file)
  key_file = get_keyfile
  system("scp -o 'StrictHostKeyChecking no' -i #{key_file} ec2-user@#{host}:#{remote_file} #{local_file}")
end

#reset_taskObject



45
46
47
48
49
50
51
52
53
# File 'lib/tddium/config.rb', line 45

def reset_task
  config_path = find_config
  config = read_config
  if config_path
    puts "Deleting old configuration in #{config_path}:"
    puts config.inspect
    rm config_path, :force => true
  end
end

#result_directoryObject

Prepare the result directory, as specified by config.

If the directory doesn’t exist create it, and a latest subdirectory.

If the latest subdirectory exists, rotate it and create a new empty latest.



11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/tddium/reporting.rb', line 11

def result_directory
  conf = read_config
  latest = File.join(conf[:result_directory], 'latest')

  if File.directory?(latest) then
    mtime = File.stat(latest).mtime.strftime("%Y%m%d-%H%M%S")
    archive = File.join(conf[:result_directory], mtime)
    FileUtils.mv(latest, archive)
  end
  FileUtils.mkdir_p latest
  latest
end

#session_instances(session_key) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/tddium/instance.rb', line 172

def session_instances(session_key)
  servers = find_instances
  if servers.nil?
    return nil
  else
    session_servers = []
    servers.each do |s|
      # in Fog 0.3.33, :filters is buggy and won't accept resourceId or resource_id
      tags = @ec2pool.tags(:filters => {:key => 'tddium_session'}).select{|t| t.resource_id == s.id}
      if tags.first and tags.first.value == session_key then
        STDERR.puts "selecting instance #{s.id} #{s.dns_name} from our session"
        session_servers << s
      else
        STDERR.puts "skipping instance #{s.id} #{s.dns_name} created in another session"
      end
    end
    return session_servers
  end
end

#setup_environment(server) ⇒ Object



17
18
19
20
21
22
23
24
# File 'lib/tddium/instance.rb', line 17

def setup_environment(server)
  if !$tunnel_pid.nil?
    ENV['SELENIUM_RC_HOST'] = 'localhost'
  else
    ENV['SELENIUM_RC_HOST'] = server.dns_name
  end
  ENV['TDDIUM'] = '1'
end

#setup_task(args) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/tddium/rails.rb', line 60

def setup_task(args)
  args.with_defaults(:environment => "selenium")

  File.copy(File.join(RAILS_ROOT, 'config', 'environments', 'test.rb'),
            File.join(RAILS_ROOT, 'config', 'environments', "#{args.environment}.rb"))

  open(File.join(RAILS_ROOT, 'config', 'environments', "#{args.environment}.rb"), 'a') do |f|
    f.puts "\nmodule SpecStorm"
    f.puts "  USE_NAMESPACE_HACK = true"
    f.puts "end"
  end
end

#spec_opts(result_path) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
# File 'lib/tddium/config.rb', line 136

def spec_opts(result_path)
  conf = read_config
  s = []
  s << '--color'
  s << "--require '#{conf[:require_files]}'" if conf[:require_files]
  s << "--require 'rubygems,selenium/rspec/reporting/selenium_test_report_formatter'"
  s << "--format=Selenium::RSpec::SeleniumTestReportFormatter:#{result_path}"
  s << "--format=progress"                
  s << ENV['TDDIUM_SPEC_OPTS'] if ENV['TDDIUM_SPEC_OPTS']
  s
end

#ssh_tunnel(hostname) ⇒ Object

Subprocess main body to create an ssh tunnel to hostname for selenium, binding remote:4444 to local:4444. Authenticate with the private key in key_file.

The ssh tunnel will auto-accept the remote host key.



9
10
11
12
13
14
15
16
17
# File 'lib/tddium/ssh.rb', line 9

def ssh_tunnel(hostname)
  ssh_up = false
  tries = 0
  while !ssh_up && tries < 3
    sleep 3
    ssh_up = remote_cmd(hostname, "-L 4444:#{hostname}:4444 -N")
    tries += 1
  end
end

#start_instance(session_key = nil) ⇒ Object

Start and setup an EC2 instance to run a selenium-grid node. Set the tddium_session tag to session_key, if it’s specified.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/tddium/instance.rb', line 28

def start_instance(session_key=nil)
  conf = read_config

  if session_key.nil?
    @tddium_session = rand(2**64-1).to_s(36)
  else
    @tddium_session = session_key
  end

  key_file = get_keyfile

  @ec2pool = Fog::AWS::Compute.new(:aws_access_key_id => conf[:aws_key],
                                   :aws_secret_access_key => conf[:aws_secret])

  server = @ec2pool.servers.create(:flavor_id => 'm1.large',
                                   :groups => ['selenium-grid'],
                                   :image_id => AMI_NAME,
                                   :name => 'sg-server',
                                   :key_name => conf[:key_name])

  server.wait_for { ready? }
  server.reload

  @ec2pool.tags.create(:key => 'tddium_session', 
                       :value => @tddium_session,
                       :resource_id => server.id)

  if conf.include?(:server_tag) then
    server_tag = conf[:server_tag].split('=')

    @ec2pool.tags.create(:key => server_tag[0],
                         :value => server_tag[1],
                         :resource_id => server.id)
  end


  puts "started instance #{server.id} #{server.dns_name} in group #{server.groups} with tags #{server.tags.inspect}"

  if conf[:ssh_tunnel] == "true"
    make_ssh_tunnel(key_file, server)
  end

  setup_environment(server)

  uri = wait_for_selenium(ENV['SELENIUM_RC_HOST'])

  puts "Selenium Console:"
  puts "#{uri}"

  if !key_file.nil?
    STDERR.puts "You can login via \"ssh -i #{key_file} ec2-user@#{server.dns_name}\""
    STDERR.puts "Making /var/log/messages world readable"
    remote_cmd(server.dns_name, "sudo chmod 644 /var/log/messages")
  else
    # TODO: Remove when /var/log/messages bug is fixed
    STDERR.puts "No key_file provided.  /var/log/messages may not be readable by ec2-user."
  end

  server
end

#stop_all_instancesObject



192
193
194
195
196
197
198
# File 'lib/tddium/instance.rb', line 192

def stop_all_instances
  servers = find_instances
  servers.each do |s|
    STDERR.puts "stopping instance #{s.id} #{s.dns_name}"
    s.destroy
  end
end

#stop_instance(session_key = nil) ⇒ Object

Stop the instance created by start_instance



201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/tddium/instance.rb', line 201

def stop_instance(session_key=nil)
  conf = read_config

  kill_tunnel

  servers = session_instances(session_key ? session_key : @tddium_session)
  servers.each do |s|
    STDERR.puts "stopping instance #{s.id} #{s.dns_name} from our session"
    s.destroy
  end
  nil
end

#test_batches(num_batches) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/tddium/parallel.rb', line 26

def test_batches(num_batches)
  tests = find_test_files
  STDERR.puts "\t#{tests.size} test files"

  chunk_size = tests.size / num_batches
  remainder = tests.size % num_batches
  batches = []
  num_batches.times do |c|
    if c < remainder
      batches << tests[((c*chunk_size)+c),(chunk_size+1)]
    else
      batches << tests[((c*chunk_size)+remainder),chunk_size]
    end
  end
  
  STDERR.puts batches.inspect
  batches
end

#wait_for_selenium(hostname) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/tddium/instance.rb', line 89

def wait_for_selenium(hostname)
  uri = URI.parse("http://#{hostname}:4444/console")
  http = Net::HTTP.new(uri.host, uri.port)
  http.open_timeout = 60
  http.read_timeout = 60

  rc_up = false
  tries = 0
  while !rc_up && tries < 5
    begin
      http.request(Net::HTTP::Get.new(uri.request_uri))
      rc_up = true
    rescue Errno::ECONNREFUSED
      sleep 5
    rescue Timeout::Error
    ensure
      tries += 1
    end
  end
  raise "Couldn't connect to #{uri.request_uri}" unless rc_up
  uri
end

#write_config(config) ⇒ Object



34
35
36
37
38
# File 'lib/tddium/config.rb', line 34

def write_config(config)
  File.open(get_config_paths[0], 'w', 0600) do |f|
    YAML.dump(config, f)
  end
end