testbot 0.2.6

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.
@@ -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