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 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.6'
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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require File.expand_path(File.join(File.dirname(__FILE__), '../lib/testbot.rb'))
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '../lib/shared/testbot.rb'))
4
4
 
5
5
  def show_help
6
6
  puts "Testbot #{Testbot.version}"
@@ -1,4 +1,4 @@
1
- require File.expand_path(File.dirname(__FILE__) + "/../../testbot")
1
+ require File.expand_path(File.dirname(__FILE__) + "/../../shared/testbot")
2
2
  require "acts_as_rails3_generator"
3
3
 
4
4
  class TestbotGenerator < Rails::Generators::Base
data/lib/railtie.rb CHANGED
@@ -1,5 +1,3 @@
1
- require 'testbot'
2
-
3
1
  begin
4
2
  require 'rails'
5
3
  @rails_loaded = true
@@ -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
- class Job
2
- attr_reader :root, :project, :requester_mac
3
-
4
- def initialize(runner, id, requester_mac, project, root, type, ruby_interpreter, files)
5
- @runner, @id, @requester_mac, @project, @root, @type, @ruby_interpreter, @files =
6
- runner, id, requester_mac, project, root, type, ruby_interpreter, files
7
- end
8
-
9
- def jruby?
10
- @ruby_interpreter == 'jruby'
11
- end
12
-
13
- def run(instance)
14
- puts "Running job #{@id} from #{@requester_mac}... "
15
- test_env_number = (instance == 0) ? '' : instance + 1
16
- result = "\n#{`hostname`.chomp}:#{Dir.pwd}\n"
17
- base_environment = "export RAILS_ENV=test; export TEST_ENV_NUMBER=#{test_env_number}; cd #{@project};"
18
-
19
- adapter = Adapter.find(@type)
20
- result += `#{base_environment} #{adapter.command(@project, ruby_cmd, @files)} 2>&1`
21
-
22
- Server.put("/jobs/#{@id}", :body => { :result => result, :success => ($?.exitstatus == 0) })
23
- puts "Job #{@id} finished."
24
- end
25
-
26
- private
27
-
28
- def ruby_cmd
29
- if @ruby_interpreter == 'jruby' && @runner.config.jruby_opts
30
- 'jruby ' + @runner.config.jruby_opts
31
- else
32
- @ruby_interpreter
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