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