testbot 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ class TestbotGenerator < Rails::Generators::Base
2
+ source_root File.expand_path('../templates', __FILE__)
3
+
4
+ class_option :connect, :type => :string, :required => true, :desc => "Which server to use (required)"
5
+ class_option :project, :type => :string, :default => nil, :desc => "The name of your project (default: #{Testbot::DEFAULT_PROJECT})"
6
+ class_option :rsync_path, :type => :string, :default => nil, :desc => "Sync path on the server (default: #{Testbot::DEFAULT_SERVER_PATH})"
7
+ class_option :rsync_ignores, :type => :string, :default => nil, :desc => "Files to rsync_ignores when syncing (default: include all)"
8
+ class_option :ssh_tunnel, :type => :boolean, :default => nil, :desc => "Use a ssh tunnel"
9
+ class_option :user, :type => :string, :default => nil, :desc => "Use a custom rsync / ssh tunnel user (default: #{Testbot::DEFAULT_USER})"
10
+
11
+ def generate_config
12
+ template "testbot.yml.erb", "config/testbot.yml"
13
+ template "testbot.rake.erb", "lib/tasks/testbot.rake"
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require 'httparty'
3
+
4
+ class Server
5
+ include HTTParty
6
+ end
7
+
8
+ class NewRunner
9
+
10
+ def self.run_jobs
11
+ next_job = Server.get('/jobs/next') rescue nil
12
+ return unless next_job
13
+ id, root, specs = next_job.split(',')
14
+ result = run_and_return_results("export RAILS_ENV=test; export RSPEC_COLOR=true; rake testbot:before_run; script/spec -O spec/spec.opts #{specs}")
15
+ Server.put("/jobs/#{id}", :body => { :result => result })
16
+ end
17
+
18
+ def self.load_config
19
+ host = YAML.load_file("#{ENV['HOME']}/.testbot_runner.yml")[:server_host]
20
+ Server.base_uri "http://#{host}:2288"
21
+ end
22
+
23
+ private
24
+
25
+ # I'd really like to know how to do this better and more testable
26
+ def self.run_and_return_results(cmd)
27
+ `#{cmd} 2>&1`
28
+ end
29
+
30
+ end
@@ -0,0 +1,10 @@
1
+ require 'testbot'
2
+ require 'rails'
3
+
4
+ module Testbot
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ load File.expand_path(File.join(File.dirname(__FILE__), "tasks/testbot.rake"))
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,147 @@
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__) + '/adapters/adapter'
7
+ require File.expand_path(File.dirname(__FILE__) + '/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
+ class Requester
19
+
20
+ attr_reader :config
21
+
22
+ def initialize(config = {})
23
+ config = config.symbolize_keys_without_active_support
24
+ config[:rsync_path] ||= Testbot::DEFAULT_SERVER_PATH
25
+ config[:project] ||= Testbot::DEFAULT_PROJECT
26
+ config[:server_user] ||= Testbot::DEFAULT_USER
27
+ config[:available_runner_usage] ||= Testbot::DEFAULT_RUNNER_USAGE
28
+ @config = OpenStruct.new(config)
29
+ end
30
+
31
+ def run_tests(adapter, dir)
32
+ puts if config.simple_output
33
+
34
+ if config.ssh_tunnel
35
+ SSHTunnel.new(config.server_host, config.server_user, adapter.requester_port).open
36
+ server_uri = "http://127.0.0.1:#{adapter.requester_port}"
37
+ else
38
+ server_uri = "http://#{config.server_host}:#{Testbot::SERVER_PORT}"
39
+ end
40
+
41
+ rsync_ignores = config.rsync_ignores.to_s.split.map { |pattern| "--exclude='#{pattern}'" }.join(' ')
42
+ system "rsync -az --delete -e ssh #{rsync_ignores} . #{rsync_uri}"
43
+
44
+ files = find_tests(adapter, dir)
45
+ sizes = find_sizes(files)
46
+
47
+ build_id = HTTParty.post("#{server_uri}/builds", :body => { :root => root,
48
+ :type => adapter.type.to_s,
49
+ :project => config.project,
50
+ :requester_mac => Mac.addr,
51
+ :available_runner_usage => config.available_runner_usage,
52
+ :files => files.join(' '),
53
+ :sizes => sizes.join(' '),
54
+ :jruby => jruby? })
55
+
56
+
57
+ last_results_size = 0
58
+ success = true
59
+ error_count = 0
60
+ while true
61
+ sleep 1
62
+
63
+ begin
64
+ @build = HTTParty.get("#{server_uri}/builds/#{build_id}", :format => :json)
65
+ next unless @build
66
+ rescue Exception => ex
67
+ error_count += 1
68
+ if error_count > 4
69
+ puts "Failed to get status: #{ex.message}"
70
+ error_count = 0
71
+ end
72
+ next
73
+ end
74
+
75
+ results = @build['results'][last_results_size..-1]
76
+ unless results == ''
77
+ if config.simple_output
78
+ print results.gsub(/[^\.F]|Finished/, '')
79
+ STDOUT.flush
80
+ else
81
+ puts results
82
+ end
83
+ end
84
+
85
+ last_results_size = @build['results'].size
86
+
87
+ success = false if failed_build?(@build)
88
+ break if @build['done']
89
+ end
90
+
91
+ puts if config.simple_output
92
+
93
+ success
94
+ end
95
+
96
+ def self.create_by_config(path)
97
+ config = YAML.load_file(path)
98
+ Requester.new(config)
99
+ end
100
+
101
+ def result_lines
102
+ @build['results'].split("\n").find_all { |line| line_is_result?(line) }.map { |line| line.chomp }
103
+ end
104
+
105
+ private
106
+
107
+ def root
108
+ if localhost?
109
+ config.rsync_path
110
+ else
111
+ "#{config.server_user}@#{config.server_host}:#{config.rsync_path}"
112
+ end
113
+ end
114
+
115
+ def rsync_uri
116
+ localhost? ? config.rsync_path : "#{config.server_user}@#{config.server_host}:#{config.rsync_path}"
117
+ end
118
+
119
+ def localhost?
120
+ [ '0.0.0.0', 'localhost', '127.0.0.1' ].include?(config.server_host)
121
+ end
122
+
123
+ def failed_build?(build)
124
+ result_lines.any? { |line| line_is_failure?(line) }
125
+ end
126
+
127
+ def line_is_result?(line)
128
+ line =~ /\d+ fail/
129
+ end
130
+
131
+ def line_is_failure?(line)
132
+ line =~ /(\d{2,}|[1-9]) (fail|error)/
133
+ end
134
+
135
+ def find_tests(adapter, dir)
136
+ Dir["#{dir}/#{adapter.file_pattern}"]
137
+ end
138
+
139
+ def find_sizes(files)
140
+ files.map { |file| File.stat(file).size }
141
+ end
142
+
143
+ def jruby?
144
+ RUBY_PLATFORM =~ /java/ || !!ENV['USE_JRUBY']
145
+ end
146
+
147
+ end
@@ -0,0 +1,233 @@
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__) + '/adapters/adapter'
7
+
8
+ TIME_BETWEEN_POLLS = 1
9
+ TIME_BETWEEN_PINGS = 5
10
+ TIME_BETWEEN_VERSION_CHECKS = 60
11
+ MAX_CPU_USAGE_WHEN_IDLE = 50
12
+
13
+ class CPU
14
+
15
+ def self.current_usage
16
+ process_usages = `ps -eo pcpu`
17
+ total_usage = process_usages.split("\n").inject(0) { |sum, usage| sum += usage.strip.to_f }
18
+ (total_usage / count).to_i
19
+ end
20
+
21
+ def self.count
22
+ case RUBY_PLATFORM
23
+ when /darwin/
24
+ `hwprefs cpu_count`.to_i
25
+ when /linux/
26
+ `cat /proc/cpuinfo | grep processor | wc -l`.to_i
27
+ end
28
+ end
29
+
30
+ end
31
+
32
+ class Job
33
+ attr_reader :root, :project, :requester_mac
34
+
35
+ def initialize(runner, id, requester_mac, project, root, type, ruby_interpreter, files)
36
+ @runner, @id, @requester_mac, @project, @root, @type, @ruby_interpreter, @files =
37
+ runner, id, requester_mac, project, root, type, ruby_interpreter, files
38
+ end
39
+
40
+ def jruby?
41
+ @ruby_interpreter == 'jruby'
42
+ end
43
+
44
+ def run(instance)
45
+ puts "Running job #{@id} from #{@requester_mac}... "
46
+ test_env_number = (instance == 0) ? '' : instance + 1
47
+ result = "\n#{`hostname`.chomp}:#{Dir.pwd}\n"
48
+ base_environment = "export RAILS_ENV=test; export TEST_ENV_NUMBER=#{test_env_number}; cd #{@project};"
49
+
50
+ adapter = Adapter.find(@type)
51
+ result += `#{base_environment} #{adapter.command(ruby_cmd, @files)} 2>&1`
52
+
53
+ Server.put("/jobs/#{@id}", :body => { :result => result })
54
+ puts "Job #{@id} finished."
55
+ end
56
+
57
+ private
58
+
59
+ def ruby_cmd
60
+ if @ruby_interpreter == 'jruby' && @runner.config.jruby_opts
61
+ 'jruby ' + @runner.config.jruby_opts
62
+ else
63
+ @ruby_interpreter
64
+ end
65
+ end
66
+ end
67
+
68
+ class Server
69
+ include HTTParty
70
+ end
71
+
72
+ class Runner
73
+
74
+ def initialize(config)
75
+ @instances = []
76
+ @last_requester_mac = nil
77
+ @last_version_check = Time.now - TIME_BETWEEN_VERSION_CHECKS - 1
78
+ @config = OpenStruct.new(config)
79
+ @config.max_instances = @config.max_instances ? @config.max_instances.to_i : CPU.count
80
+
81
+ if @config.ssh_tunnel
82
+ server_uri = "http://127.0.0.1:#{Testbot::SERVER_PORT}"
83
+ else
84
+ server_uri = "http://#{@config.server_host}:#{Testbot::SERVER_PORT}"
85
+ end
86
+
87
+ Server.base_uri(server_uri)
88
+ end
89
+
90
+ attr_reader :config
91
+
92
+ def run!
93
+ # Remove legacy instance* and *_rsync|git style folders
94
+ Dir.entries(".").find_all { |name| name.include?('instance') || name.include?('_rsync') ||
95
+ name.include?('_git') }.each { |folder|
96
+ system "rm -rf #{folder}"
97
+ }
98
+
99
+ SSHTunnel.new(@config.server_host, @config.server_user || Testbot::DEFAULT_USER).open if @config.ssh_tunnel
100
+ while true
101
+ begin
102
+ update_uid!
103
+ start_ping
104
+ wait_for_jobs
105
+ rescue Exception => ex
106
+ break if [ 'SignalException', 'Interrupt' ].include?(ex.class.to_s)
107
+ puts "The runner crashed, restarting. Error: #{ex.inspect} #{ex.class}"
108
+ end
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def update_uid!
115
+ # When a runner crashes or is restarted it might loose current job info. Because
116
+ # of this we provide a new unique ID to the server so that it does not wait for
117
+ # lost jobs to complete.
118
+ @uid = "#{Time.now.to_i}@#{Mac.addr}"
119
+ end
120
+
121
+ def wait_for_jobs
122
+ loop do
123
+ sleep TIME_BETWEEN_POLLS
124
+ check_for_update if time_for_update?
125
+
126
+ # Only get jobs from one requester at a time
127
+ next_params = base_params
128
+ if @instances.size > 0
129
+ next_params.merge!({ :requester_mac => @last_requester_mac })
130
+ next_params.merge!({ :no_jruby => true }) if max_jruby_instances?
131
+ else
132
+ @last_requester_mac = nil
133
+ end
134
+
135
+ # Makes sure all instances are listed as available after a run
136
+ clear_completed_instances
137
+ next unless cpu_available?
138
+
139
+ next_job = Server.get("/jobs/next", :query => next_params) rescue nil
140
+ next if next_job == nil
141
+
142
+ job = Job.new(*([ self, next_job.split(',') ].flatten))
143
+ if first_job_from_requester?
144
+ fetch_code(job)
145
+ before_run(job) if File.exists?("#{job.project}/lib/tasks/testbot.rake")
146
+ end
147
+
148
+ @instances << [ Thread.new { job.run(free_instance_number) },
149
+ free_instance_number, job ]
150
+ @last_requester_mac = job.requester_mac
151
+ loop do
152
+ clear_completed_instances
153
+ break unless max_instances_running?
154
+ end
155
+ end
156
+ end
157
+
158
+ def max_jruby_instances?
159
+ return unless @config.max_jruby_instances
160
+ @instances.find_all { |thread, n, job| job.jruby? }.size >= @config.max_jruby_instances
161
+ end
162
+
163
+ def fetch_code(job)
164
+ system "rsync -az --delete -e ssh #{job.root}/ #{job.project}"
165
+ end
166
+
167
+ def before_run(job)
168
+ system "export RAILS_ENV=test; export TEST_INSTANCES=#{@config.max_instances}; cd #{job.project}; rake testbot:before_run"
169
+ end
170
+
171
+ def first_job_from_requester?
172
+ @last_requester_mac == nil
173
+ end
174
+
175
+ def cpu_available?
176
+ @instances.size > 0 || CPU.current_usage < MAX_CPU_USAGE_WHEN_IDLE
177
+ end
178
+
179
+ def time_for_update?
180
+ time_for_update = ((Time.now - @last_version_check) >= TIME_BETWEEN_VERSION_CHECKS)
181
+ @last_version_check = Time.now if time_for_update
182
+ time_for_update
183
+ end
184
+
185
+ def check_for_update
186
+ return unless @config.auto_update
187
+ version = Server.get('/version') rescue Testbot::VERSION
188
+ return unless version != Testbot::VERSION
189
+
190
+ successful_install = system "gem install testbot -v #{version}"
191
+
192
+ # This closes the process and attempts to re-run the same command.
193
+ exec "testbot #{ARGV.join(' ')}" if successful_install
194
+ end
195
+
196
+ def ping_params
197
+ { :hostname => (@hostname ||= `hostname`.chomp), :max_instances => @config.max_instances,
198
+ :idle_instances => (@config.max_instances - @instances.size), :username => ENV['USER'] }.merge(base_params)
199
+ end
200
+
201
+ def base_params
202
+ { :version => Testbot::VERSION, :uid => @uid }
203
+ end
204
+
205
+ def max_instances_running?
206
+ @instances.size == @config.max_instances
207
+ end
208
+
209
+ def clear_completed_instances
210
+ @instances.each_with_index do |data, index|
211
+ @instances.delete_at(index) if data.first.join(0.25)
212
+ end
213
+ end
214
+
215
+ def free_instance_number
216
+ 0.upto(@config.max_instances - 1) do |number|
217
+ return number unless @instances.find { |instance, n, job| n == number }
218
+ end
219
+ end
220
+
221
+ def start_ping
222
+ Thread.new do
223
+ while true
224
+ begin
225
+ Server.get("/runners/ping", :body => ping_params)
226
+ rescue
227
+ end
228
+ sleep TIME_BETWEEN_PINGS
229
+ end
230
+ end
231
+ end
232
+
233
+ end
@@ -0,0 +1,72 @@
1
+ require 'rubygems'
2
+ require 'sinatra'
3
+ require 'yaml'
4
+ require 'json'
5
+ require File.join(File.dirname(__FILE__), 'server/job.rb')
6
+ require File.join(File.dirname(__FILE__), 'server/group.rb') unless defined?(Group)
7
+ require File.join(File.dirname(__FILE__), 'server/runner.rb')
8
+ require File.join(File.dirname(__FILE__), 'server/build.rb')
9
+ require File.expand_path(File.join(File.dirname(__FILE__), 'testbot'))
10
+
11
+ if ENV['INTEGRATION_TEST']
12
+ set :port, 22880
13
+ else
14
+ set :port, Testbot::SERVER_PORT
15
+ end
16
+
17
+ disable :logging if ENV['DISABLE_LOGGING']
18
+
19
+ class Server
20
+ def self.valid_version?(runner_version)
21
+ Testbot::VERSION == runner_version
22
+ end
23
+ end
24
+
25
+ post '/builds' do
26
+ build = Build.create_and_build_jobs(params)[:id].to_s
27
+ end
28
+
29
+ get '/builds/:id' do
30
+ build = Build.find(:id => params[:id].to_i)
31
+ build.destroy if build[:done]
32
+ { "done" => build[:done], "results" => build[:results] }.to_json
33
+ end
34
+
35
+ get '/jobs/next' do
36
+ next_job, runner = Job.next(params, @env['REMOTE_ADDR'])
37
+ if next_job
38
+ next_job.update(:taken_at => Time.now, :taken_by_id => runner.id)
39
+ [ next_job[:id], next_job[:requester_mac], next_job[:project], next_job[:root], next_job[:type], (next_job[:jruby] == 1 ? 'jruby' : 'ruby'), next_job[:files] ].join(',')
40
+ end
41
+ end
42
+
43
+ put '/jobs/:id' do
44
+ Job.find(:id => params[:id].to_i).update(:result => params[:result]); nil
45
+ end
46
+
47
+ get '/runners/ping' do
48
+ return unless Server.valid_version?(params[:version])
49
+ runner = Runner.find(:uid => params[:uid])
50
+ runner.update(params.merge({ :last_seen_at => Time.now })) if runner
51
+ nil
52
+ end
53
+
54
+ get '/runners/outdated' do
55
+ Runner.find_all_outdated.map { |runner| [ runner[:ip], runner[:hostname], runner[:uid] ].join(' ') }.join("\n").strip
56
+ end
57
+
58
+ get '/runners/available_instances' do
59
+ Runner.available_instances.to_s
60
+ end
61
+
62
+ get '/runners/total_instances' do
63
+ Runner.total_instances.to_s
64
+ end
65
+
66
+ get '/runners/available' do
67
+ Runner.find_all_available.map { |runner| [ runner[:ip], runner[:hostname], runner[:uid], runner[:username], runner[:idle_instances] ].join(' ') }.join("\n").strip
68
+ end
69
+
70
+ get '/version' do
71
+ Testbot::VERSION
72
+ end