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.
- data/CHANGELOG +62 -0
- data/Gemfile +12 -0
- data/README.markdown +128 -0
- data/bin/testbot +59 -0
- data/lib/adapters/adapter.rb +22 -0
- data/lib/adapters/cucumber_adapter.rb +31 -0
- data/lib/adapters/rspec_adapter.rb +31 -0
- data/lib/adapters/test_unit_adapter.rb +31 -0
- data/lib/generators/testbot/templates/testbot.rake.erb +35 -0
- data/lib/generators/testbot/templates/testbot.yml.erb +40 -0
- data/lib/generators/testbot/testbot_generator.rb +15 -0
- data/lib/new_runner.rb +30 -0
- data/lib/railtie.rb +10 -0
- data/lib/requester.rb +147 -0
- data/lib/runner.rb +233 -0
- data/lib/server.rb +72 -0
- data/lib/server/build.rb +29 -0
- data/lib/server/db.rb +44 -0
- data/lib/server/group.rb +44 -0
- data/lib/server/job.rb +41 -0
- data/lib/server/runner.rb +38 -0
- data/lib/shared/simple_daemonize.rb +19 -0
- data/lib/shared/ssh_tunnel.rb +36 -0
- data/lib/tasks/testbot.rake +32 -0
- data/lib/testbot.rb +126 -0
- data/testbot.gemspec +27 -0
- metadata +221 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
class TestbotGenerator < Rails::Generators::Base
|
2
|
+
source_root File.expand_path('../templates', __FILE__)
|
3
|
+
|
4
|
+
class_option :connect, :type => :string, :required => true, :desc => "Which server to use (required)"
|
5
|
+
class_option :project, :type => :string, :default => nil, :desc => "The name of your project (default: #{Testbot::DEFAULT_PROJECT})"
|
6
|
+
class_option :rsync_path, :type => :string, :default => nil, :desc => "Sync path on the server (default: #{Testbot::DEFAULT_SERVER_PATH})"
|
7
|
+
class_option :rsync_ignores, :type => :string, :default => nil, :desc => "Files to rsync_ignores when syncing (default: include all)"
|
8
|
+
class_option :ssh_tunnel, :type => :boolean, :default => nil, :desc => "Use a ssh tunnel"
|
9
|
+
class_option :user, :type => :string, :default => nil, :desc => "Use a custom rsync / ssh tunnel user (default: #{Testbot::DEFAULT_USER})"
|
10
|
+
|
11
|
+
def generate_config
|
12
|
+
template "testbot.yml.erb", "config/testbot.yml"
|
13
|
+
template "testbot.rake.erb", "lib/tasks/testbot.rake"
|
14
|
+
end
|
15
|
+
end
|
data/lib/new_runner.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'httparty'
|
3
|
+
|
4
|
+
class Server
|
5
|
+
include HTTParty
|
6
|
+
end
|
7
|
+
|
8
|
+
class NewRunner
|
9
|
+
|
10
|
+
def self.run_jobs
|
11
|
+
next_job = Server.get('/jobs/next') rescue nil
|
12
|
+
return unless next_job
|
13
|
+
id, root, specs = next_job.split(',')
|
14
|
+
result = run_and_return_results("export RAILS_ENV=test; export RSPEC_COLOR=true; rake testbot:before_run; script/spec -O spec/spec.opts #{specs}")
|
15
|
+
Server.put("/jobs/#{id}", :body => { :result => result })
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.load_config
|
19
|
+
host = YAML.load_file("#{ENV['HOME']}/.testbot_runner.yml")[:server_host]
|
20
|
+
Server.base_uri "http://#{host}:2288"
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# I'd really like to know how to do this better and more testable
|
26
|
+
def self.run_and_return_results(cmd)
|
27
|
+
`#{cmd} 2>&1`
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
data/lib/railtie.rb
ADDED
data/lib/requester.rb
ADDED
@@ -0,0 +1,147 @@
|
|
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__) + '/adapters/adapter'
|
7
|
+
require File.expand_path(File.dirname(__FILE__) + '/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
|
+
class Requester
|
19
|
+
|
20
|
+
attr_reader :config
|
21
|
+
|
22
|
+
def initialize(config = {})
|
23
|
+
config = config.symbolize_keys_without_active_support
|
24
|
+
config[:rsync_path] ||= Testbot::DEFAULT_SERVER_PATH
|
25
|
+
config[:project] ||= Testbot::DEFAULT_PROJECT
|
26
|
+
config[:server_user] ||= Testbot::DEFAULT_USER
|
27
|
+
config[:available_runner_usage] ||= Testbot::DEFAULT_RUNNER_USAGE
|
28
|
+
@config = OpenStruct.new(config)
|
29
|
+
end
|
30
|
+
|
31
|
+
def run_tests(adapter, dir)
|
32
|
+
puts if config.simple_output
|
33
|
+
|
34
|
+
if config.ssh_tunnel
|
35
|
+
SSHTunnel.new(config.server_host, config.server_user, adapter.requester_port).open
|
36
|
+
server_uri = "http://127.0.0.1:#{adapter.requester_port}"
|
37
|
+
else
|
38
|
+
server_uri = "http://#{config.server_host}:#{Testbot::SERVER_PORT}"
|
39
|
+
end
|
40
|
+
|
41
|
+
rsync_ignores = config.rsync_ignores.to_s.split.map { |pattern| "--exclude='#{pattern}'" }.join(' ')
|
42
|
+
system "rsync -az --delete -e ssh #{rsync_ignores} . #{rsync_uri}"
|
43
|
+
|
44
|
+
files = find_tests(adapter, dir)
|
45
|
+
sizes = find_sizes(files)
|
46
|
+
|
47
|
+
build_id = HTTParty.post("#{server_uri}/builds", :body => { :root => root,
|
48
|
+
:type => adapter.type.to_s,
|
49
|
+
:project => config.project,
|
50
|
+
:requester_mac => Mac.addr,
|
51
|
+
:available_runner_usage => config.available_runner_usage,
|
52
|
+
:files => files.join(' '),
|
53
|
+
:sizes => sizes.join(' '),
|
54
|
+
:jruby => jruby? })
|
55
|
+
|
56
|
+
|
57
|
+
last_results_size = 0
|
58
|
+
success = true
|
59
|
+
error_count = 0
|
60
|
+
while true
|
61
|
+
sleep 1
|
62
|
+
|
63
|
+
begin
|
64
|
+
@build = HTTParty.get("#{server_uri}/builds/#{build_id}", :format => :json)
|
65
|
+
next unless @build
|
66
|
+
rescue Exception => ex
|
67
|
+
error_count += 1
|
68
|
+
if error_count > 4
|
69
|
+
puts "Failed to get status: #{ex.message}"
|
70
|
+
error_count = 0
|
71
|
+
end
|
72
|
+
next
|
73
|
+
end
|
74
|
+
|
75
|
+
results = @build['results'][last_results_size..-1]
|
76
|
+
unless results == ''
|
77
|
+
if config.simple_output
|
78
|
+
print results.gsub(/[^\.F]|Finished/, '')
|
79
|
+
STDOUT.flush
|
80
|
+
else
|
81
|
+
puts results
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
last_results_size = @build['results'].size
|
86
|
+
|
87
|
+
success = false if failed_build?(@build)
|
88
|
+
break if @build['done']
|
89
|
+
end
|
90
|
+
|
91
|
+
puts if config.simple_output
|
92
|
+
|
93
|
+
success
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.create_by_config(path)
|
97
|
+
config = YAML.load_file(path)
|
98
|
+
Requester.new(config)
|
99
|
+
end
|
100
|
+
|
101
|
+
def result_lines
|
102
|
+
@build['results'].split("\n").find_all { |line| line_is_result?(line) }.map { |line| line.chomp }
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def root
|
108
|
+
if localhost?
|
109
|
+
config.rsync_path
|
110
|
+
else
|
111
|
+
"#{config.server_user}@#{config.server_host}:#{config.rsync_path}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def rsync_uri
|
116
|
+
localhost? ? config.rsync_path : "#{config.server_user}@#{config.server_host}:#{config.rsync_path}"
|
117
|
+
end
|
118
|
+
|
119
|
+
def localhost?
|
120
|
+
[ '0.0.0.0', 'localhost', '127.0.0.1' ].include?(config.server_host)
|
121
|
+
end
|
122
|
+
|
123
|
+
def failed_build?(build)
|
124
|
+
result_lines.any? { |line| line_is_failure?(line) }
|
125
|
+
end
|
126
|
+
|
127
|
+
def line_is_result?(line)
|
128
|
+
line =~ /\d+ fail/
|
129
|
+
end
|
130
|
+
|
131
|
+
def line_is_failure?(line)
|
132
|
+
line =~ /(\d{2,}|[1-9]) (fail|error)/
|
133
|
+
end
|
134
|
+
|
135
|
+
def find_tests(adapter, dir)
|
136
|
+
Dir["#{dir}/#{adapter.file_pattern}"]
|
137
|
+
end
|
138
|
+
|
139
|
+
def find_sizes(files)
|
140
|
+
files.map { |file| File.stat(file).size }
|
141
|
+
end
|
142
|
+
|
143
|
+
def jruby?
|
144
|
+
RUBY_PLATFORM =~ /java/ || !!ENV['USE_JRUBY']
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
data/lib/runner.rb
ADDED
@@ -0,0 +1,233 @@
|
|
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__) + '/adapters/adapter'
|
7
|
+
|
8
|
+
TIME_BETWEEN_POLLS = 1
|
9
|
+
TIME_BETWEEN_PINGS = 5
|
10
|
+
TIME_BETWEEN_VERSION_CHECKS = 60
|
11
|
+
MAX_CPU_USAGE_WHEN_IDLE = 50
|
12
|
+
|
13
|
+
class CPU
|
14
|
+
|
15
|
+
def self.current_usage
|
16
|
+
process_usages = `ps -eo pcpu`
|
17
|
+
total_usage = process_usages.split("\n").inject(0) { |sum, usage| sum += usage.strip.to_f }
|
18
|
+
(total_usage / count).to_i
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.count
|
22
|
+
case RUBY_PLATFORM
|
23
|
+
when /darwin/
|
24
|
+
`hwprefs cpu_count`.to_i
|
25
|
+
when /linux/
|
26
|
+
`cat /proc/cpuinfo | grep processor | wc -l`.to_i
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
class Job
|
33
|
+
attr_reader :root, :project, :requester_mac
|
34
|
+
|
35
|
+
def initialize(runner, id, requester_mac, project, root, type, ruby_interpreter, files)
|
36
|
+
@runner, @id, @requester_mac, @project, @root, @type, @ruby_interpreter, @files =
|
37
|
+
runner, id, requester_mac, project, root, type, ruby_interpreter, files
|
38
|
+
end
|
39
|
+
|
40
|
+
def jruby?
|
41
|
+
@ruby_interpreter == 'jruby'
|
42
|
+
end
|
43
|
+
|
44
|
+
def run(instance)
|
45
|
+
puts "Running job #{@id} from #{@requester_mac}... "
|
46
|
+
test_env_number = (instance == 0) ? '' : instance + 1
|
47
|
+
result = "\n#{`hostname`.chomp}:#{Dir.pwd}\n"
|
48
|
+
base_environment = "export RAILS_ENV=test; export TEST_ENV_NUMBER=#{test_env_number}; cd #{@project};"
|
49
|
+
|
50
|
+
adapter = Adapter.find(@type)
|
51
|
+
result += `#{base_environment} #{adapter.command(ruby_cmd, @files)} 2>&1`
|
52
|
+
|
53
|
+
Server.put("/jobs/#{@id}", :body => { :result => result })
|
54
|
+
puts "Job #{@id} finished."
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def ruby_cmd
|
60
|
+
if @ruby_interpreter == 'jruby' && @runner.config.jruby_opts
|
61
|
+
'jruby ' + @runner.config.jruby_opts
|
62
|
+
else
|
63
|
+
@ruby_interpreter
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Server
|
69
|
+
include HTTParty
|
70
|
+
end
|
71
|
+
|
72
|
+
class Runner
|
73
|
+
|
74
|
+
def initialize(config)
|
75
|
+
@instances = []
|
76
|
+
@last_requester_mac = nil
|
77
|
+
@last_version_check = Time.now - TIME_BETWEEN_VERSION_CHECKS - 1
|
78
|
+
@config = OpenStruct.new(config)
|
79
|
+
@config.max_instances = @config.max_instances ? @config.max_instances.to_i : CPU.count
|
80
|
+
|
81
|
+
if @config.ssh_tunnel
|
82
|
+
server_uri = "http://127.0.0.1:#{Testbot::SERVER_PORT}"
|
83
|
+
else
|
84
|
+
server_uri = "http://#{@config.server_host}:#{Testbot::SERVER_PORT}"
|
85
|
+
end
|
86
|
+
|
87
|
+
Server.base_uri(server_uri)
|
88
|
+
end
|
89
|
+
|
90
|
+
attr_reader :config
|
91
|
+
|
92
|
+
def run!
|
93
|
+
# Remove legacy instance* and *_rsync|git style folders
|
94
|
+
Dir.entries(".").find_all { |name| name.include?('instance') || name.include?('_rsync') ||
|
95
|
+
name.include?('_git') }.each { |folder|
|
96
|
+
system "rm -rf #{folder}"
|
97
|
+
}
|
98
|
+
|
99
|
+
SSHTunnel.new(@config.server_host, @config.server_user || Testbot::DEFAULT_USER).open if @config.ssh_tunnel
|
100
|
+
while true
|
101
|
+
begin
|
102
|
+
update_uid!
|
103
|
+
start_ping
|
104
|
+
wait_for_jobs
|
105
|
+
rescue Exception => ex
|
106
|
+
break if [ 'SignalException', 'Interrupt' ].include?(ex.class.to_s)
|
107
|
+
puts "The runner crashed, restarting. Error: #{ex.inspect} #{ex.class}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def update_uid!
|
115
|
+
# When a runner crashes or is restarted it might loose current job info. Because
|
116
|
+
# of this we provide a new unique ID to the server so that it does not wait for
|
117
|
+
# lost jobs to complete.
|
118
|
+
@uid = "#{Time.now.to_i}@#{Mac.addr}"
|
119
|
+
end
|
120
|
+
|
121
|
+
def wait_for_jobs
|
122
|
+
loop do
|
123
|
+
sleep TIME_BETWEEN_POLLS
|
124
|
+
check_for_update if time_for_update?
|
125
|
+
|
126
|
+
# Only get jobs from one requester at a time
|
127
|
+
next_params = base_params
|
128
|
+
if @instances.size > 0
|
129
|
+
next_params.merge!({ :requester_mac => @last_requester_mac })
|
130
|
+
next_params.merge!({ :no_jruby => true }) if max_jruby_instances?
|
131
|
+
else
|
132
|
+
@last_requester_mac = nil
|
133
|
+
end
|
134
|
+
|
135
|
+
# Makes sure all instances are listed as available after a run
|
136
|
+
clear_completed_instances
|
137
|
+
next unless cpu_available?
|
138
|
+
|
139
|
+
next_job = Server.get("/jobs/next", :query => next_params) rescue nil
|
140
|
+
next if next_job == nil
|
141
|
+
|
142
|
+
job = Job.new(*([ self, next_job.split(',') ].flatten))
|
143
|
+
if first_job_from_requester?
|
144
|
+
fetch_code(job)
|
145
|
+
before_run(job) if File.exists?("#{job.project}/lib/tasks/testbot.rake")
|
146
|
+
end
|
147
|
+
|
148
|
+
@instances << [ Thread.new { job.run(free_instance_number) },
|
149
|
+
free_instance_number, job ]
|
150
|
+
@last_requester_mac = job.requester_mac
|
151
|
+
loop do
|
152
|
+
clear_completed_instances
|
153
|
+
break unless max_instances_running?
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def max_jruby_instances?
|
159
|
+
return unless @config.max_jruby_instances
|
160
|
+
@instances.find_all { |thread, n, job| job.jruby? }.size >= @config.max_jruby_instances
|
161
|
+
end
|
162
|
+
|
163
|
+
def fetch_code(job)
|
164
|
+
system "rsync -az --delete -e ssh #{job.root}/ #{job.project}"
|
165
|
+
end
|
166
|
+
|
167
|
+
def before_run(job)
|
168
|
+
system "export RAILS_ENV=test; export TEST_INSTANCES=#{@config.max_instances}; cd #{job.project}; rake testbot:before_run"
|
169
|
+
end
|
170
|
+
|
171
|
+
def first_job_from_requester?
|
172
|
+
@last_requester_mac == nil
|
173
|
+
end
|
174
|
+
|
175
|
+
def cpu_available?
|
176
|
+
@instances.size > 0 || CPU.current_usage < MAX_CPU_USAGE_WHEN_IDLE
|
177
|
+
end
|
178
|
+
|
179
|
+
def time_for_update?
|
180
|
+
time_for_update = ((Time.now - @last_version_check) >= TIME_BETWEEN_VERSION_CHECKS)
|
181
|
+
@last_version_check = Time.now if time_for_update
|
182
|
+
time_for_update
|
183
|
+
end
|
184
|
+
|
185
|
+
def check_for_update
|
186
|
+
return unless @config.auto_update
|
187
|
+
version = Server.get('/version') rescue Testbot::VERSION
|
188
|
+
return unless version != Testbot::VERSION
|
189
|
+
|
190
|
+
successful_install = system "gem install testbot -v #{version}"
|
191
|
+
|
192
|
+
# This closes the process and attempts to re-run the same command.
|
193
|
+
exec "testbot #{ARGV.join(' ')}" if successful_install
|
194
|
+
end
|
195
|
+
|
196
|
+
def ping_params
|
197
|
+
{ :hostname => (@hostname ||= `hostname`.chomp), :max_instances => @config.max_instances,
|
198
|
+
:idle_instances => (@config.max_instances - @instances.size), :username => ENV['USER'] }.merge(base_params)
|
199
|
+
end
|
200
|
+
|
201
|
+
def base_params
|
202
|
+
{ :version => Testbot::VERSION, :uid => @uid }
|
203
|
+
end
|
204
|
+
|
205
|
+
def max_instances_running?
|
206
|
+
@instances.size == @config.max_instances
|
207
|
+
end
|
208
|
+
|
209
|
+
def clear_completed_instances
|
210
|
+
@instances.each_with_index do |data, index|
|
211
|
+
@instances.delete_at(index) if data.first.join(0.25)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def free_instance_number
|
216
|
+
0.upto(@config.max_instances - 1) do |number|
|
217
|
+
return number unless @instances.find { |instance, n, job| n == number }
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def start_ping
|
222
|
+
Thread.new do
|
223
|
+
while true
|
224
|
+
begin
|
225
|
+
Server.get("/runners/ping", :body => ping_params)
|
226
|
+
rescue
|
227
|
+
end
|
228
|
+
sleep TIME_BETWEEN_PINGS
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
data/lib/server.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sinatra'
|
3
|
+
require 'yaml'
|
4
|
+
require 'json'
|
5
|
+
require File.join(File.dirname(__FILE__), 'server/job.rb')
|
6
|
+
require File.join(File.dirname(__FILE__), 'server/group.rb') unless defined?(Group)
|
7
|
+
require File.join(File.dirname(__FILE__), 'server/runner.rb')
|
8
|
+
require File.join(File.dirname(__FILE__), 'server/build.rb')
|
9
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'testbot'))
|
10
|
+
|
11
|
+
if ENV['INTEGRATION_TEST']
|
12
|
+
set :port, 22880
|
13
|
+
else
|
14
|
+
set :port, Testbot::SERVER_PORT
|
15
|
+
end
|
16
|
+
|
17
|
+
disable :logging if ENV['DISABLE_LOGGING']
|
18
|
+
|
19
|
+
class Server
|
20
|
+
def self.valid_version?(runner_version)
|
21
|
+
Testbot::VERSION == runner_version
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
post '/builds' do
|
26
|
+
build = Build.create_and_build_jobs(params)[:id].to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
get '/builds/:id' do
|
30
|
+
build = Build.find(:id => params[:id].to_i)
|
31
|
+
build.destroy if build[:done]
|
32
|
+
{ "done" => build[:done], "results" => build[:results] }.to_json
|
33
|
+
end
|
34
|
+
|
35
|
+
get '/jobs/next' do
|
36
|
+
next_job, runner = Job.next(params, @env['REMOTE_ADDR'])
|
37
|
+
if next_job
|
38
|
+
next_job.update(:taken_at => Time.now, :taken_by_id => runner.id)
|
39
|
+
[ next_job[:id], next_job[:requester_mac], next_job[:project], next_job[:root], next_job[:type], (next_job[:jruby] == 1 ? 'jruby' : 'ruby'), next_job[:files] ].join(',')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
put '/jobs/:id' do
|
44
|
+
Job.find(:id => params[:id].to_i).update(:result => params[:result]); nil
|
45
|
+
end
|
46
|
+
|
47
|
+
get '/runners/ping' do
|
48
|
+
return unless Server.valid_version?(params[:version])
|
49
|
+
runner = Runner.find(:uid => params[:uid])
|
50
|
+
runner.update(params.merge({ :last_seen_at => Time.now })) if runner
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
get '/runners/outdated' do
|
55
|
+
Runner.find_all_outdated.map { |runner| [ runner[:ip], runner[:hostname], runner[:uid] ].join(' ') }.join("\n").strip
|
56
|
+
end
|
57
|
+
|
58
|
+
get '/runners/available_instances' do
|
59
|
+
Runner.available_instances.to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
get '/runners/total_instances' do
|
63
|
+
Runner.total_instances.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
get '/runners/available' do
|
67
|
+
Runner.find_all_available.map { |runner| [ runner[:ip], runner[:hostname], runner[:uid], runner[:username], runner[:idle_instances] ].join(' ') }.join("\n").strip
|
68
|
+
end
|
69
|
+
|
70
|
+
get '/version' do
|
71
|
+
Testbot::VERSION
|
72
|
+
end
|