testbot 0.4.6 → 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/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
|