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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/CHANGELOG +264 -0
  4. data/Gemfile +3 -0
  5. data/README.markdown +141 -0
  6. data/Rakefile +35 -0
  7. data/bin/testbot +59 -0
  8. data/lib/generators/testbot/templates/testbot.rake.erb +35 -0
  9. data/lib/generators/testbot/templates/testbot.yml.erb +45 -0
  10. data/lib/generators/testbot/testbot_generator.rb +19 -0
  11. data/lib/railtie.rb +16 -0
  12. data/lib/requester/requester.rb +171 -0
  13. data/lib/runner/job.rb +112 -0
  14. data/lib/runner/runner.rb +222 -0
  15. data/lib/runner/safe_result_text.rb +29 -0
  16. data/lib/server/build.rb +36 -0
  17. data/lib/server/group.rb +48 -0
  18. data/lib/server/job.rb +64 -0
  19. data/lib/server/memory_model.rb +91 -0
  20. data/lib/server/runner.rb +47 -0
  21. data/lib/server/server.rb +103 -0
  22. data/lib/server/status/javascripts/jquery-1.4.4.min.js +167 -0
  23. data/lib/server/status/status.html +48 -0
  24. data/lib/server/status/stylesheets/status.css +14 -0
  25. data/lib/shared/adapters/adapter.rb +27 -0
  26. data/lib/shared/adapters/cucumber_adapter.rb +91 -0
  27. data/lib/shared/adapters/helpers/ruby_env.rb +47 -0
  28. data/lib/shared/adapters/rspec2_adapter.rb +61 -0
  29. data/lib/shared/adapters/rspec_adapter.rb +79 -0
  30. data/lib/shared/adapters/test_unit_adapter.rb +44 -0
  31. data/lib/shared/color.rb +16 -0
  32. data/lib/shared/simple_daemonize.rb +25 -0
  33. data/lib/shared/ssh_tunnel.rb +36 -0
  34. data/lib/shared/testbot.rb +132 -0
  35. data/lib/shared/version.rb +12 -0
  36. data/lib/tasks/testbot.rake +30 -0
  37. data/lib/testbot.rb +2 -0
  38. data/test/fixtures/local/Rakefile +7 -0
  39. data/test/fixtures/local/config/testbot.yml +5 -0
  40. data/test/fixtures/local/log/test.log +0 -0
  41. data/test/fixtures/local/script/spec +2 -0
  42. data/test/fixtures/local/spec/models/car_spec.rb +0 -0
  43. data/test/fixtures/local/spec/models/house_spec.rb +0 -0
  44. data/test/fixtures/local/spec/spec.opts +0 -0
  45. data/test/fixtures/local/tmp/restart.txt +0 -0
  46. data/test/integration_test.rb +55 -0
  47. data/test/requester/requester_test.rb +407 -0
  48. data/test/requester/testbot.yml +7 -0
  49. data/test/requester/testbot_with_erb.yml +2 -0
  50. data/test/runner/job_test.rb +94 -0
  51. data/test/runner/safe_result_text_test.rb +20 -0
  52. data/test/server/group_test.rb +43 -0
  53. data/test/server/server_test.rb +511 -0
  54. data/test/shared/adapters/adapter_test.rb +22 -0
  55. data/test/shared/adapters/cucumber_adapter_test.rb +72 -0
  56. data/test/shared/adapters/helpers/ruby_env_test.rb +108 -0
  57. data/test/shared/adapters/rspec_adapter_test.rb +109 -0
  58. data/test/shared/testbot_test.rb +185 -0
  59. data/testbot.gemspec +34 -0
  60. 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
@@ -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
@@ -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