testbot_instructure 0.7.8
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.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/CHANGELOG +264 -0
- data/Gemfile +3 -0
- data/README.markdown +141 -0
- data/Rakefile +35 -0
- data/bin/testbot +59 -0
- data/lib/generators/testbot/templates/testbot.rake.erb +35 -0
- data/lib/generators/testbot/templates/testbot.yml.erb +45 -0
- data/lib/generators/testbot/testbot_generator.rb +19 -0
- data/lib/railtie.rb +16 -0
- data/lib/requester/requester.rb +171 -0
- data/lib/runner/job.rb +112 -0
- data/lib/runner/runner.rb +222 -0
- data/lib/runner/safe_result_text.rb +29 -0
- data/lib/server/build.rb +36 -0
- data/lib/server/group.rb +48 -0
- data/lib/server/job.rb +64 -0
- data/lib/server/memory_model.rb +91 -0
- data/lib/server/runner.rb +47 -0
- data/lib/server/server.rb +103 -0
- data/lib/server/status/javascripts/jquery-1.4.4.min.js +167 -0
- data/lib/server/status/status.html +48 -0
- data/lib/server/status/stylesheets/status.css +14 -0
- data/lib/shared/adapters/adapter.rb +27 -0
- data/lib/shared/adapters/cucumber_adapter.rb +91 -0
- data/lib/shared/adapters/helpers/ruby_env.rb +47 -0
- data/lib/shared/adapters/rspec2_adapter.rb +61 -0
- data/lib/shared/adapters/rspec_adapter.rb +79 -0
- data/lib/shared/adapters/test_unit_adapter.rb +44 -0
- data/lib/shared/color.rb +16 -0
- data/lib/shared/simple_daemonize.rb +25 -0
- data/lib/shared/ssh_tunnel.rb +36 -0
- data/lib/shared/testbot.rb +132 -0
- data/lib/shared/version.rb +12 -0
- data/lib/tasks/testbot.rake +30 -0
- data/lib/testbot.rb +2 -0
- data/test/fixtures/local/Rakefile +7 -0
- data/test/fixtures/local/config/testbot.yml +5 -0
- data/test/fixtures/local/log/test.log +0 -0
- data/test/fixtures/local/script/spec +2 -0
- data/test/fixtures/local/spec/models/car_spec.rb +0 -0
- data/test/fixtures/local/spec/models/house_spec.rb +0 -0
- data/test/fixtures/local/spec/spec.opts +0 -0
- data/test/fixtures/local/tmp/restart.txt +0 -0
- data/test/integration_test.rb +55 -0
- data/test/requester/requester_test.rb +407 -0
- data/test/requester/testbot.yml +7 -0
- data/test/requester/testbot_with_erb.yml +2 -0
- data/test/runner/job_test.rb +94 -0
- data/test/runner/safe_result_text_test.rb +20 -0
- data/test/server/group_test.rb +43 -0
- data/test/server/server_test.rb +511 -0
- data/test/shared/adapters/adapter_test.rb +22 -0
- data/test/shared/adapters/cucumber_adapter_test.rb +72 -0
- data/test/shared/adapters/helpers/ruby_env_test.rb +108 -0
- data/test/shared/adapters/rspec_adapter_test.rb +109 -0
- data/test/shared/testbot_test.rb +185 -0
- data/testbot.gemspec +34 -0
- metadata +313 -0
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'httparty'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'erb'
|
5
|
+
require File.dirname(__FILE__) + '/../shared/ssh_tunnel'
|
6
|
+
require File.expand_path(File.dirname(__FILE__) + '/../shared/testbot')
|
7
|
+
|
8
|
+
class Hash
|
9
|
+
def symbolize_keys_without_active_support
|
10
|
+
inject({}) do |options, (key, value)|
|
11
|
+
options[(key.to_sym rescue key) || key] = value
|
12
|
+
options
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module Testbot::Requester
|
18
|
+
|
19
|
+
class Requester
|
20
|
+
|
21
|
+
attr_reader :config
|
22
|
+
|
23
|
+
def initialize(config = {})
|
24
|
+
config = config.symbolize_keys_without_active_support
|
25
|
+
config[:rsync_path] ||= Testbot::DEFAULT_SERVER_PATH
|
26
|
+
config[:project] ||= Testbot::DEFAULT_PROJECT
|
27
|
+
config[:server_user] ||= Testbot::DEFAULT_USER
|
28
|
+
config[:available_runner_usage] ||= Testbot::DEFAULT_RUNNER_USAGE
|
29
|
+
@config = OpenStruct.new(config)
|
30
|
+
end
|
31
|
+
|
32
|
+
def run_tests(adapter, dir)
|
33
|
+
puts if config.simple_output || config.logging
|
34
|
+
|
35
|
+
if config.ssh_tunnel
|
36
|
+
log "Setting up ssh tunnel" do
|
37
|
+
SSHTunnel.new(config.server_host, config.server_user, adapter.requester_port).open
|
38
|
+
end
|
39
|
+
server_uri = "http://127.0.0.1:#{adapter.requester_port}"
|
40
|
+
else
|
41
|
+
server_uri = "http://#{config.server_host}:#{Testbot::SERVER_PORT}"
|
42
|
+
end
|
43
|
+
|
44
|
+
log "Syncing files" do
|
45
|
+
rsync_ignores = config.rsync_ignores.to_s.split.map { |pattern| "--exclude='#{pattern}'" }.join(' ')
|
46
|
+
system("rsync -az --delete --delete-excluded -e ssh #{rsync_ignores} . #{rsync_uri}")
|
47
|
+
|
48
|
+
exitstatus = $?.exitstatus
|
49
|
+
unless exitstatus == 0
|
50
|
+
puts "rsync failed with exit code #{exitstatus}"
|
51
|
+
exit 1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
files = adapter.test_files(dir)
|
56
|
+
sizes = adapter.get_sizes(files)
|
57
|
+
|
58
|
+
build_id = nil
|
59
|
+
log "Requesting run" do
|
60
|
+
response = HTTParty.post("#{server_uri}/builds", :body => { :root => root,
|
61
|
+
:type => adapter.type.to_s,
|
62
|
+
:project => config.project,
|
63
|
+
:available_runner_usage => config.available_runner_usage,
|
64
|
+
:files => files.join(' '),
|
65
|
+
:sizes => sizes.join(' '),
|
66
|
+
:jruby => jruby? }).response
|
67
|
+
|
68
|
+
if response.code == "503"
|
69
|
+
puts "No runners available. If you just started a runner, try again. It usually takes a few seconds before they're available."
|
70
|
+
return false
|
71
|
+
elsif response.code != "200"
|
72
|
+
puts "Could not create build, #{response.code}: #{response.body}"
|
73
|
+
return false
|
74
|
+
else
|
75
|
+
build_id = response.body
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
at_exit do
|
80
|
+
unless ENV['IN_TEST'] || @done
|
81
|
+
log "Notifying server we want to stop the run" do
|
82
|
+
HTTParty.delete("#{server_uri}/builds/#{build_id}")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
puts if config.logging
|
88
|
+
|
89
|
+
last_results_size = 0
|
90
|
+
success = true
|
91
|
+
error_count = 0
|
92
|
+
while true
|
93
|
+
sleep 0.5
|
94
|
+
|
95
|
+
begin
|
96
|
+
@build = HTTParty.get("#{server_uri}/builds/#{build_id}", :format => :json)
|
97
|
+
next unless @build
|
98
|
+
rescue Exception => ex
|
99
|
+
error_count += 1
|
100
|
+
if error_count > 4
|
101
|
+
puts "Failed to get status: #{ex.message}"
|
102
|
+
error_count = 0
|
103
|
+
end
|
104
|
+
next
|
105
|
+
end
|
106
|
+
|
107
|
+
results = @build['results'][last_results_size..-1]
|
108
|
+
unless results == ''
|
109
|
+
if config.simple_output
|
110
|
+
print results.gsub(/[^\.F]|Finished/, '')
|
111
|
+
STDOUT.flush
|
112
|
+
else
|
113
|
+
print results
|
114
|
+
STDOUT.flush
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
last_results_size = @build['results'].size
|
119
|
+
|
120
|
+
break if @build['done']
|
121
|
+
end
|
122
|
+
|
123
|
+
puts if config.simple_output
|
124
|
+
|
125
|
+
if adapter.respond_to?(:sum_results)
|
126
|
+
puts "\n" + adapter.sum_results(@build['results'])
|
127
|
+
end
|
128
|
+
|
129
|
+
@done = true
|
130
|
+
@build["success"]
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.create_by_config(path)
|
134
|
+
Requester.new(YAML.load(ERB.new(File.open(path).read).result))
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def log(text)
|
140
|
+
if config.logging
|
141
|
+
print "#{text}... "; STDOUT.flush
|
142
|
+
yield
|
143
|
+
puts "done"
|
144
|
+
else
|
145
|
+
yield
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def root
|
150
|
+
if localhost?
|
151
|
+
config.rsync_path
|
152
|
+
else
|
153
|
+
"#{config.server_user}@#{config.server_host}:#{config.rsync_path}"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def rsync_uri
|
158
|
+
localhost? ? config.rsync_path : "#{config.server_user}@#{config.server_host}:#{config.rsync_path}"
|
159
|
+
end
|
160
|
+
|
161
|
+
def localhost?
|
162
|
+
[ '0.0.0.0', 'localhost', '127.0.0.1' ].include?(config.server_host)
|
163
|
+
end
|
164
|
+
|
165
|
+
def jruby?
|
166
|
+
RUBY_PLATFORM =~ /java/ || !!ENV['USE_JRUBY']
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
data/lib/runner/job.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'runner.rb'))
|
2
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'safe_result_text.rb'))
|
3
|
+
require 'posix/spawn'
|
4
|
+
|
5
|
+
module Testbot::Runner
|
6
|
+
class Job
|
7
|
+
attr_reader :root, :project, :build_id
|
8
|
+
|
9
|
+
TIME_TO_WAIT_BETWEEN_POSTING_RESULTS = 5
|
10
|
+
|
11
|
+
def initialize(runner, id, build_id, project, root, type, ruby_interpreter, files)
|
12
|
+
@runner, @id, @build_id, @project, @root, @type, @ruby_interpreter, @files =
|
13
|
+
runner, id, build_id, project, root, type, ruby_interpreter, files
|
14
|
+
@success = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def jruby?
|
18
|
+
@ruby_interpreter == 'jruby'
|
19
|
+
end
|
20
|
+
|
21
|
+
def run(instance)
|
22
|
+
return if @killed
|
23
|
+
puts "Running job #{@id} (build #{@build_id})... "
|
24
|
+
test_env_number = (instance == 0) ? '' : instance + 1
|
25
|
+
result = "\n#{`hostname`.chomp}:#{Dir.pwd}\n"
|
26
|
+
base_environment = "export RAILS_ENV=test; export TEST_ENV_NUMBER=#{test_env_number}; cd #{@project};"
|
27
|
+
|
28
|
+
adapter = Adapter.find(@type)
|
29
|
+
run_time = measure_run_time do
|
30
|
+
result += run_and_return_result("#{base_environment} #{adapter.command(@project, ruby_cmd, @files)}")
|
31
|
+
end
|
32
|
+
|
33
|
+
Server.put("/jobs/#{@id}", :body => { :result => SafeResultText.clean(result), :status => status, :time => run_time })
|
34
|
+
puts "Job #{@id} finished."
|
35
|
+
end
|
36
|
+
|
37
|
+
def kill!(build_id)
|
38
|
+
if @build_id == build_id && @pid
|
39
|
+
kill_processes
|
40
|
+
@killed = true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def kill_processes
|
47
|
+
# Kill process and its children (processes in the same group)
|
48
|
+
Process.kill('KILL', -@pid) rescue :failed_to_kill_process
|
49
|
+
end
|
50
|
+
|
51
|
+
def status
|
52
|
+
success? ? "successful" : "failed"
|
53
|
+
end
|
54
|
+
|
55
|
+
def measure_run_time
|
56
|
+
start_time = Time.now
|
57
|
+
yield
|
58
|
+
(Time.now - start_time) * 100
|
59
|
+
end
|
60
|
+
|
61
|
+
def post_results(output)
|
62
|
+
Server.put("/jobs/#{@id}", :body => { :result => SafeResultText.clean(output), :status => "building" })
|
63
|
+
rescue Timeout::Error
|
64
|
+
puts "Got a timeout when posting an job result update. This can happen when the server is busy and is not a critical error."
|
65
|
+
end
|
66
|
+
|
67
|
+
def run_and_return_result(command)
|
68
|
+
read_pipe = spawn_process(command)
|
69
|
+
|
70
|
+
output = ""
|
71
|
+
last_post_time = Time.now
|
72
|
+
while char = read_pipe.getc
|
73
|
+
char = (char.is_a?(Fixnum) ? char.chr : char) # 1.8 <-> 1.9
|
74
|
+
output << char
|
75
|
+
if Time.now - last_post_time > TIME_TO_WAIT_BETWEEN_POSTING_RESULTS
|
76
|
+
post_results(output)
|
77
|
+
last_post_time = Time.now
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Kill child processes, if any
|
82
|
+
kill_processes
|
83
|
+
|
84
|
+
output
|
85
|
+
end
|
86
|
+
|
87
|
+
def spawn_process(command)
|
88
|
+
read_pipe, write_pipe = IO.pipe
|
89
|
+
@pid = POSIX::Spawn::spawn(command, :err => write_pipe, :out => write_pipe, :pgroup => true)
|
90
|
+
|
91
|
+
Thread.new do
|
92
|
+
Process.waitpid(@pid)
|
93
|
+
@success = ($?.exitstatus == 0)
|
94
|
+
write_pipe.close
|
95
|
+
end
|
96
|
+
|
97
|
+
read_pipe
|
98
|
+
end
|
99
|
+
|
100
|
+
def success?
|
101
|
+
@success
|
102
|
+
end
|
103
|
+
|
104
|
+
def ruby_cmd
|
105
|
+
if @ruby_interpreter == 'jruby' && @runner.config.jruby_opts
|
106
|
+
'jruby ' + @runner.config.jruby_opts
|
107
|
+
else
|
108
|
+
@ruby_interpreter
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'httparty'
|
3
|
+
require 'ostruct'
|
4
|
+
require File.expand_path(File.dirname(__FILE__) + '/../shared/ssh_tunnel')
|
5
|
+
require File.expand_path(File.dirname(__FILE__) + '/../shared/adapters/adapter')
|
6
|
+
require File.expand_path(File.dirname(__FILE__) + '/job')
|
7
|
+
|
8
|
+
module Testbot::Runner
|
9
|
+
TIME_BETWEEN_NORMAL_POLLS = 1
|
10
|
+
TIME_BETWEEN_QUICK_POLLS = 0.1
|
11
|
+
TIME_BETWEEN_PINGS = 5
|
12
|
+
TIME_BETWEEN_VERSION_CHECKS = Testbot.version.include?('.DEV.') ? 10 : 60
|
13
|
+
|
14
|
+
class CPU
|
15
|
+
|
16
|
+
def self.count
|
17
|
+
case RUBY_PLATFORM
|
18
|
+
when /darwin/
|
19
|
+
`sysctl machdep.cpu.core_count | awk '{ print $2 }'`.to_i
|
20
|
+
when /linux/
|
21
|
+
`cat /proc/cpuinfo | grep processor | wc -l`.to_i
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
class Server
|
28
|
+
include HTTParty
|
29
|
+
default_timeout 10
|
30
|
+
end
|
31
|
+
|
32
|
+
class Runner
|
33
|
+
|
34
|
+
def initialize(config)
|
35
|
+
@instances = []
|
36
|
+
@last_build_id = nil
|
37
|
+
@last_version_check = Time.now - TIME_BETWEEN_VERSION_CHECKS - 1
|
38
|
+
@config = OpenStruct.new(config)
|
39
|
+
@config.max_instances = @config.max_instances ? @config.max_instances.to_i : CPU.count
|
40
|
+
|
41
|
+
if @config.ssh_tunnel
|
42
|
+
server_uri = "http://127.0.0.1:#{Testbot::SERVER_PORT}"
|
43
|
+
else
|
44
|
+
server_uri = "http://#{@config.server_host}:#{Testbot::SERVER_PORT}"
|
45
|
+
end
|
46
|
+
|
47
|
+
Server.base_uri(server_uri)
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_reader :config
|
51
|
+
|
52
|
+
def run!
|
53
|
+
# Remove legacy instance* and *_rsync|git style folders
|
54
|
+
Dir.entries(".").find_all { |name| name.include?('instance') || name.include?('_rsync') ||
|
55
|
+
name.include?('_git') }.each { |folder|
|
56
|
+
system "rm -rf #{folder}"
|
57
|
+
}
|
58
|
+
|
59
|
+
SSHTunnel.new(@config.server_host, @config.server_user || Testbot::DEFAULT_USER).open if @config.ssh_tunnel
|
60
|
+
while true
|
61
|
+
begin
|
62
|
+
update_uid!
|
63
|
+
start_ping
|
64
|
+
wait_for_jobs
|
65
|
+
rescue Exception => ex
|
66
|
+
break if [ 'SignalException', 'Interrupt' ].include?(ex.class.to_s)
|
67
|
+
puts "The runner crashed, restarting. Error: #{ex.inspect} #{ex.class}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def update_uid!
|
75
|
+
# When a runner crashes or is restarted it might loose current job info. Because
|
76
|
+
# of this we provide a new unique ID to the server so that it does not wait for
|
77
|
+
# lost jobs to complete.
|
78
|
+
@uid = "#{Time.now.to_i * rand}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def wait_for_jobs
|
82
|
+
last_check_found_a_job = false
|
83
|
+
loop do
|
84
|
+
sleep (last_check_found_a_job ? TIME_BETWEEN_QUICK_POLLS : TIME_BETWEEN_NORMAL_POLLS)
|
85
|
+
|
86
|
+
check_for_update if !last_check_found_a_job && time_for_update?
|
87
|
+
|
88
|
+
# Only get jobs from one build at a time
|
89
|
+
next_params = base_params
|
90
|
+
if @instances.size > 0
|
91
|
+
next_params.merge!({ :build_id => @last_build_id })
|
92
|
+
next_params.merge!({ :no_jruby => true }) if max_jruby_instances?
|
93
|
+
else
|
94
|
+
@last_build_id = nil
|
95
|
+
end
|
96
|
+
|
97
|
+
# Makes sure all instances are listed as available after a run
|
98
|
+
clear_completed_instances
|
99
|
+
|
100
|
+
next_job = Server.get("/jobs/next", :query => next_params) rescue nil
|
101
|
+
last_check_found_a_job = (next_job != nil && next_job.body != "")
|
102
|
+
next unless last_check_found_a_job
|
103
|
+
|
104
|
+
job = Job.new(*([ self, next_job.split(',') ].flatten))
|
105
|
+
if first_job_from_build?
|
106
|
+
fetch_code(job)
|
107
|
+
before_run(job)
|
108
|
+
end
|
109
|
+
|
110
|
+
@last_build_id = job.build_id
|
111
|
+
|
112
|
+
# Must be outside the thread or it will sometimes run
|
113
|
+
# multiple jobs using the same instance number.
|
114
|
+
instance_number = free_instance_number
|
115
|
+
|
116
|
+
@instances << [ Thread.new { job.run(instance_number) }, instance_number, job ]
|
117
|
+
|
118
|
+
loop do
|
119
|
+
clear_completed_instances
|
120
|
+
break unless max_instances_running?
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def max_jruby_instances?
|
126
|
+
return unless @config.max_jruby_instances
|
127
|
+
@instances.find_all { |thread, n, job| job.jruby? }.size >= @config.max_jruby_instances
|
128
|
+
end
|
129
|
+
|
130
|
+
def fetch_code(job)
|
131
|
+
system "rsync -az --delete --delete-excluded -e ssh #{job.root}/ #{job.project}"
|
132
|
+
end
|
133
|
+
|
134
|
+
def before_run(job)
|
135
|
+
rvm_prefix = RubyEnv.rvm_prefix(job.project)
|
136
|
+
bundler_cmd = (RubyEnv.bundler?(job.project) ? [rvm_prefix, "bundle &&", rvm_prefix, "bundle exec"] : [rvm_prefix]).compact.join(" ")
|
137
|
+
command_prefix = "cd #{job.project} && export RAILS_ENV=test && export TEST_INSTANCES=#{@config.max_instances} && #{bundler_cmd}"
|
138
|
+
|
139
|
+
if File.exists?("#{job.project}/lib/tasks/testbot.rake")
|
140
|
+
system "#{command_prefix} rake testbot:before_run"
|
141
|
+
elsif File.exists?("#{job.project}/config/testbot/before_run.rb")
|
142
|
+
system "#{command_prefix} ruby config/testbot/before_run.rb"
|
143
|
+
else
|
144
|
+
# workaround to bundle within the correct env
|
145
|
+
system "#{command_prefix} ruby -e ''"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def first_job_from_build?
|
150
|
+
@last_build_id == nil
|
151
|
+
end
|
152
|
+
|
153
|
+
def time_for_update?
|
154
|
+
time_for_update = ((Time.now - @last_version_check) >= TIME_BETWEEN_VERSION_CHECKS)
|
155
|
+
@last_version_check = Time.now if time_for_update
|
156
|
+
time_for_update
|
157
|
+
end
|
158
|
+
|
159
|
+
def check_for_update
|
160
|
+
return unless @config.auto_update
|
161
|
+
version = Server.get('/version') rescue Testbot.version
|
162
|
+
return unless version != Testbot.version
|
163
|
+
|
164
|
+
# In a PXE cluster with a shared gem folder we only want one of them to do the update
|
165
|
+
if @config.wait_for_updated_gem
|
166
|
+
# Gem.available? is cached so it won't detect new gems.
|
167
|
+
gem = Gem::Dependency.new("testbot", version)
|
168
|
+
successful_install = !Gem::SourceIndex.from_installed_gems.search(gem).empty?
|
169
|
+
else
|
170
|
+
if version.include?(".DEV.")
|
171
|
+
successful_install = system("wget #{@config.dev_gem_root}/testbot-#{version}.gem && gem install testbot-#{version}.gem --no-ri --no-rdoc && rm testbot-#{version}.gem")
|
172
|
+
else
|
173
|
+
successful_install = system "gem install testbot -v #{version} --no-ri --no-rdoc"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
system "testbot #{ARGV.join(' ')}" if successful_install
|
178
|
+
end
|
179
|
+
|
180
|
+
def ping_params
|
181
|
+
{ :hostname => (@hostname ||= `hostname`.chomp), :max_instances => @config.max_instances,
|
182
|
+
:idle_instances => (@config.max_instances - @instances.size), :username => ENV['USER'], :build_id => @last_build_id }.merge(base_params)
|
183
|
+
end
|
184
|
+
|
185
|
+
def base_params
|
186
|
+
{ :version => Testbot.version, :uid => @uid }
|
187
|
+
end
|
188
|
+
|
189
|
+
def max_instances_running?
|
190
|
+
@instances.size == @config.max_instances
|
191
|
+
end
|
192
|
+
|
193
|
+
def clear_completed_instances
|
194
|
+
@instances.each_with_index do |data, index|
|
195
|
+
@instances.delete_at(index) if data.first.join(0.25)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def free_instance_number
|
200
|
+
0.upto(@config.max_instances - 1) do |number|
|
201
|
+
return number unless @instances.find { |instance, n, job| n == number }
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def start_ping
|
206
|
+
Thread.new do
|
207
|
+
while true
|
208
|
+
begin
|
209
|
+
response = Server.get("/runners/ping", :body => ping_params).body
|
210
|
+
if response.include?('stop_build')
|
211
|
+
build_id = response.split(',').last
|
212
|
+
@instances.each { |instance, n, job| job.kill!(build_id) }
|
213
|
+
end
|
214
|
+
rescue
|
215
|
+
end
|
216
|
+
sleep TIME_BETWEEN_PINGS
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'iconv'
|
2
|
+
|
3
|
+
module Testbot::Runner
|
4
|
+
class SafeResultText
|
5
|
+
def self.clean(text)
|
6
|
+
clean_escape_sequences(strip_invalid_utf8(text))
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.strip_invalid_utf8(text)
|
10
|
+
# http://po-ru.com/diary/fixing-invalid-utf-8-in-ruby-revisited/
|
11
|
+
ic = Iconv.new('UTF-8//IGNORE', 'UTF-8')
|
12
|
+
ic.iconv(text + ' ')[0..-2]
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.clean_escape_sequences(text)
|
16
|
+
tail_marker = "^[[0m"
|
17
|
+
tail = text.rindex(tail_marker) && text[text.rindex(tail_marker)+tail_marker.length..-1]
|
18
|
+
if !tail
|
19
|
+
text
|
20
|
+
elsif tail.include?("^[[") && !tail.include?("m")
|
21
|
+
text[0..text.rindex(tail_marker) + tail_marker.length - 1]
|
22
|
+
elsif text.scan(/\[.*?m/).last != tail_marker
|
23
|
+
text[0..text.rindex(tail_marker) + tail_marker.length - 1]
|
24
|
+
else
|
25
|
+
text
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/server/build.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Testbot::Server
|
2
|
+
|
3
|
+
class Build < MemoryModel
|
4
|
+
|
5
|
+
def initialize(hash)
|
6
|
+
super({ :success => true, :done => false, :results => '' }.merge(hash))
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.create_and_build_jobs(hash)
|
10
|
+
hash["jruby"] = (hash["jruby"] == "true") ? 1 : 0
|
11
|
+
build = create(hash.reject { |k, v| k == 'available_runner_usage' })
|
12
|
+
build.create_jobs!(hash['available_runner_usage'])
|
13
|
+
build
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_jobs!(available_runner_usage)
|
17
|
+
groups = Group.build(self.files.split, self.sizes.split.map { |size| size.to_i },
|
18
|
+
Runner.total_instances.to_f * (available_runner_usage.to_i / 100.0), self.type)
|
19
|
+
groups.each do |group|
|
20
|
+
Job.create(:files => group.join(' '),
|
21
|
+
:root => self.root,
|
22
|
+
:project => self.project,
|
23
|
+
:type => self.type,
|
24
|
+
:build => self,
|
25
|
+
:jruby => self.jruby)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def destroy
|
30
|
+
Job.all.find_all { |j| j.build == self }.each { |job| job.destroy }
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
data/lib/server/group.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
module Testbot::Server
|
4
|
+
|
5
|
+
class Group
|
6
|
+
|
7
|
+
DEFAULT = nil
|
8
|
+
|
9
|
+
def self.build(files, sizes, instance_count, type)
|
10
|
+
tests_with_sizes = slow_tests_first(map_files_and_sizes(files, sizes))
|
11
|
+
|
12
|
+
groups = []
|
13
|
+
current_group, current_size = 0, 0
|
14
|
+
tests_with_sizes.each do |test, size|
|
15
|
+
# inserts into next group if current is full and we are not in the last group
|
16
|
+
if (0.5*size + current_size) > group_size(tests_with_sizes, instance_count) and instance_count > current_group + 1
|
17
|
+
current_size = size
|
18
|
+
current_group += 1
|
19
|
+
else
|
20
|
+
current_size += size
|
21
|
+
end
|
22
|
+
groups[current_group] ||= []
|
23
|
+
groups[current_group] << test
|
24
|
+
end
|
25
|
+
|
26
|
+
groups.compact
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def self.group_size(tests_with_sizes, group_count)
|
32
|
+
total = tests_with_sizes.inject(0) { |sum, test| sum += test[1] }
|
33
|
+
total / group_count.to_f
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.map_files_and_sizes(files, sizes)
|
37
|
+
list = []
|
38
|
+
files.each_with_index { |file, i| list << [ file, sizes[i] ] }
|
39
|
+
list
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.slow_tests_first(tests)
|
43
|
+
tests.sort_by { |test, time| time.to_i }.reverse
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/lib/server/job.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module Testbot::Server
|
2
|
+
|
3
|
+
class Job < MemoryModel
|
4
|
+
|
5
|
+
def update(hash)
|
6
|
+
super(hash)
|
7
|
+
if self.build
|
8
|
+
self.done = done?
|
9
|
+
done = !Job.all.find { |j| !j.done && j.build == self.build }
|
10
|
+
self.build.update(:results => build_results(build), :done => done)
|
11
|
+
|
12
|
+
build_broken_by_job = (self.status == "failed" && build.success)
|
13
|
+
self.build.update(:success => false) if build_broken_by_job
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.next(params, remove_addr)
|
18
|
+
clean_params = params.reject { |k, v| k == "no_jruby" }
|
19
|
+
runner = Runner.record! clean_params.merge({ :ip => remove_addr, :last_seen_at => Time.now })
|
20
|
+
return unless Server.valid_version?(params[:version])
|
21
|
+
[ next_job(params["build_id"], params["no_jruby"]), runner ]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def build_results(build)
|
27
|
+
self.last_result_position ||= 0
|
28
|
+
new_results = self.result.to_s[self.last_result_position..-1] || ""
|
29
|
+
self.last_result_position = self.result.to_s.size
|
30
|
+
|
31
|
+
# Don't know why this is needed as the job should cleanup
|
32
|
+
# escape sequences.
|
33
|
+
if new_results[0,4] == '[32m'
|
34
|
+
new_results = new_results[4..-1]
|
35
|
+
end
|
36
|
+
|
37
|
+
build.results.to_s + new_results
|
38
|
+
end
|
39
|
+
|
40
|
+
def done?
|
41
|
+
self.status == "successful" || self.status == "failed"
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.next_job(build_id, no_jruby)
|
45
|
+
release_jobs_taken_by_missing_runners!
|
46
|
+
jobs = Job.all.find_all { |j|
|
47
|
+
!j.taken_at &&
|
48
|
+
(build_id ? j.build.id.to_s == build_id : true) &&
|
49
|
+
(no_jruby ? j.jruby != 1 : true)
|
50
|
+
}
|
51
|
+
|
52
|
+
jobs[rand(jobs.size)]
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.release_jobs_taken_by_missing_runners!
|
56
|
+
missing_runners = Runner.all.find_all { |r| r.last_seen_at < (Time.now - Runner.timeout) }
|
57
|
+
missing_runners.each { |runner|
|
58
|
+
Job.all.find_all { |job| job.taken_by == runner }.each { |job| job.update(:taken_at => nil) }
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|