testbot 0.4.6 → 0.4.7
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +6 -0
- data/README.markdown +3 -1
- data/bin/testbot +1 -1
- data/lib/generators/testbot/testbot_generator.rb +1 -1
- data/lib/railtie.rb +0 -2
- data/lib/requester/requester.rb +134 -0
- data/lib/runner/job.rb +52 -33
- data/lib/runner/runner.rb +215 -0
- data/lib/server/build.rb +29 -25
- data/lib/server/db.rb +42 -39
- data/lib/server/group.rb +42 -38
- data/lib/server/job.rb +44 -40
- data/lib/server/runner.rb +37 -33
- data/lib/server/server.rb +74 -0
- data/lib/{adapters → shared/adapters}/adapter.rb +0 -0
- data/lib/{adapters → shared/adapters}/cucumber_adapter.rb +0 -0
- data/lib/{adapters → shared/adapters}/helpers/ruby_env.rb +0 -0
- data/lib/{adapters → shared/adapters}/rspec_adapter.rb +0 -0
- data/lib/{adapters → shared/adapters}/test_unit_adapter.rb +0 -0
- data/lib/shared/testbot.rb +141 -0
- data/lib/tasks/testbot.rake +3 -3
- data/lib/testbot.rb +2 -146
- data/testbot.gemspec +3 -7
- metadata +13 -30
- data/lib/new_runner.rb +0 -30
- data/lib/requester.rb +0 -130
- data/lib/runner.rb +0 -213
- data/lib/server.rb +0 -72
data/CHANGELOG
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
0.4.7
|
2
|
+
|
3
|
+
- Refactored the code into modules with one directory for each.
|
4
|
+
- Added a notice about our IRC channel, #testbot on freenode.
|
5
|
+
- No longer dependent on mongel, now using webrat.
|
6
|
+
|
1
7
|
0.4.6
|
2
8
|
|
3
9
|
Fixed a bug that caused auto_update not to check for an update after a job had been run.
|
data/README.markdown
CHANGED
@@ -2,6 +2,7 @@ Testbot is a test distribution tool that works with Rails, RSpec, Test::Unit and
|
|
2
2
|
|
3
3
|
Using testbot on 11 machines (25 cores) we got our test suite down to **2 minutes from 30**. [More examples of how testbot is used](http://github.com/joakimk/testbot/wiki/How-testbot-is-being-used).
|
4
4
|
|
5
|
+
|
5
6
|
Installing
|
6
7
|
----
|
7
8
|
|
@@ -68,7 +69,7 @@ Using testbot with Rails 3:
|
|
68
69
|
|
69
70
|
Using testbot with Rails 2:
|
70
71
|
|
71
|
-
ruby script/plugin install git://github.com/joakimk/testbot.git -r 'refs/tags/v0.4.
|
72
|
+
ruby script/plugin install git://github.com/joakimk/testbot.git -r 'refs/tags/v0.4.7'
|
72
73
|
script/generate testbot --connect 192.168.0.100
|
73
74
|
|
74
75
|
rake testbot:spec (or :test, :features)
|
@@ -123,4 +124,5 @@ Add a **lib/adapters/framework_name_adapter.rb** file, update **lib/adapters/ada
|
|
123
124
|
More
|
124
125
|
----
|
125
126
|
|
127
|
+
* IRC channel: #testbot (freenode)
|
126
128
|
* Check the [wiki](http://github.com/joakimk/testbot/wiki) for more info.
|
data/bin/testbot
CHANGED
data/lib/railtie.rb
CHANGED
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'httparty'
|
3
|
+
require 'macaddr'
|
4
|
+
require 'ostruct'
|
5
|
+
require File.dirname(__FILE__) + '/../shared/ssh_tunnel'
|
6
|
+
require File.dirname(__FILE__) + '/../shared/adapters/adapter'
|
7
|
+
require File.expand_path(File.dirname(__FILE__) + '/../shared/testbot')
|
8
|
+
|
9
|
+
class Hash
|
10
|
+
def symbolize_keys_without_active_support
|
11
|
+
inject({}) do |options, (key, value)|
|
12
|
+
options[(key.to_sym rescue key) || key] = value
|
13
|
+
options
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Testbot::Requester
|
19
|
+
|
20
|
+
class Requester
|
21
|
+
|
22
|
+
attr_reader :config
|
23
|
+
|
24
|
+
def initialize(config = {})
|
25
|
+
config = config.symbolize_keys_without_active_support
|
26
|
+
config[:rsync_path] ||= Testbot::DEFAULT_SERVER_PATH
|
27
|
+
config[:project] ||= Testbot::DEFAULT_PROJECT
|
28
|
+
config[:server_user] ||= Testbot::DEFAULT_USER
|
29
|
+
config[:available_runner_usage] ||= Testbot::DEFAULT_RUNNER_USAGE
|
30
|
+
@config = OpenStruct.new(config)
|
31
|
+
end
|
32
|
+
|
33
|
+
def run_tests(adapter, dir)
|
34
|
+
puts if config.simple_output
|
35
|
+
|
36
|
+
if config.ssh_tunnel
|
37
|
+
SSHTunnel.new(config.server_host, config.server_user, adapter.requester_port).open
|
38
|
+
server_uri = "http://127.0.0.1:#{adapter.requester_port}"
|
39
|
+
else
|
40
|
+
server_uri = "http://#{config.server_host}:#{Testbot::SERVER_PORT}"
|
41
|
+
end
|
42
|
+
|
43
|
+
rsync_ignores = config.rsync_ignores.to_s.split.map { |pattern| "--exclude='#{pattern}'" }.join(' ')
|
44
|
+
system "rsync -az --delete -e ssh #{rsync_ignores} . #{rsync_uri}"
|
45
|
+
|
46
|
+
files = adapter.test_files(dir)
|
47
|
+
sizes = adapter.get_sizes(files)
|
48
|
+
|
49
|
+
build_id = HTTParty.post("#{server_uri}/builds", :body => { :root => root,
|
50
|
+
:type => adapter.type.to_s,
|
51
|
+
:project => config.project,
|
52
|
+
:requester_mac => Mac.addr,
|
53
|
+
:available_runner_usage => config.available_runner_usage,
|
54
|
+
:files => files.join(' '),
|
55
|
+
:sizes => sizes.join(' '),
|
56
|
+
:jruby => jruby? })
|
57
|
+
|
58
|
+
|
59
|
+
last_results_size = 0
|
60
|
+
success = true
|
61
|
+
error_count = 0
|
62
|
+
while true
|
63
|
+
sleep 1
|
64
|
+
|
65
|
+
begin
|
66
|
+
@build = HTTParty.get("#{server_uri}/builds/#{build_id}", :format => :json)
|
67
|
+
next unless @build
|
68
|
+
rescue Exception => ex
|
69
|
+
error_count += 1
|
70
|
+
if error_count > 4
|
71
|
+
puts "Failed to get status: #{ex.message}"
|
72
|
+
error_count = 0
|
73
|
+
end
|
74
|
+
next
|
75
|
+
end
|
76
|
+
|
77
|
+
results = @build['results'][last_results_size..-1]
|
78
|
+
unless results == ''
|
79
|
+
if config.simple_output
|
80
|
+
print results.gsub(/[^\.F]|Finished/, '')
|
81
|
+
STDOUT.flush
|
82
|
+
else
|
83
|
+
puts results
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
last_results_size = @build['results'].size
|
88
|
+
|
89
|
+
break if @build['done']
|
90
|
+
end
|
91
|
+
|
92
|
+
puts if config.simple_output
|
93
|
+
|
94
|
+
@build["success"]
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.create_by_config(path)
|
98
|
+
config = YAML.load_file(path)
|
99
|
+
Requester.new(config)
|
100
|
+
end
|
101
|
+
|
102
|
+
def result_lines
|
103
|
+
@build['results'].split("\n").find_all { |line| line_is_result?(line) }.map { |line| line.chomp }
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def root
|
109
|
+
if localhost?
|
110
|
+
config.rsync_path
|
111
|
+
else
|
112
|
+
"#{config.server_user}@#{config.server_host}:#{config.rsync_path}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def rsync_uri
|
117
|
+
localhost? ? config.rsync_path : "#{config.server_user}@#{config.server_host}:#{config.rsync_path}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def localhost?
|
121
|
+
[ '0.0.0.0', 'localhost', '127.0.0.1' ].include?(config.server_host)
|
122
|
+
end
|
123
|
+
|
124
|
+
def line_is_result?(line)
|
125
|
+
line =~ /\d+ fail/
|
126
|
+
end
|
127
|
+
|
128
|
+
def jruby?
|
129
|
+
RUBY_PLATFORM =~ /java/ || !!ENV['USE_JRUBY']
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
data/lib/runner/job.rb
CHANGED
@@ -1,36 +1,55 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'runner.rb'))
|
2
|
+
|
3
|
+
module Testbot::Runner
|
4
|
+
class Job
|
5
|
+
attr_reader :root, :project, :requester_mac
|
6
|
+
|
7
|
+
def initialize(runner, id, requester_mac, project, root, type, ruby_interpreter, files)
|
8
|
+
@runner, @id, @requester_mac, @project, @root, @type, @ruby_interpreter, @files =
|
9
|
+
runner, id, requester_mac, project, root, type, ruby_interpreter, files
|
10
|
+
end
|
11
|
+
|
12
|
+
def jruby?
|
13
|
+
@ruby_interpreter == 'jruby'
|
14
|
+
end
|
15
|
+
|
16
|
+
def run(instance)
|
17
|
+
puts "Running job #{@id} from #{@requester_mac}... "
|
18
|
+
test_env_number = (instance == 0) ? '' : instance + 1
|
19
|
+
result = "\n#{`hostname`.chomp}:#{Dir.pwd}\n"
|
20
|
+
base_environment = "export RAILS_ENV=test; export TEST_ENV_NUMBER=#{test_env_number}; cd #{@project};"
|
21
|
+
|
22
|
+
adapter = Adapter.find(@type)
|
23
|
+
run_time = measure_run_time do
|
24
|
+
result += run_and_return_result("#{base_environment} #{adapter.command(@project, ruby_cmd, @files)}")
|
25
|
+
end
|
26
|
+
|
27
|
+
Server.put("/jobs/#{@id}", :body => { :result => result, :success => success?, :time => run_time })
|
28
|
+
puts "Job #{@id} finished."
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def measure_run_time
|
34
|
+
start_time = Time.now
|
35
|
+
yield
|
36
|
+
Time.now - start_time
|
37
|
+
end
|
38
|
+
|
39
|
+
def run_and_return_result(command)
|
40
|
+
`#{command} 2>&1`
|
41
|
+
end
|
42
|
+
|
43
|
+
def success?
|
44
|
+
$?.exitstatus == 0
|
45
|
+
end
|
46
|
+
|
47
|
+
def ruby_cmd
|
48
|
+
if @ruby_interpreter == 'jruby' && @runner.config.jruby_opts
|
49
|
+
'jruby ' + @runner.config.jruby_opts
|
50
|
+
else
|
51
|
+
@ruby_interpreter
|
52
|
+
end
|
33
53
|
end
|
34
54
|
end
|
35
55
|
end
|
36
|
-
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'httparty'
|
3
|
+
require 'macaddr'
|
4
|
+
require 'ostruct'
|
5
|
+
require File.expand_path(File.dirname(__FILE__) + '/../shared/ssh_tunnel')
|
6
|
+
require File.expand_path(File.dirname(__FILE__) + '/../shared/adapters/adapter')
|
7
|
+
require File.expand_path(File.dirname(__FILE__) + '/job')
|
8
|
+
|
9
|
+
module Testbot::Runner
|
10
|
+
TIME_BETWEEN_NORMAL_POLLS = 1
|
11
|
+
TIME_BETWEEN_QUICK_POLLS = 0.1
|
12
|
+
TIME_BETWEEN_PINGS = 5
|
13
|
+
TIME_BETWEEN_VERSION_CHECKS = Testbot.version.include?('.DEV.') ? 10 : 60
|
14
|
+
MAX_CPU_USAGE_WHEN_IDLE = 50
|
15
|
+
|
16
|
+
class CPU
|
17
|
+
|
18
|
+
def self.current_usage
|
19
|
+
process_usages = `ps -eo pcpu`
|
20
|
+
total_usage = process_usages.split("\n").inject(0) { |sum, usage| sum += usage.strip.to_f }
|
21
|
+
(total_usage / count).to_i
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.count
|
25
|
+
case RUBY_PLATFORM
|
26
|
+
when /darwin/
|
27
|
+
`hwprefs cpu_count`.to_i
|
28
|
+
when /linux/
|
29
|
+
`cat /proc/cpuinfo | grep processor | wc -l`.to_i
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class Server
|
36
|
+
include HTTParty
|
37
|
+
end
|
38
|
+
|
39
|
+
class Runner
|
40
|
+
|
41
|
+
def initialize(config)
|
42
|
+
@instances = []
|
43
|
+
@last_requester_mac = nil
|
44
|
+
@last_version_check = Time.now - TIME_BETWEEN_VERSION_CHECKS - 1
|
45
|
+
@config = OpenStruct.new(config)
|
46
|
+
@config.max_instances = @config.max_instances ? @config.max_instances.to_i : CPU.count
|
47
|
+
|
48
|
+
if @config.ssh_tunnel
|
49
|
+
server_uri = "http://127.0.0.1:#{Testbot::SERVER_PORT}"
|
50
|
+
else
|
51
|
+
server_uri = "http://#{@config.server_host}:#{Testbot::SERVER_PORT}"
|
52
|
+
end
|
53
|
+
|
54
|
+
Server.base_uri(server_uri)
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :config
|
58
|
+
|
59
|
+
def run!
|
60
|
+
# Remove legacy instance* and *_rsync|git style folders
|
61
|
+
Dir.entries(".").find_all { |name| name.include?('instance') || name.include?('_rsync') ||
|
62
|
+
name.include?('_git') }.each { |folder|
|
63
|
+
system "rm -rf #{folder}"
|
64
|
+
}
|
65
|
+
|
66
|
+
SSHTunnel.new(@config.server_host, @config.server_user || Testbot::DEFAULT_USER).open if @config.ssh_tunnel
|
67
|
+
while true
|
68
|
+
begin
|
69
|
+
update_uid!
|
70
|
+
start_ping
|
71
|
+
wait_for_jobs
|
72
|
+
rescue Exception => ex
|
73
|
+
break if [ 'SignalException', 'Interrupt' ].include?(ex.class.to_s)
|
74
|
+
puts "The runner crashed, restarting. Error: #{ex.inspect} #{ex.class}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def update_uid!
|
82
|
+
# When a runner crashes or is restarted it might loose current job info. Because
|
83
|
+
# of this we provide a new unique ID to the server so that it does not wait for
|
84
|
+
# lost jobs to complete.
|
85
|
+
@uid = "#{Time.now.to_i}@#{Mac.addr}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def wait_for_jobs
|
89
|
+
last_check_found_a_job = false
|
90
|
+
loop do
|
91
|
+
sleep (last_check_found_a_job ? TIME_BETWEEN_QUICK_POLLS : TIME_BETWEEN_NORMAL_POLLS)
|
92
|
+
|
93
|
+
check_for_update if !last_check_found_a_job && time_for_update?
|
94
|
+
|
95
|
+
# Only get jobs from one requester at a time
|
96
|
+
next_params = base_params
|
97
|
+
if @instances.size > 0
|
98
|
+
next_params.merge!({ :requester_mac => @last_requester_mac })
|
99
|
+
next_params.merge!({ :no_jruby => true }) if max_jruby_instances?
|
100
|
+
else
|
101
|
+
@last_requester_mac = nil
|
102
|
+
end
|
103
|
+
|
104
|
+
# Makes sure all instances are listed as available after a run
|
105
|
+
clear_completed_instances
|
106
|
+
next unless cpu_available?
|
107
|
+
|
108
|
+
next_job = Server.get("/jobs/next", :query => next_params) rescue nil
|
109
|
+
last_check_found_a_job = (next_job != nil)
|
110
|
+
next unless last_check_found_a_job
|
111
|
+
|
112
|
+
job = Job.new(*([ self, next_job.split(',') ].flatten))
|
113
|
+
if first_job_from_requester?
|
114
|
+
fetch_code(job)
|
115
|
+
before_run(job) if File.exists?("#{job.project}/lib/tasks/testbot.rake")
|
116
|
+
end
|
117
|
+
|
118
|
+
@instances << [ Thread.new { job.run(free_instance_number) },
|
119
|
+
free_instance_number, job ]
|
120
|
+
@last_requester_mac = job.requester_mac
|
121
|
+
loop do
|
122
|
+
clear_completed_instances
|
123
|
+
break unless max_instances_running?
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def max_jruby_instances?
|
129
|
+
return unless @config.max_jruby_instances
|
130
|
+
@instances.find_all { |thread, n, job| job.jruby? }.size >= @config.max_jruby_instances
|
131
|
+
end
|
132
|
+
|
133
|
+
def fetch_code(job)
|
134
|
+
system "rsync -az --delete -e ssh #{job.root}/ #{job.project}"
|
135
|
+
end
|
136
|
+
|
137
|
+
def before_run(job)
|
138
|
+
bundler_cmd = RubyEnv.bundler?(job.project) ? "bundle; " : ""
|
139
|
+
system "export RAILS_ENV=test; export TEST_INSTANCES=#{@config.max_instances}; cd #{job.project}; #{bundler_cmd} rake testbot:before_run"
|
140
|
+
end
|
141
|
+
|
142
|
+
def first_job_from_requester?
|
143
|
+
@last_requester_mac == nil
|
144
|
+
end
|
145
|
+
|
146
|
+
def cpu_available?
|
147
|
+
@instances.size > 0 || CPU.current_usage < MAX_CPU_USAGE_WHEN_IDLE
|
148
|
+
end
|
149
|
+
|
150
|
+
def time_for_update?
|
151
|
+
time_for_update = ((Time.now - @last_version_check) >= TIME_BETWEEN_VERSION_CHECKS)
|
152
|
+
@last_version_check = Time.now if time_for_update
|
153
|
+
time_for_update
|
154
|
+
end
|
155
|
+
|
156
|
+
def check_for_update
|
157
|
+
return unless @config.auto_update
|
158
|
+
version = Server.get('/version') rescue Testbot.version
|
159
|
+
return unless version != Testbot.version
|
160
|
+
|
161
|
+
# In a PXE cluster with a shared gem folder we only want one of them to do the update
|
162
|
+
if @config.wait_for_updated_gem
|
163
|
+
# Gem.available? is cached so it won't detect new gems.
|
164
|
+
gem = Gem::Dependency.new("testbot", version)
|
165
|
+
successful_install = !Gem::SourceIndex.from_installed_gems.search(gem).empty?
|
166
|
+
else
|
167
|
+
if version.include?(".DEV.")
|
168
|
+
successful_install = system("wget #{@config.dev_gem_root}/testbot-#{version}.gem && gem install testbot-#{version}.gem --no-ri --no-rdoc && rm testbot-#{version}.gem")
|
169
|
+
else
|
170
|
+
successful_install = system "gem install testbot -v #{version} --no-ri --no-rdoc"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
system "testbot #{ARGV.join(' ')}" if successful_install
|
175
|
+
end
|
176
|
+
|
177
|
+
def ping_params
|
178
|
+
{ :hostname => (@hostname ||= `hostname`.chomp), :max_instances => @config.max_instances,
|
179
|
+
:idle_instances => (@config.max_instances - @instances.size), :username => ENV['USER'] }.merge(base_params)
|
180
|
+
end
|
181
|
+
|
182
|
+
def base_params
|
183
|
+
{ :version => Testbot.version, :uid => @uid }
|
184
|
+
end
|
185
|
+
|
186
|
+
def max_instances_running?
|
187
|
+
@instances.size == @config.max_instances
|
188
|
+
end
|
189
|
+
|
190
|
+
def clear_completed_instances
|
191
|
+
@instances.each_with_index do |data, index|
|
192
|
+
@instances.delete_at(index) if data.first.join(0.25)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def free_instance_number
|
197
|
+
0.upto(@config.max_instances - 1) do |number|
|
198
|
+
return number unless @instances.find { |instance, n, job| n == number }
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def start_ping
|
203
|
+
Thread.new do
|
204
|
+
while true
|
205
|
+
begin
|
206
|
+
Server.get("/runners/ping", :body => ping_params)
|
207
|
+
rescue
|
208
|
+
end
|
209
|
+
sleep TIME_BETWEEN_PINGS
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
end
|