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