testbot 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,29 @@
1
+ class Build < Sequel::Model
2
+
3
+ def self.create_and_build_jobs(hash)
4
+ hash["jruby"] = (hash["jruby"] == "true") ? 1 : 0
5
+ build = create(hash.reject { |k, v| k == 'available_runner_usage' })
6
+ build.create_jobs!(hash['available_runner_usage'])
7
+ build
8
+ end
9
+
10
+ def create_jobs!(available_runner_usage)
11
+ groups = Group.build(self[:files].split, self[:sizes].split.map { |size| size.to_i },
12
+ Runner.total_instances.to_f * (available_runner_usage.to_i / 100.0), self[:type])
13
+ groups.each do |group|
14
+ Job.create(:files => group.join(' '),
15
+ :root => self[:root],
16
+ :project => self[:project],
17
+ :type => self[:type],
18
+ :requester_mac => self[:requester_mac],
19
+ :build_id => self[:id],
20
+ :jruby => self[:jruby])
21
+ end
22
+ end
23
+
24
+ def destroy
25
+ Job.filter([ 'build_id = ?', self[:id] ]).each { |job| job.destroy }
26
+ super
27
+ end
28
+
29
+ end
@@ -0,0 +1,44 @@
1
+ require 'sequel'
2
+
3
+ DB = Sequel.sqlite
4
+
5
+ DB.create_table :builds do
6
+ primary_key :id
7
+ String :files
8
+ String :sizes
9
+ String :results, :default => ''
10
+ String :root
11
+ String :project
12
+ String :type
13
+ String :requester_mac
14
+ Integer :jruby
15
+ Boolean :done, :default => false
16
+ end
17
+
18
+
19
+ DB.create_table :jobs do
20
+ primary_key :id
21
+ String :files
22
+ String :result
23
+ String :root
24
+ String :project
25
+ String :type
26
+ String :requester_mac
27
+ Integer :jruby
28
+ Integer :build_id
29
+ Integer :taken_by_id
30
+ Datetime :taken_at, :default => nil
31
+ end
32
+
33
+ DB.create_table :runners do
34
+ primary_key :id
35
+ String :ip
36
+ String :hostname
37
+ String :uid
38
+ String :username
39
+ String :version
40
+ Integer :idle_instances
41
+ Integer :max_instances
42
+ Datetime :last_seen_at
43
+ end
44
+
@@ -0,0 +1,44 @@
1
+ require 'rubygems'
2
+
3
+ class Group
4
+
5
+ DEFAULT = nil
6
+
7
+ def self.build(files, sizes, instance_count, type)
8
+ tests_with_sizes = slow_tests_first(map_files_and_sizes(files, sizes))
9
+
10
+ groups = []
11
+ current_group, current_size = 0, 0
12
+ tests_with_sizes.each do |test, size|
13
+ # inserts into next group if current is full and we are not in the last group
14
+ if (0.5*size + current_size) > group_size(tests_with_sizes, instance_count) and instance_count > current_group + 1
15
+ current_size = size
16
+ current_group += 1
17
+ else
18
+ current_size += size
19
+ end
20
+ groups[current_group] ||= []
21
+ groups[current_group] << test
22
+ end
23
+
24
+ groups.compact
25
+ end
26
+
27
+ private
28
+
29
+ def self.group_size(tests_with_sizes, group_count)
30
+ total = tests_with_sizes.inject(0) { |sum, test| sum += test[1] }
31
+ total / group_count.to_f
32
+ end
33
+
34
+ def self.map_files_and_sizes(files, sizes)
35
+ list = []
36
+ files.each_with_index { |file, i| list << [ file, sizes[i] ] }
37
+ list
38
+ end
39
+
40
+ def self.slow_tests_first(tests)
41
+ tests.sort_by { |test, time| time.to_i }.reverse
42
+ end
43
+
44
+ end
@@ -0,0 +1,41 @@
1
+ require File.join(File.dirname(__FILE__), 'db.rb') unless defined?(DB)
2
+
3
+ class Job < Sequel::Model
4
+ def update(hash)
5
+ super(hash)
6
+ if build = Build.find([ "id = ?", self[:build_id] ])
7
+ done = Job.filter([ "result IS NULL AND build_id = ?", self[:build_id] ]).count == 0
8
+ build.update(:results => build[:results].to_s + hash[:result].to_s,
9
+ :done => done)
10
+ end
11
+ end
12
+
13
+ def self.next(params, remove_addr)
14
+ clean_params = params.reject { |k, v| [ "requester_mac", "no_jruby" ].include?(k) }
15
+ runner = Runner.record! clean_params.merge({ :ip => remove_addr, :last_seen_at => Time.now })
16
+ return unless Server.valid_version?(params[:version])
17
+ [ next_job_query(params["requester_mac"], params["no_jruby"]).first, runner ]
18
+ end
19
+
20
+ private
21
+
22
+ def self.next_job_query(requester_mac, no_jruby)
23
+ release_jobs_taken_by_missing_runners!
24
+ query = Job.filter("taken_at IS NULL").order("Random()".lit)
25
+ filters = []
26
+ filters << "requester_mac = '#{requester_mac}'" if requester_mac
27
+ filters << "jruby != 1" if no_jruby
28
+ if filters.empty?
29
+ query
30
+ else
31
+ query.filter(filters.join(' AND '))
32
+ end
33
+ end
34
+
35
+ def self.release_jobs_taken_by_missing_runners!
36
+ missing_runners = Runner.filter([ "last_seen_at < ?", (Time.now - Runner.timeout) ])
37
+ missing_runners.each { |r|
38
+ Job.filter(:taken_by_id => r[:id]).update(:taken_at => nil)
39
+ }
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ require File.join(File.dirname(__FILE__), 'db.rb') unless defined?(DB)
2
+
3
+ class Runner < Sequel::Model
4
+
5
+ def self.record!(hash)
6
+ create_or_update_by_mac!(hash)
7
+ end
8
+
9
+ def self.create_or_update_by_mac!(hash)
10
+ if (runner = find(:uid => hash[:uid]))
11
+ runner.update hash
12
+ else
13
+ Runner.create hash
14
+ end
15
+ end
16
+
17
+ def self.timeout
18
+ 10
19
+ end
20
+
21
+ def self.find_all_outdated
22
+ DB[:runners].filter("version != ? OR version IS NULL", Testbot::VERSION)
23
+ end
24
+
25
+ def self.find_all_available
26
+ DB[:runners].filter("version = ? AND last_seen_at > ?", Testbot::VERSION, Time.now - Runner.timeout)
27
+ end
28
+
29
+ def self.available_instances
30
+ find_all_available.inject(0) { |sum, r| r[:idle_instances] + sum }
31
+ end
32
+
33
+ def self.total_instances
34
+ return 1 if ENV['INTEGRATION_TEST']
35
+ find_all_available.inject(0) { |sum, r| r[:max_instances] + sum }
36
+ end
37
+
38
+ end
@@ -0,0 +1,19 @@
1
+ class SimpleDaemonize
2
+
3
+ def self.start(proc, pid_path)
4
+ pid = fork {
5
+ STDOUT.reopen "/dev/null"
6
+ proc.call
7
+ }
8
+
9
+ File.open(pid_path, 'w') { |file| file.write(pid) }
10
+ pid
11
+ end
12
+
13
+ def self.stop(pid_path)
14
+ return unless File.exists?(pid_path)
15
+ system "kill #{File.read(pid_path)} &> /dev/null"
16
+ system "rm #{pid_path} &> /dev/null"
17
+ end
18
+
19
+ end
@@ -0,0 +1,36 @@
1
+ require 'rubygems'
2
+ require 'net/ssh'
3
+
4
+ class SSHTunnel
5
+
6
+ def initialize(host, user, local_port = 2288)
7
+ @host, @user, @local_port = host, user, local_port
8
+ end
9
+
10
+ def open
11
+ connect
12
+
13
+ start_time = Time.now
14
+ while true
15
+ break if @up
16
+ sleep 0.5
17
+
18
+ if Time.now - start_time > 5
19
+ puts "SSH connection failed, trying again..."
20
+ start_time = Time.now
21
+ connect
22
+ end
23
+ end
24
+ end
25
+
26
+ def connect
27
+ @thread.kill! if @thread
28
+ @thread = Thread.new do
29
+ Net::SSH.start(@host, @user, { :timeout => 1 }) do |ssh|
30
+ ssh.forward.local(@local_port, 'localhost', Testbot::SERVER_PORT)
31
+ ssh.loop { @up = true }
32
+ end
33
+ end
34
+ end
35
+
36
+ end
@@ -0,0 +1,32 @@
1
+ require File.dirname(__FILE__) + '/../adapters/adapter'
2
+
3
+ namespace :testbot do
4
+
5
+ def run_and_show_results(adapter, custom_path)
6
+ Rake::Task["testbot:before_request"].invoke
7
+
8
+ require File.join(File.dirname(__FILE__), '..', "requester.rb")
9
+ requester = Requester.create_by_config("#{Rails.root}/config/testbot.yml")
10
+
11
+ puts "Running #{adapter.pluralized}..."
12
+ start_time = Time.now
13
+
14
+ path = custom_path ? "#{adapter.base_path}/#{custom_path}" : adapter.base_path
15
+ success = requester.run_tests(adapter, path)
16
+
17
+ puts
18
+ puts requester.result_lines.join("\n")
19
+ puts
20
+ puts "Finished in #{Time.now - start_time} seconds."
21
+ success
22
+ end
23
+
24
+ Adapter.all.each do |adapter|
25
+
26
+ desc "Run the #{adapter.name} tests using testbot"
27
+ task adapter.type, :custom_path do |_, args|
28
+ exit 1 unless run_and_show_results(adapter, args[:custom_path])
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,126 @@
1
+ require File.join(File.dirname(__FILE__), '/shared/simple_daemonize')
2
+ require File.join(File.dirname(__FILE__), '/adapters/adapter')
3
+ require 'fileutils'
4
+
5
+ module Testbot
6
+ require 'railtie' if defined?(Rails)
7
+
8
+ VERSION = "0.2.6"
9
+ SERVER_PID = "/tmp/testbot_server.pid"
10
+ RUNNER_PID = "/tmp/testbot_runner.pid"
11
+ DEFAULT_WORKING_DIR = "/tmp/testbot"
12
+ DEFAULT_SERVER_PATH = "/tmp/testbot/#{ENV['USER']}"
13
+ DEFAULT_USER = "testbot"
14
+ DEFAULT_PROJECT = "project"
15
+ DEFAULT_RUNNER_USAGE = "100%"
16
+ SERVER_PORT = ENV['INTEGRATION_TEST'] ? 22880 : 2288
17
+
18
+ class CLI
19
+
20
+ def self.run(argv)
21
+ return false if argv == []
22
+ opts = parse_args(argv)
23
+
24
+ if opts[:help]
25
+ return false
26
+ elsif opts[:version]
27
+ puts "Testbot #{Testbot::VERSION}"
28
+ elsif [ true, 'run', 'start' ].include?(opts[:server])
29
+ start_server(opts[:server])
30
+ elsif opts[:server] == 'stop'
31
+ stop('server', Testbot::SERVER_PID)
32
+ elsif [ true, 'run', 'start' ].include?(opts[:runner])
33
+ require File.join(File.dirname(__FILE__), '/runner')
34
+ return false unless valid_runner_opts?(opts)
35
+ start_runner(opts)
36
+ elsif opts[:runner] == 'stop'
37
+ stop('runner', Testbot::RUNNER_PID)
38
+ elsif adapter = Adapter.all.find { |adapter| opts[adapter.type.to_sym] }
39
+ require File.join(File.dirname(__FILE__), '/requester')
40
+ start_requester(opts, adapter)
41
+ end
42
+
43
+ true
44
+ end
45
+
46
+ def self.parse_args(argv)
47
+ last_setter = nil
48
+ hash = {}
49
+ str = ''
50
+ argv.each_with_index do |arg, i|
51
+ if arg.include?('--')
52
+ str = ''
53
+ last_setter = arg.split('--').last.to_sym
54
+ hash[last_setter] = true if (i == argv.size - 1) || argv[i+1].include?('--')
55
+ else
56
+ str += ' ' + arg
57
+ hash[last_setter] = str.strip
58
+ end
59
+ end
60
+ hash
61
+ end
62
+
63
+ def self.start_runner(opts)
64
+ stop('runner', Testbot::RUNNER_PID)
65
+
66
+ proc = lambda {
67
+ working_dir = opts[:working_dir] || Testbot::DEFAULT_WORKING_DIR
68
+ FileUtils.mkdir_p(working_dir)
69
+ Dir.chdir(working_dir)
70
+ runner = Runner.new(:server_host => opts[:connect],
71
+ :auto_update => opts[:auto_update], :max_instances => opts[:cpus],
72
+ :ssh_tunnel => opts[:ssh_tunnel], :user => opts[:user],
73
+ :max_jruby_instances => opts[:max_jruby_instances],
74
+ :jruby_opts => opts[:jruby_opts])
75
+ runner.run!
76
+ }
77
+
78
+ if opts[:runner] == 'run'
79
+ proc.call
80
+ else
81
+ pid = SimpleDaemonize.start(proc, Testbot::RUNNER_PID)
82
+ puts "Testbot runner started (pid: #{pid})"
83
+ end
84
+ end
85
+
86
+ def self.start_server(type)
87
+ stop('server', Testbot::SERVER_PID)
88
+
89
+ if type == 'run'
90
+ require File.join(File.dirname(__FILE__), '/server')
91
+ Sinatra::Application.run! :environment => "production"
92
+ else
93
+ pid = SimpleDaemonize.start(lambda {
94
+ ENV['DISABLE_LOGGING'] = "true"
95
+ require File.join(File.dirname(__FILE__), '/server')
96
+ Sinatra::Application.run! :environment => "production"
97
+ }, Testbot::SERVER_PID)
98
+ puts "Testbot server started (pid: #{pid})"
99
+ end
100
+ end
101
+
102
+ def self.stop(name, pid)
103
+ puts "Testbot #{name} stopped" if SimpleDaemonize.stop(pid)
104
+ end
105
+
106
+ def self.start_requester(opts, adapter)
107
+ requester = Requester.new(:server_host => opts[:connect],
108
+ :rsync_path => opts[:rsync_path],
109
+ :rsync_ignores => opts[:rsync_ignores].to_s,
110
+ :available_runner_usage => nil,
111
+ :project => opts[:project],
112
+ :ssh_tunnel => opts[:ssh_tunnel], :server_user => opts[:user])
113
+ requester.run_tests(adapter, adapter.base_path)
114
+ end
115
+
116
+ def self.valid_runner_opts?(opts)
117
+ opts[:connect].is_a?(String)
118
+ end
119
+
120
+ def self.lib_path
121
+ File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
122
+ end
123
+
124
+ end
125
+
126
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require File.expand_path("lib/testbot")
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "testbot"
7
+ s.version = Testbot::VERSION
8
+ s.authors = ["Joakim Kolsjö"]
9
+ s.email = ["joakim.kolsjo@gmail.com"]
10
+ s.homepage = "http://github.com/joakimk/testbot"
11
+ s.summary = %q{A test distribution tool.}
12
+ s.description = %q{Testbot is a test distribution tool that works with Rails, RSpec, Test::Unit and Cucumber.}
13
+ s.bindir = "bin"
14
+ s.executables = [ "testbot" ]
15
+ s.files = Dir.glob("lib/**/*") + %w(Gemfile testbot.gemspec CHANGELOG README.markdown bin/testbot)
16
+ s.add_dependency('sinatra', '=1.0.0') # To be able to use rack 1.0.1 which is compatible with rails 2.
17
+ s.add_dependency('httparty', '>= 0.6.1')
18
+ s.add_dependency('macaddr', '>= 1.0.0')
19
+ s.add_dependency('net-ssh', '>= 2.0.23')
20
+ s.add_dependency('sequel', '>= 3.16.0')
21
+ s.add_dependency('sqlite3-ruby', '>= 1.2.5')
22
+ s.add_dependency('json', '>= 1.4.6')
23
+
24
+ # Because sinatra's "disable :logging" does not work with WEBrick.
25
+ # Also, using a version that builds on ruby 1.9.2.
26
+ s.add_dependency('mongrel', '1.2.0.pre2')
27
+ end