# Use a custom port"
56 |
57 | end
58 |
59 | show_help unless Testbot::CLI.run(ARGV)
60 |
--------------------------------------------------------------------------------
/lib/generators/testbot/templates/testbot.rake.erb:
--------------------------------------------------------------------------------
1 | namespace :testbot do
2 | task :before_request do
3 | # This is run after you start a request (ex: rake testbot:spec)
4 | end
5 |
6 | task :before_run do
7 | # This is run by the runner after files are synced but before tests are run
8 |
9 | # Example: Setting up a test database
10 | database_yml = <<-DB_CONFIG
11 | test:
12 | adapter: mysql
13 | encoding: utf8
14 | database: <%= options[:project] %>_testbot_test<%%= ENV['TEST_ENV_NUMBER'] %>
15 | username: root
16 | password:
17 | host: localhost
18 | DB_CONFIG
19 |
20 | # database_file_path = "config/database.yml"
21 | # File.open(database_file_path, 'w') { |f| f.write(database_yml) }
22 | #
23 | # # Setup databases for all instances
24 | # 0.upto(ENV['TEST_INSTANCES'].to_i - 1) do |instance|
25 | # test_env_number = (instance == 0) ? '' : instance + 1
26 | # system "mysqladmin -u root -f drop <%= options[:project] %>_testbot_test#{test_env_number} > /dev/null 2>&1"
27 | # system "mysqladmin -u root -f create <%= options[:project] %>_testbot_test#{test_env_number} > /dev/null 2>&1"
28 | # system "export RAILS_ENV=test; export TEST_ENV_NUMBER=#{test_env_number}; rake db:test:load"
29 | # end
30 |
31 | # Example: Building gems
32 | # system "rm vendor/gems/*/ext/**/*.o > /dev/null 2>&1"
33 | # system "rake gems:build:force > /dev/null 2>&1"
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/generators/testbot/templates/testbot.yml.erb:
--------------------------------------------------------------------------------
1 | # You can use ERB here.
2 |
3 | # Which server to use.
4 | server_host: <%= options[:connect] %>
5 | <%- if options[:user] -%>
6 | server_user: <%= options[:user] %>
7 | <%- else -%>
8 | # server_user: <%= Testbot::DEFAULT_USER %>
9 | <%- end -%>
10 |
11 | # Project prefix. Used for prefixing project files
12 | # so that you can run multiple projects in the same testbot network.
13 | <%- if options[:project] %>
14 | project: <%= options[:project] %>
15 | <%- else -%>
16 | # project: <%= Testbot::DEFAULT_PROJECT %>
17 | <%- end -%>
18 |
19 | # RSync settings. The folder where your files are synced to
20 | # and then fetched from before running the tests.
21 | <%- if options[:rsync_path] -%>
22 | rsync_path: <%= options[:rsync_path] %>
23 | <%- else -%>
24 | # rsync_path: <%= Testbot::DEFAULT_SERVER_PATH %>
25 | <%- end -%>
26 | <%- if options[:rsync_ignores] -%>
27 | rsync_ignores: <%= options[:rsync_ignores] %>
28 | <%- else -%>
29 | # rsync_ignores:
30 | <%- end -%>
31 |
32 | # To tunnel traffic through SSH
33 | <%- if options[:ssh_tunnel] -%>
34 | ssh_tunnel: true
35 | <%- else -%>
36 | # ssh_tunnel: true
37 | <%- end -%>
38 |
39 | # Runner usage. Set to a lower percentage to not use
40 | # every available instance or higher to create more
41 | # jobs than there are instances.
42 | available_runner_usage: 100%
43 |
44 | # Enable more logging from the requester
45 | # logging: true
46 |
--------------------------------------------------------------------------------
/lib/generators/testbot/testbot_generator.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + "/../../shared/testbot")
2 | require "acts_as_rails3_generator"
3 |
4 | class TestbotGenerator < Rails::Generators::Base
5 | source_root File.expand_path('../templates', __FILE__)
6 |
7 | class_option :connect, :type => :string, :required => true, :desc => "Which server to use (required)"
8 | class_option :project, :type => :string, :default => nil, :desc => "The name of your project (default: #{Testbot::DEFAULT_PROJECT})"
9 | class_option :rsync_path, :type => :string, :default => nil, :desc => "Sync path on the server (default: #{Testbot::DEFAULT_SERVER_PATH})"
10 | class_option :rsync_ignores, :type => :string, :default => nil, :desc => "Files to rsync_ignores when syncing (default: include all)"
11 | class_option :ssh_tunnel, :type => :boolean, :default => nil, :desc => "Use a ssh tunnel"
12 | class_option :user, :type => :string, :default => nil, :desc => "Use a custom rsync / ssh tunnel user (default: #{Testbot::DEFAULT_USER})"
13 |
14 | def generate_config
15 | template "testbot.yml.erb", "config/testbot.yml"
16 | template "testbot.rake.erb", "lib/tasks/testbot.rake"
17 | end
18 | end
19 |
20 |
--------------------------------------------------------------------------------
/lib/railtie.rb:
--------------------------------------------------------------------------------
1 | begin
2 | require 'rails'
3 | @rails_loaded = true
4 | rescue LoadError => ex
5 | @rails_loaded = false
6 | end
7 |
8 | if @rails_loaded
9 | module Testbot
10 | class Railtie < Rails::Railtie
11 | rake_tasks do
12 | load File.expand_path(File.join(File.dirname(__FILE__), "tasks/testbot.rake"))
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/requester/requester.rb:
--------------------------------------------------------------------------------
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
172 |
--------------------------------------------------------------------------------
/lib/runner/job.rb:
--------------------------------------------------------------------------------
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
113 |
--------------------------------------------------------------------------------
/lib/runner/runner.rb:
--------------------------------------------------------------------------------
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
223 |
--------------------------------------------------------------------------------
/lib/runner/safe_result_text.rb:
--------------------------------------------------------------------------------
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
30 |
--------------------------------------------------------------------------------
/lib/server/build.rb:
--------------------------------------------------------------------------------
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
37 |
--------------------------------------------------------------------------------
/lib/server/group.rb:
--------------------------------------------------------------------------------
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
49 |
--------------------------------------------------------------------------------
/lib/server/job.rb:
--------------------------------------------------------------------------------
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
65 |
--------------------------------------------------------------------------------
/lib/server/memory_model.rb:
--------------------------------------------------------------------------------
1 | class MemoryModel < OpenStruct
2 |
3 | @@db = {}
4 | @@types = {}
5 |
6 | def initialize(hash)
7 | @@types[self.class] ||= {}
8 | hash = resolve_types(symbolize_keys(hash))
9 | super(hash)
10 | end
11 |
12 | def id
13 | object_id
14 | end
15 |
16 | def type
17 | @table[:type]
18 | end
19 |
20 | def attributes
21 | @table
22 | end
23 |
24 | def update(hash)
25 | @table.merge!(resolve_types(symbolize_keys(hash)))
26 | self
27 | end
28 |
29 | def destroy
30 | self.class.all.delete_if { |b| b.id == id }
31 | end
32 |
33 | def self.find(id)
34 | all.find { |r| r.id == id.to_i }
35 | end
36 |
37 | def self.create(hash = {})
38 | all << new(hash)
39 | all[-1]
40 | end
41 |
42 | def self.all
43 | @@db[self] ||= []
44 | @@db[self]
45 | end
46 |
47 | def self.first
48 | all.first
49 | end
50 |
51 | def self.delete_all
52 | all.clear
53 | end
54 |
55 | def self.count
56 | all.size
57 | end
58 |
59 | def self.attribute(attribute, type)
60 | @@types[self] ||= {}
61 | @@types[self][attribute] = type
62 | end
63 |
64 | private
65 |
66 | def resolve_types(hash)
67 | hash.each { |attribute, value|
68 | case @@types[self.class][attribute]
69 | when :integer
70 | hash[attribute] = value.to_i
71 | when :boolean
72 | if value == "true"
73 | hash[attribute] = true
74 | elsif value == "false"
75 | hash[attribute] = false
76 | elsif value != true && value != false
77 | hash[attribute] = nil
78 | end
79 | end
80 | }
81 | hash
82 | end
83 |
84 | def symbolize_keys(hash)
85 | h = {}
86 | hash.each { |k, v| h[k.to_sym] = v }
87 | h
88 | end
89 |
90 | end
91 |
92 |
--------------------------------------------------------------------------------
/lib/server/runner.rb:
--------------------------------------------------------------------------------
1 | module Testbot::Server
2 |
3 | class Runner < MemoryModel
4 |
5 | attribute :idle_instances, :integer
6 | attribute :max_instances, :integer
7 |
8 | def self.record!(hash)
9 | create_or_update_by_mac!(hash)
10 | end
11 |
12 | def self.create_or_update_by_mac!(hash)
13 | if runner = find_by_uid(hash[:uid])
14 | runner.update hash
15 | else
16 | Runner.create hash
17 | end
18 | end
19 |
20 | def self.timeout
21 | 10
22 | end
23 |
24 | def self.find_by_uid(uid)
25 | all.find { |r| r.uid == uid }
26 | end
27 |
28 | def self.find_all_outdated
29 | all.find_all { |r| r.version != Testbot.version }
30 | end
31 |
32 | def self.find_all_available
33 | all.find_all { |r| r.idle_instances && r.version == Testbot.version && r.last_seen_at > (Time.now - Runner.timeout) }
34 | end
35 |
36 | def self.available_instances
37 | find_all_available.inject(0) { |sum, r| r.idle_instances + sum }
38 | end
39 |
40 | def self.total_instances
41 | return 1 if ENV['INTEGRATION_TEST']
42 | find_all_available.inject(0) { |sum, r| r.max_instances + sum }
43 | end
44 |
45 | end
46 |
47 | end
48 |
--------------------------------------------------------------------------------
/lib/server/server.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'sinatra'
3 | require 'yaml'
4 | require 'json'
5 | require File.expand_path(File.join(File.dirname(__FILE__), '/../shared/testbot'))
6 | require File.expand_path(File.join(File.dirname(__FILE__), 'memory_model.rb'))
7 | require File.expand_path(File.join(File.dirname(__FILE__), 'job.rb'))
8 | require File.expand_path(File.join(File.dirname(__FILE__), 'group.rb'))
9 | require File.expand_path(File.join(File.dirname(__FILE__), 'runner.rb'))
10 | require File.expand_path(File.join(File.dirname(__FILE__), 'build.rb'))
11 |
12 | module Testbot::Server
13 |
14 | if ENV['INTEGRATION_TEST']
15 | set :port, 22880
16 | else
17 | set :port, Testbot::SERVER_PORT
18 | end
19 |
20 | class Server
21 | def self.valid_version?(runner_version)
22 | Testbot.version == runner_version
23 | end
24 | end
25 |
26 | post '/builds' do
27 | if Runner.total_instances == 0
28 | [ 503, "No runners available" ]
29 | else
30 | Build.create_and_build_jobs(params).id.to_s
31 | end
32 | end
33 |
34 | get '/builds/:id' do
35 | build = Build.find(params[:id])
36 | build.destroy if build.done
37 | { "done" => build.done, "results" => build.results, "success" => build.success }.to_json
38 | end
39 |
40 | delete '/builds/:id' do
41 | build = Build.find(params[:id])
42 | build.destroy if build
43 | nil
44 | end
45 |
46 | get '/jobs/next' do
47 | next_job, runner = Job.next(params, @env['REMOTE_ADDR'])
48 | if next_job
49 | next_job.update(:taken_at => Time.now, :taken_by => runner)
50 | [ next_job.id, next_job.build.id, next_job.project, next_job.root, next_job.type, (next_job.jruby == 1 ? 'jruby' : 'ruby'), next_job.files ].join(',')
51 | end
52 | end
53 |
54 | put '/jobs/:id' do
55 | Job.find(params[:id]).update(:result => params[:result], :status => params[:status]); nil
56 | end
57 |
58 | get '/runners/ping' do
59 | return unless Server.valid_version?(params[:version])
60 | runner = Runner.find_by_uid(params[:uid])
61 | if runner
62 | runner.update(params.reject { |k, v| k == "build_id" }.merge({ :last_seen_at => Time.now, :build => Build.find(params[:build_id]) }))
63 | unless params[:build_id] == '' || params[:build_id] == nil || runner.build
64 | return "stop_build,#{params[:build_id]}"
65 | end
66 | end
67 | nil
68 | end
69 |
70 | get '/runners' do
71 | Runner.find_all_available.map { |r| r.attributes }.to_json
72 | end
73 |
74 | get '/runners/outdated' do
75 | Runner.find_all_outdated.map { |runner| [ runner.ip, runner.hostname, runner.uid ].join(' ') }.join("\n").strip
76 | end
77 |
78 | get '/runners/available_instances' do
79 | Runner.available_instances.to_s
80 | end
81 |
82 | get '/runners/total_instances' do
83 | Runner.total_instances.to_s
84 | end
85 |
86 | get '/runners/available' do
87 | Runner.find_all_available.map { |runner| [ runner.ip, runner.hostname, runner.uid, runner.username, runner.idle_instances ].join(' ') }.join("\n").strip
88 | end
89 |
90 | get '/version' do
91 | Testbot.version
92 | end
93 |
94 | get '/status' do
95 | File.read(File.join(File.dirname(__FILE__), '/status/status.html'))
96 | end
97 |
98 | get '/status/:dir/:file' do
99 | File.read(File.join(File.dirname(__FILE__), "/status/#{params[:dir]}/#{params[:file]}"))
100 | end
101 |
102 | end
103 |
104 |
--------------------------------------------------------------------------------
/lib/server/status/status.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Testbot status
5 |
6 |
7 |
8 |
41 |
42 |
43 | Testbot status
44 | Loading...
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/lib/server/status/stylesheets/status.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: black;
3 | color: white;
4 | font-family: Monaco, monospace; font-size: 10pt;
5 | }
6 |
7 | #total {
8 | margin-top: 20px;
9 | }
10 |
11 | #runners li {
12 | list-style: none;
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/lib/shared/adapters/adapter.rb:
--------------------------------------------------------------------------------
1 | class Adapter
2 |
3 | FILES = Dir[File.dirname(__FILE__) + "/*_adapter.rb"]
4 | FILES.each { |file| require(file) }
5 |
6 | def self.all
7 | FILES.map { |file| load_adapter(file) }
8 | end
9 |
10 | def self.find(type)
11 | if adapter = all.find { |adapter| adapter.type == type.to_s }
12 | adapter
13 | else
14 | raise "Unknown adapter: #{type}"
15 | end
16 | end
17 |
18 | private
19 |
20 | def self.load_adapter(file)
21 | eval("::" + File.basename(file).
22 | gsub(/\.rb/, '').
23 | gsub(/(?:^|_)(.)/) { $1.upcase })
24 | end
25 |
26 | end
27 |
28 |
--------------------------------------------------------------------------------
/lib/shared/adapters/cucumber_adapter.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), "/helpers/ruby_env"))
2 | require File.expand_path(File.join(File.dirname(__FILE__), "../color"))
3 |
4 | class CucumberAdapter
5 |
6 | def self.command(project_path, ruby_interpreter, files)
7 | cucumber_command = RubyEnv.ruby_command(project_path, :script => "script/cucumber", :bin => "cucumber",
8 | :ruby_interpreter => ruby_interpreter)
9 | "export AUTOTEST=1; #{cucumber_command} -f progress --backtrace -r features/support -r features/step_definitions #{files} -t ~@disabled"
10 | end
11 |
12 | def self.test_files(dir)
13 | Dir["#{dir}/#{file_pattern}"]
14 | end
15 |
16 | def self.get_sizes(files)
17 | files.map { |file| File.stat(file).size }
18 | end
19 |
20 | def self.requester_port
21 | 2230
22 | end
23 |
24 | def self.pluralized
25 | 'features'
26 | end
27 |
28 | def self.base_path
29 | pluralized
30 | end
31 |
32 | def self.name
33 | 'Cucumber'
34 | end
35 |
36 | def self.type
37 | pluralized
38 | end
39 |
40 | # This is an optional method. It gets passed the entire test result and summarizes it. See the tests.
41 | def self.sum_results(text)
42 | scenarios, steps = parse_scenarios_and_steps(text)
43 |
44 | scenarios_line = "#{scenarios[:total]} scenarios (" + [
45 | (Color.colorize("#{scenarios[:failed]} failed", :red) if scenarios[:failed] > 0),
46 | (Color.colorize("#{scenarios[:undefined]} undefined", :orange) if scenarios[:undefined] > 0),
47 | (Color.colorize("#{scenarios[:passed]} passed", :green) if scenarios[:passed] > 0)
48 | ].compact.join(', ') + ")"
49 |
50 | steps_line = "#{steps[:total]} steps (" + [
51 | (Color.colorize("#{steps[:failed]} failed", :red) if steps[:failed] > 0),
52 | (Color.colorize("#{steps[:skipped]} skipped", :cyan) if steps[:skipped] > 0),
53 | (Color.colorize("#{steps[:undefined]} undefined", :orange) if steps[:undefined] > 0),
54 | (Color.colorize("#{steps[:passed]} passed", :green) if steps[:passed] > 0)
55 | ].compact.join(', ') + ")"
56 |
57 | scenarios_line + "\n" + steps_line
58 | end
59 |
60 | private
61 |
62 | def self.parse_scenarios_and_steps(text)
63 | results = {
64 | :scenarios => { :total => 0, :passed => 0, :failed => 0, :undefined => 0 },
65 | :steps => { :total => 0, :passed => 0, :failed => 0, :skipped => 0, :undefined => 0 }
66 | }
67 |
68 | Color.strip(text).split("\n").each do |line|
69 | type = line.include?("scenarios") ? :scenarios : :steps
70 |
71 | if match = line.match(/\((.+)\)/)
72 | results[type][:total] += line.split.first.to_i
73 | parse_status_counts(results[type], match[1])
74 | end
75 | end
76 |
77 | [ results[:scenarios], results[:steps] ]
78 | end
79 |
80 | def self.parse_status_counts(results, status_counts)
81 | status_counts.split(', ').each do |part|
82 | results.keys.each do |key|
83 | results[key] += part.split.first.to_i if part.include?(key.to_s)
84 | end
85 | end
86 | end
87 |
88 | def self.file_pattern
89 | '**/**/*.feature'
90 | end
91 | end
92 |
--------------------------------------------------------------------------------
/lib/shared/adapters/helpers/ruby_env.rb:
--------------------------------------------------------------------------------
1 | class RubyEnv
2 | def self.bundler?(project_path)
3 | gem_exists?("bundler") && File.exists?("#{project_path}/Gemfile")
4 | end
5 |
6 | def self.gem_exists?(gem)
7 | if Gem::Specification.respond_to?(:find_by_name)
8 | Gem::Specification.find_by_name(gem)
9 | else
10 | # older depricated method
11 | Gem.available?(gem)
12 | end
13 | rescue Gem::LoadError
14 | false
15 | end
16 |
17 | def self.ruby_command(project_path, opts = {})
18 | ruby_interpreter = opts[:ruby_interpreter] || "ruby"
19 |
20 | if opts[:script] && File.exists?("#{project_path}/#{opts[:script]}")
21 | command = opts[:script]
22 | elsif opts[:bin]
23 | command = opts[:bin]
24 | else
25 | command = nil
26 | end
27 |
28 | if bundler?(project_path)
29 | "#{rvm_prefix(project_path)} #{ruby_interpreter} -S bundle exec #{command}".strip
30 | else
31 | "#{rvm_prefix(project_path)} #{ruby_interpreter} -S #{command}".strip
32 | end
33 | end
34 |
35 | def self.rvm_prefix(project_path)
36 | if rvm?
37 | rvmrc_path = File.join project_path, ".rvmrc"
38 | if File.exists?(rvmrc_path)
39 | File.read(rvmrc_path).to_s.strip + " exec"
40 | end
41 | end
42 | end
43 |
44 | def self.rvm?
45 | system("rvm info") != nil
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/shared/adapters/rspec2_adapter.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), "/helpers/ruby_env"))
2 |
3 | class Rspec2Adapter
4 |
5 | def self.command(project_path, ruby_interpreter, files)
6 | spec_command = RubyEnv.ruby_command(project_path,
7 | :bin => "rspec",
8 | :ruby_interpreter => ruby_interpreter)
9 |
10 | if File.exists?("#{project_path}/spec/spec.opts")
11 | spec_command += " -O spec/spec.opts"
12 | end
13 |
14 | "export RSPEC_COLOR=true; #{spec_command} #{files}"
15 | end
16 |
17 | def self.test_files(dir)
18 | Dir["#{dir}/#{file_pattern}"]
19 | end
20 |
21 | def self.get_sizes(files)
22 | files.map { |file| File.stat(file).size }
23 | end
24 |
25 | def self.requester_port
26 | 2299
27 | end
28 |
29 | def self.pluralized
30 | 'specs'
31 | end
32 |
33 | def self.base_path
34 | "spec"
35 | end
36 |
37 | def self.name
38 | 'RSpec2'
39 | end
40 |
41 | def self.type
42 | 'rspec'
43 | end
44 |
45 | private
46 |
47 | def self.file_pattern
48 | '**/**/*_spec.rb'
49 | end
50 |
51 | end
52 |
--------------------------------------------------------------------------------
/lib/shared/adapters/rspec_adapter.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), "/helpers/ruby_env"))
2 | require File.expand_path(File.join(File.dirname(__FILE__), "../color"))
3 |
4 | class RspecAdapter
5 |
6 | def self.command(project_path, ruby_interpreter, files)
7 | spec_command = RubyEnv.ruby_command(project_path, :script => "script/spec", :bin => "rspec",
8 | :ruby_interpreter => ruby_interpreter)
9 | if File.exists?("#{project_path}/spec/spec.opts")
10 | spec_command += " -O spec/spec.opts"
11 | end
12 |
13 | "export RSPEC_COLOR=true; #{spec_command} #{files}"
14 | end
15 |
16 | def self.test_files(dir)
17 | Dir["#{dir}/#{file_pattern}"]
18 | end
19 |
20 | def self.get_sizes(files)
21 | files.map { |file| File.stat(file).size }
22 | end
23 |
24 | def self.requester_port
25 | 2299
26 | end
27 |
28 | def self.pluralized
29 | 'specs'
30 | end
31 |
32 | def self.base_path
33 | type
34 | end
35 |
36 | def self.name
37 | 'RSpec'
38 | end
39 |
40 | def self.type
41 | 'spec'
42 | end
43 |
44 | # This is an optional method. It gets passed the entire test result and summarizes it. See the tests.
45 | def self.sum_results(results)
46 | examples, failures, pending = 0, 0, 0
47 | results.split("\n").each do |line|
48 | line =~ /(\d+) examples?, (\d+) failures?(, (\d+) pending)?/
49 | next unless $1
50 | examples += $1.to_i
51 | failures += $2.to_i
52 | pending += $4.to_i
53 | end
54 |
55 | result = [ pluralize(examples, 'example'), pluralize(failures, 'failure'), (pending > 0 ? "#{pending} pending" : nil) ].compact.join(', ')
56 | if failures == 0 && pending == 0
57 | Color.colorize(result, :green)
58 | elsif failures == 0 && pending > 0
59 | Color.colorize(result, :orange)
60 | else
61 | Color.colorize(result, :red)
62 | end
63 | end
64 |
65 | private
66 |
67 | def self.pluralize(count, singular)
68 | if count == 1
69 | "#{count} #{singular}"
70 | else
71 | "#{count} #{singular}s"
72 | end
73 | end
74 |
75 | def self.file_pattern
76 | '**/**/*_spec.rb'
77 | end
78 |
79 | end
80 |
--------------------------------------------------------------------------------
/lib/shared/adapters/test_unit_adapter.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), "/helpers/ruby_env"))
2 |
3 | class TestUnitAdapter
4 |
5 | def self.command(project_path, ruby_interpreter, files)
6 | ruby_command = RubyEnv.ruby_command(project_path, :ruby_interpreter => ruby_interpreter)
7 | %{#{ruby_command} -Itest -e '%w(#{files}).each { |file| require(Dir.pwd + "/" + file) }'}
8 | end
9 |
10 | def self.test_files(dir)
11 | Dir["#{dir}/#{file_pattern}"]
12 | end
13 |
14 | def self.get_sizes(files)
15 | files.map { |file| File.stat(file).size }
16 | end
17 |
18 | def self.requester_port
19 | 2231
20 | end
21 |
22 | def self.pluralized
23 | 'tests'
24 | end
25 |
26 | def self.base_path
27 | type
28 | end
29 |
30 | def self.name
31 | 'Test::Unit'
32 | end
33 |
34 | def self.type
35 | 'test'
36 | end
37 |
38 | private
39 |
40 | def self.file_pattern
41 | '**/**/*_test.rb'
42 | end
43 |
44 | end
45 |
--------------------------------------------------------------------------------
/lib/shared/color.rb:
--------------------------------------------------------------------------------
1 | class Color
2 | def self.colorize(text, color)
3 | colors = { :green => 32, :orange => 33, :red => 31, :cyan => 36 }
4 |
5 | if colors[color]
6 | "\033[#{colors[color]}m#{text}\033[0m"
7 | else
8 | raise "Color not implemented: #{color}"
9 | end
10 | end
11 |
12 | def self.strip(text)
13 | text.gsub(/\e.+?m/, '')
14 | end
15 | end
16 |
17 |
--------------------------------------------------------------------------------
/lib/shared/simple_daemonize.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'daemons'
3 |
4 | class SimpleDaemonize
5 |
6 | def self.start(proc, pid_path, app_name)
7 | working_dir = Dir.pwd
8 |
9 | group = Daemons::ApplicationGroup.new(app_name)
10 | group.new_application(:mode => :none).start
11 |
12 | File.open(pid_path, 'w') { |file| file.write(Process.pid) }
13 | Dir.chdir(working_dir)
14 | proc.call
15 | end
16 |
17 | def self.stop(pid_path)
18 | return unless File.exists?(pid_path)
19 | pid = File.read(pid_path)
20 |
21 | system "kill -9 #{pid} &> /dev/null"
22 | system "rm #{pid_path} &> /dev/null"
23 | end
24 |
25 | end
26 |
--------------------------------------------------------------------------------
/lib/shared/ssh_tunnel.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'net/ssh'
3 |
4 | class SSHTunnel
5 | def initialize(host, user, local_port = 2288)
6 | @host, @user, @local_port = host, user, local_port
7 | end
8 |
9 | def open
10 | connect
11 |
12 | start_time = Time.now
13 | while true
14 | break if @up
15 | sleep 0.5
16 |
17 | if Time.now - start_time > 5
18 | puts "SSH connection failed, trying again..."
19 | start_time = Time.now
20 | connect
21 | end
22 | end
23 | end
24 |
25 | def connect
26 | @thread.kill if @thread
27 | @thread = Thread.new do
28 | Net::SSH.start(@host, @user, { :timeout => 1 }) do |ssh|
29 | ssh.forward.local(@local_port, 'localhost', Testbot::SERVER_PORT)
30 | ssh.loop { @up = true }
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/shared/testbot.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '/version'))
2 | require File.expand_path(File.join(File.dirname(__FILE__), '/simple_daemonize'))
3 | require File.expand_path(File.join(File.dirname(__FILE__), '/adapters/adapter'))
4 | require 'fileutils'
5 |
6 | module Testbot
7 | require 'railtie' if defined?(Rails)
8 |
9 | if ENV['INTEGRATION_TEST']
10 | SERVER_PID = "/tmp/integration_test_testbot_server.pid"
11 | RUNNER_PID = "/tmp/integration_test_testbot_runner.pid"
12 | else
13 | SERVER_PID = "/tmp/testbot_server.pid"
14 | RUNNER_PID = "/tmp/testbot_runner.pid"
15 | end
16 |
17 | DEFAULT_WORKING_DIR = "/tmp/testbot"
18 | DEFAULT_SERVER_PATH = "/tmp/testbot/#{ENV['USER']}"
19 | DEFAULT_USER = "testbot"
20 | DEFAULT_PROJECT = "project"
21 | DEFAULT_RUNNER_USAGE = "100%"
22 | SERVER_PORT = ENV['INTEGRATION_TEST'] ? 22880 : 2288
23 |
24 | class CLI
25 |
26 | def self.run(argv)
27 | return false if argv == []
28 | opts = parse_args(argv)
29 |
30 | if opts[:help]
31 | return false
32 | elsif opts[:version]
33 | puts "Testbot #{Testbot.version}"
34 | elsif [ true, 'run', 'start' ].include?(opts[:server])
35 | start_server(opts[:server])
36 | elsif opts[:server] == 'stop'
37 | stop('server', Testbot::SERVER_PID)
38 | elsif [ true, 'run', 'start' ].include?(opts[:runner])
39 | require File.expand_path(File.join(File.dirname(__FILE__), '/../runner/runner'))
40 | return false unless valid_runner_opts?(opts)
41 | start_runner(opts)
42 | elsif opts[:runner] == 'stop'
43 | stop('runner', Testbot::RUNNER_PID)
44 | elsif adapter = Adapter.all.find { |adapter| opts[adapter.type.to_sym] }
45 | require File.expand_path(File.join(File.dirname(__FILE__), '/../requester/requester'))
46 | start_requester(opts, adapter)
47 | end
48 |
49 | true
50 | end
51 |
52 | def self.parse_args(argv)
53 | last_setter = nil
54 | hash = {}
55 | str = ''
56 | argv.each_with_index do |arg, i|
57 | if arg.include?('--')
58 | str = ''
59 | last_setter = arg.split('--').last.to_sym
60 | hash[last_setter] = true if (i == argv.size - 1) || argv[i+1].include?('--')
61 | else
62 | str += ' ' + arg
63 | hash[last_setter] = str.strip
64 | end
65 | end
66 | hash
67 | end
68 |
69 | def self.start_runner(opts)
70 | stop('runner', Testbot::RUNNER_PID)
71 |
72 | proc = lambda {
73 | working_dir = opts[:working_dir] || Testbot::DEFAULT_WORKING_DIR
74 | FileUtils.mkdir_p(working_dir)
75 | Dir.chdir(working_dir)
76 | runner = Runner::Runner.new(:server_host => opts[:connect],
77 | :auto_update => opts[:auto_update], :max_instances => opts[:cpus],
78 | :ssh_tunnel => opts[:ssh_tunnel], :server_user => opts[:user],
79 | :max_jruby_instances => opts[:max_jruby_instances],
80 | :dev_gem_root => opts[:dev_gem_root],
81 | :wait_for_updated_gem => opts[:wait_for_updated_gem],
82 | :jruby_opts => opts[:jruby_opts])
83 | runner.run!
84 | }
85 |
86 | if opts[:runner] == 'run'
87 | proc.call
88 | else
89 | puts "Testbot runner started (pid: #{Process.pid})"
90 | SimpleDaemonize.start(proc, Testbot::RUNNER_PID, "testbot (runner)")
91 | end
92 | end
93 |
94 | def self.start_server(type)
95 | stop('server', Testbot::SERVER_PID)
96 | require File.expand_path(File.join(File.dirname(__FILE__), '/../server/server'))
97 |
98 | if type == 'run'
99 | Sinatra::Application.run! :environment => "production"
100 | else
101 | puts "Testbot server started (pid: #{Process.pid})"
102 | SimpleDaemonize.start(lambda {
103 | Sinatra::Application.run! :environment => "production"
104 | }, Testbot::SERVER_PID, "testbot (server)")
105 | end
106 | end
107 |
108 | def self.stop(name, pid)
109 | puts "Testbot #{name} stopped" if SimpleDaemonize.stop(pid)
110 | end
111 |
112 | def self.start_requester(opts, adapter)
113 | requester = Requester::Requester.new(:server_host => opts[:connect],
114 | :rsync_path => opts[:rsync_path],
115 | :rsync_ignores => opts[:rsync_ignores].to_s,
116 | :available_runner_usage => nil,
117 | :project => opts[:project],
118 | :ssh_tunnel => opts[:ssh_tunnel], :server_user => opts[:user])
119 | requester.run_tests(adapter, adapter.base_path)
120 | end
121 |
122 | def self.valid_runner_opts?(opts)
123 | opts[:connect].is_a?(String)
124 | end
125 |
126 | def self.lib_path
127 | File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
128 | end
129 |
130 | end
131 |
132 | end
133 |
--------------------------------------------------------------------------------
/lib/shared/version.rb:
--------------------------------------------------------------------------------
1 | module Testbot
2 | # Don't forget to update readme and changelog
3 | def self.version
4 | version = "0.7.9"
5 | dev_version_file = File.join(File.dirname(__FILE__), '..', '..', 'DEV_VERSION')
6 | if File.exists?(dev_version_file)
7 | version += File.read(dev_version_file)
8 | end
9 | version
10 | end
11 | end
12 |
13 |
--------------------------------------------------------------------------------
/lib/tasks/testbot.rake:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/../shared/adapters/adapter'
2 |
3 | namespace :testbot do
4 |
5 | def run_and_show_results(adapter, custom_path)
6 | 'testbot:before_request'.tap { |t| Rake::Task.task_defined?(t) && Rake::Task[t].invoke }
7 |
8 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'requester', 'requester.rb'))
9 | requester = Testbot::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 "Finished in #{Time.now - start_time} seconds."
19 | success
20 | end
21 |
22 | Adapter.all.each do |adapter|
23 |
24 | desc "Run the #{adapter.name} tests using testbot"
25 | task adapter.type, :custom_path do |_, args|
26 | exit 1 unless run_and_show_results(adapter, args[:custom_path])
27 | end
28 |
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/testbot.rb:
--------------------------------------------------------------------------------
1 | # Rails plugin hook
2 | require File.expand_path(File.join(File.dirname(__FILE__), '/shared/testbot'))
3 |
--------------------------------------------------------------------------------
/script/test:
--------------------------------------------------------------------------------
1 | turbux_rspec
--------------------------------------------------------------------------------
/test/fixtures/local/Rakefile:
--------------------------------------------------------------------------------
1 | namespace :testbot do
2 |
3 | task :before_run do
4 | puts "prepare got called"
5 | end
6 |
7 | end
8 |
--------------------------------------------------------------------------------
/test/fixtures/local/config/testbot.yml:
--------------------------------------------------------------------------------
1 | server_host: localhost
2 | rsync_path: ../server
3 | available_runner_usage: 100%
4 | rsync_ignores: log/* tmp/*
5 |
6 |
--------------------------------------------------------------------------------
/test/fixtures/local/log/test.log:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joakimk/testbot/d200b0ff53b7c1b886ff515fc0d160d41067b13a/test/fixtures/local/log/test.log
--------------------------------------------------------------------------------
/test/fixtures/local/script/spec:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | puts "script/spec got called with #{ARGV.inspect}"
3 |
--------------------------------------------------------------------------------
/test/fixtures/local/spec/models/car_spec.rb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joakimk/testbot/d200b0ff53b7c1b886ff515fc0d160d41067b13a/test/fixtures/local/spec/models/car_spec.rb
--------------------------------------------------------------------------------
/test/fixtures/local/spec/models/house_spec.rb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joakimk/testbot/d200b0ff53b7c1b886ff515fc0d160d41067b13a/test/fixtures/local/spec/models/house_spec.rb
--------------------------------------------------------------------------------
/test/fixtures/local/spec/spec.opts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joakimk/testbot/d200b0ff53b7c1b886ff515fc0d160d41067b13a/test/fixtures/local/spec/spec.opts
--------------------------------------------------------------------------------
/test/fixtures/local/tmp/restart.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joakimk/testbot/d200b0ff53b7c1b886ff515fc0d160d41067b13a/test/fixtures/local/tmp/restart.txt
--------------------------------------------------------------------------------
/test/integration_test.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'test/unit'
3 | require 'fileutils'
4 | require 'shoulda'
5 |
6 | class IntegrationTest < Test::Unit::TestCase
7 | # This is slow, and Test:Unit does not have "before/after :all" method, so I'm using a single testcase for multiple tests
8 | should "be able to send a build request, have it run and show the results" do
9 | Thread.new {
10 |
11 | sleep 30
12 | puts "Still running after 30 secs, stopping..."
13 | stop
14 | }
15 |
16 | cleanup
17 | system "mkdir -p tmp/fixtures; cp -rf test/fixtures/local tmp/local"
18 | system "export INTEGRATION_TEST=true; bin/testbot --runner --connect 127.0.0.1 --working_dir tmp/runner > /dev/null"
19 | system "export INTEGRATION_TEST=true; bin/testbot --server > /dev/null"
20 |
21 | # For debug
22 | # Thread.new do
23 | # system "export INTEGRATION_TEST=true; bin/testbot --runner run --connect 127.0.0.1 --working_dir tmp/runner"
24 | # end
25 | # Thread.new do
26 | # system "export INTEGRATION_TEST=true; bin/testbot --server run"
27 | # end
28 |
29 | sleep 2.0
30 | result = `cd tmp/local; INTEGRATION_TEST=true ../../bin/testbot --spec --connect 127.0.0.1 --rsync_path ../server --rsync_ignores "log/* tmp/*"`
31 |
32 | # Should include the result from script/spec
33 | #puts result.inspect
34 | assert result.include?('script/spec got called with ["-O", "spec/spec.opts", "spec/models/house_spec.rb", "spec/models/car_spec.rb"]') ||
35 | result.include?('script/spec got called with ["-O", "spec/spec.opts", "spec/models/car_spec.rb", "spec/models/house_spec.rb"]')
36 |
37 |
38 | # Should not include ignored files
39 | assert !File.exists?("tmp/server/log/test.log")
40 | assert !File.exists?("tmp/server/tmp/restart.txt")
41 | assert !File.exists?("tmp/runner/local/log/test.log")
42 | assert !File.exists?("tmp/runner/local/tmp/restart.txt")
43 | end
44 |
45 | def teardown
46 | stop
47 | cleanup
48 | end
49 |
50 | def stop
51 | system "export INTEGRATION_TEST=true; bin/testbot --server stop > /dev/null"
52 | system "export INTEGRATION_TEST=true; bin/testbot --runner stop > /dev/null"
53 | end
54 |
55 | def cleanup
56 | system "rm -rf tmp/local tmp/fixtures"
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/test/requester/requester_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/requester/requester.rb'))
2 | require 'test/unit'
3 | require 'shoulda'
4 | require 'flexmock/test_unit'
5 |
6 | # Probably a bug in flexmock, for 1.9.2
7 | unless defined?(Test::Unit::AssertionFailedError)
8 | class Test::Unit::AssertionFailedError
9 | end
10 | end
11 |
12 | module Testbot::Requester
13 |
14 | class RequesterTest < Test::Unit::TestCase
15 |
16 | def requester_with_result(results)
17 | requester = Requester.new(:server_host => "192.168.1.100", :rsync_path => 'user@server:/tmp/somewhere')
18 |
19 | flexmock(requester).should_receive(:find_tests).and_return([])
20 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id)
21 | flexmock(requester).should_receive(:sleep).once
22 | flexmock(requester).should_receive(:print).once
23 | flexmock(requester).should_receive(:puts).once
24 | flexmock(requester).should_receive(:system)
25 | flexmock(HTTParty).should_receive(:get).once.with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
26 | :format => :json).and_return({ "done" => true, "results" => results })
27 | requester
28 | end
29 |
30 | def response_with_build_id
31 | OpenStruct.new(:response => OpenStruct.new(:code => "200", :body => "5"))
32 | end
33 |
34 | def error_response(opts = {})
35 | OpenStruct.new(:response => OpenStruct.new(opts))
36 | end
37 |
38 | def build_with_result(results)
39 | requester_with_result(results).run_tests(RspecAdapter, 'spec')
40 | end
41 |
42 | def setup
43 | ENV['USE_JRUBY'] = nil
44 | ENV['IN_TEST'] = 'true'
45 | end
46 |
47 | def mock_file_sizes
48 | flexmock(File).should_receive(:stat).and_return(mock = Object.new)
49 | flexmock(mock).should_receive(:size).and_return(0)
50 | end
51 |
52 | def fixture_path(local_path)
53 | File.join(File.dirname(__FILE__), local_path)
54 | end
55 |
56 | context "self.create_by_config" do
57 |
58 | should 'create a requester from config' do
59 | requester = Requester.create_by_config(fixture_path("testbot.yml"))
60 | assert_equal 'hostname', requester.config.server_host
61 | assert_equal '/path', requester.config.rsync_path
62 | assert_equal '.git tmp', requester.config.rsync_ignores
63 | assert_equal 'appname', requester.config.project
64 | assert_equal false, requester.config.ssh_tunnel
65 | assert_equal 'user', requester.config.server_user
66 | assert_equal '50%', requester.config.available_runner_usage
67 | end
68 |
69 | should 'accept ERB-snippets in testbot.yml' do
70 | requester = Requester.create_by_config(fixture_path("testbot_with_erb.yml"))
71 | assert_equal 'dynamic_host', requester.config.server_host
72 | assert_equal '50%', requester.config.available_runner_usage
73 | end
74 | end
75 |
76 | context "initialize" do
77 |
78 | should "use defaults when values are missing" do
79 | expected = { :server_host => 'hostname',
80 | :rsync_path => Testbot::DEFAULT_SERVER_PATH,
81 | :project => Testbot::DEFAULT_PROJECT,
82 | :server_user => Testbot::DEFAULT_USER,
83 | :available_runner_usage => Testbot::DEFAULT_RUNNER_USAGE }
84 |
85 | actual = Requester.new({ "server_host" => 'hostname' }).config
86 |
87 | assert_equal OpenStruct.new(expected), actual
88 | end
89 |
90 | end
91 |
92 | context "run_tests" do
93 |
94 | should "should be able to create a build" do
95 | requester = Requester.new(:server_host => "192.168.1.100", :rsync_path => '/path', :available_runner_usage => '60%', :project => 'things', :server_user => "cruise")
96 | flexmock(RspecAdapter).should_receive(:test_files).with('spec').once.and_return([ 'spec/models/house_spec.rb', 'spec/models/car_spec.rb' ])
97 |
98 | flexmock(File).should_receive(:stat).once.with("spec/models/house_spec.rb").and_return(mock = Object.new); flexmock(mock).should_receive(:size).and_return(10)
99 | flexmock(File).should_receive(:stat).once.with("spec/models/car_spec.rb").and_return(mock = Object.new); flexmock(mock).should_receive(:size).and_return(20)
100 |
101 | flexmock(HTTParty).should_receive(:post).once.with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds",
102 | :body => { :type => "spec",
103 | :root => "cruise@192.168.1.100:/path",
104 | :project => "things",
105 | :available_runner_usage => "60%",
106 | :files => "spec/models/house_spec.rb" +
107 | " spec/models/car_spec.rb",
108 | :sizes => "10 20",
109 | :jruby => false }).and_return(response_with_build_id)
110 |
111 | flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, 'results' => '', "success" => true })
112 | flexmock(requester).should_receive(:sleep)
113 | flexmock(requester).should_receive(:print)
114 | flexmock(requester).should_receive(:puts)
115 | flexmock(requester).should_receive(:system)
116 |
117 | assert_equal true, requester.run_tests(RspecAdapter, 'spec')
118 | end
119 |
120 | should "print a message and exit if the status is 503" do
121 | requester = Requester.new(:server_host => "192.168.1.100")
122 |
123 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ])
124 | flexmock(requester).should_receive(:system)
125 |
126 | flexmock(HTTParty).should_receive(:post).and_return(error_response(:code => "503"))
127 | flexmock(requester).should_receive(:puts)
128 | assert_equal false, requester.run_tests(RspecAdapter, 'spec')
129 | end
130 |
131 | should "print what the server returns in case there is anything but a 200 response" do
132 | requester = Requester.new(:server_host => "192.168.1.100")
133 |
134 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ])
135 | flexmock(requester).should_receive(:system)
136 |
137 | flexmock(HTTParty).should_receive(:post).and_return(error_response(:code => "123", :body => "Some error"))
138 | flexmock(requester).should_receive(:puts).with("Could not create build, 123: Some error")
139 | assert_equal false, requester.run_tests(RspecAdapter, 'spec')
140 | end
141 |
142 | should "print the sum of results formatted by the adapter" do
143 | requester = Requester.new(:server_host => "192.168.1.100")
144 |
145 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ])
146 | flexmock(requester).should_receive(:system)
147 |
148 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id)
149 |
150 | flexmock(HTTParty).should_receive(:get).times(2).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
151 | :format => :json).and_return({ "done" => false, "results" => "job 2 done: ...." },
152 | { "done" => true, "results" => "job 2 done: ....job 1 done: ...." })
153 | mock_file_sizes
154 |
155 | flexmock(requester).should_receive(:sleep).times(2).with(0.5)
156 | flexmock(requester).should_receive(:print).once.with("job 2 done: ....")
157 | flexmock(requester).should_receive(:print).once.with("job 1 done: ....")
158 | flexmock(requester).should_receive(:puts).once.with("\nformatted result")
159 |
160 | flexmock(RspecAdapter).should_receive(:sum_results).with("job 2 done: ....job 1 done: ....").and_return("formatted result")
161 | requester.run_tests(RspecAdapter, 'spec')
162 | end
163 |
164 | should "keep calling the server for results until done" do
165 | requester = Requester.new(:server_host => "192.168.1.100")
166 |
167 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ])
168 | flexmock(requester).should_receive(:system)
169 |
170 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id)
171 |
172 | flexmock(HTTParty).should_receive(:get).times(2).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
173 | :format => :json).and_return({ "done" => false, "results" => "job 2 done: ...." },
174 | { "done" => true, "results" => "job 2 done: ....job 1 done: ...." })
175 | mock_file_sizes
176 |
177 | flexmock(requester).should_receive(:sleep).times(2).with(0.5)
178 | flexmock(requester).should_receive(:print).once.with("job 2 done: ....")
179 | flexmock(requester).should_receive(:print).once.with("job 1 done: ....")
180 | flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m")
181 |
182 | requester.run_tests(RspecAdapter, 'spec')
183 | end
184 |
185 | should "return false if not successful" do
186 | requester = Requester.new(:server_host => "192.168.1.100")
187 |
188 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ])
189 | flexmock(requester).should_receive(:system)
190 |
191 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id)
192 |
193 | flexmock(HTTParty).should_receive(:get).once.with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
194 | :format => :json).and_return({ "success" => false, "done" => true, "results" => "job 2 done: ....job 1 done: ...." })
195 |
196 | flexmock(requester).should_receive(:sleep).once.with(0.5)
197 | flexmock(requester).should_receive(:print).once.with("job 2 done: ....job 1 done: ....")
198 | flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m")
199 | mock_file_sizes
200 |
201 | assert_equal false, requester.run_tests(RspecAdapter, 'spec')
202 | end
203 |
204 | should "not print empty lines when there is no result" do
205 | requester = Requester.new(:server_host => "192.168.1.100")
206 |
207 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ])
208 | flexmock(requester).should_receive(:system)
209 |
210 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id)
211 |
212 | flexmock(HTTParty).should_receive(:get).times(2).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
213 | :format => :json).and_return({ "done" => false, "results" => "" },
214 | { "done" => true, "results" => "job 2 done: ....job 1 done: ...." })
215 |
216 | flexmock(requester).should_receive(:sleep).times(2).with(0.5)
217 | flexmock(requester).should_receive(:print).once.with("job 2 done: ....job 1 done: ....")
218 | flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m")
219 | mock_file_sizes
220 |
221 | requester.run_tests(RspecAdapter, 'spec')
222 | end
223 |
224 | should "sync the files to the server" do
225 | requester = Requester.new(:server_host => "192.168.1.100", :rsync_path => '/path', :rsync_ignores => '.git tmp')
226 |
227 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ])
228 | flexmock(requester).should_receive(:system)
229 |
230 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id)
231 | flexmock(requester).should_receive(:sleep).once
232 | flexmock(requester).should_receive(:print)
233 | flexmock(requester).should_receive(:puts)
234 | flexmock(HTTParty).should_receive(:get).once.with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
235 | :format => :json).and_return({ "done" => true, "results" => "" })
236 |
237 | flexmock(requester).should_receive('system').with("rsync -az --delete --delete-excluded -e ssh --exclude='.git' --exclude='tmp' . testbot@192.168.1.100:/path")
238 | mock_file_sizes
239 |
240 | requester.run_tests(RspecAdapter, 'spec')
241 | end
242 |
243 | should "just try again if the request encounters an error while running and print on the fith time" do
244 | requester = Requester.new(:server_host => "192.168.1.100")
245 |
246 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ])
247 | flexmock(requester).should_receive(:system)
248 |
249 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id)
250 |
251 | flexmock(HTTParty).should_receive(:get).times(5).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
252 | :format => :json).and_raise('some connection error')
253 | flexmock(HTTParty).should_receive(:get).times(1).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
254 | :format => :json).and_return({ "done" => true, "results" => "job 2 done: ....job 1 done: ...." })
255 |
256 | flexmock(requester).should_receive(:sleep).times(6).with(0.5)
257 | flexmock(requester).should_receive(:puts).once.with("Failed to get status: some connection error")
258 | flexmock(requester).should_receive(:print).once.with("job 2 done: ....job 1 done: ....")
259 | flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m")
260 | mock_file_sizes
261 |
262 | requester.run_tests(RspecAdapter, 'spec')
263 | end
264 |
265 | should "just try again if the status returns as nil" do
266 | requester = Requester.new(:server_host => "192.168.1.100")
267 |
268 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ])
269 | flexmock(requester).should_receive(:system)
270 |
271 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id)
272 |
273 | flexmock(HTTParty).should_receive(:get).times(2).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
274 | :format => :json).and_return(nil,
275 | { "done" => true, "results" => "job 2 done: ....job 1 done: ...." })
276 |
277 | flexmock(requester).should_receive(:sleep).times(2).with(0.5)
278 | flexmock(requester).should_receive(:print).once.with("job 2 done: ....job 1 done: ....")
279 | flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m")
280 | mock_file_sizes
281 |
282 | requester.run_tests(RspecAdapter, 'spec')
283 | end
284 |
285 | should "remove unnessesary output from rspec when told to do so" do
286 | requester = Requester.new(:server_host => "192.168.1.100", :simple_output => true)
287 |
288 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ])
289 | flexmock(requester).should_receive(:system)
290 |
291 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id)
292 |
293 | flexmock(HTTParty).should_receive(:get).times(2).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
294 | :format => :json).and_return(nil,
295 | { "done" => true, "results" => "testbot4:\n....\n\nFinished in 84.333 seconds\n\n206 examples, 0 failures, 2 pending; testbot4:\n.F..\n\nFinished in 84.333 seconds\n\n206 examples, 0 failures, 2 pending" })
296 |
297 | flexmock(requester).should_receive(:sleep).times(2).with(0.5)
298 |
299 | # Imperfect match, includes "." in 84.333, but good enough.
300 | flexmock(requester).should_receive(:print).once.with("......F...")
301 | flexmock(requester).should_receive(:print)
302 | flexmock(requester).should_receive(:puts)
303 | mock_file_sizes
304 |
305 | requester.run_tests(RspecAdapter, 'spec')
306 | end
307 |
308 | should "use SSHTunnel when specified (with a port that does not collide with the runner)" do
309 | requester = Requester.new(:ssh_tunnel => true, :server_host => "somewhere")
310 | flexmock(requester).should_receive(:system)
311 |
312 | flexmock(SSHTunnel).should_receive(:new).once.with("somewhere", "testbot", 2299).and_return(ssh_tunnel = Object.new)
313 | flexmock(ssh_tunnel).should_receive(:open).once
314 |
315 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb' ])
316 | flexmock(HTTParty).should_receive(:post).with("http://127.0.0.1:2299/builds", any).and_return(response_with_build_id)
317 | flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, "results" => "job 1 done: ...." })
318 | flexmock(requester).should_receive(:sleep)
319 | flexmock(requester).should_receive(:print)
320 | flexmock(requester).should_receive(:puts)
321 | mock_file_sizes
322 |
323 | requester.run_tests(RspecAdapter, 'spec')
324 | end
325 |
326 | should "use another user for rsync and ssh_tunnel when specified" do
327 | requester = Requester.new(:ssh_tunnel => true, :server_host => "somewhere",
328 | :server_user => "cruise", :rsync_path => "/tmp/testbot/foo")
329 |
330 | flexmock(SSHTunnel).should_receive(:new).once.with("somewhere", "cruise", 2299).and_return(ssh_tunnel = Object.new)
331 | flexmock(ssh_tunnel).should_receive(:open).once
332 |
333 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb' ])
334 | flexmock(HTTParty).should_receive(:post).with("http://127.0.0.1:2299/builds", any).and_return(response_with_build_id)
335 | flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, "results" => "job 1 done: ...." })
336 | flexmock(requester).should_receive(:sleep)
337 | flexmock(requester).should_receive(:print)
338 | flexmock(requester).should_receive(:puts)
339 |
340 | flexmock(requester).should_receive('system').with("rsync -az --delete --delete-excluded -e ssh . cruise@somewhere:/tmp/testbot/foo")
341 | mock_file_sizes
342 |
343 | requester.run_tests(RspecAdapter, 'spec')
344 | end
345 |
346 | should "use another port for cucumber to be able to run at the same time as rspec" do
347 | requester = Requester.new(:ssh_tunnel => true, :server_host => "somewhere")
348 | flexmock(requester).should_receive(:system)
349 |
350 | flexmock(SSHTunnel).should_receive(:new).once.with("somewhere", "testbot", 2230).and_return(ssh_tunnel = Object.new)
351 | flexmock(ssh_tunnel).should_receive(:open).once
352 |
353 | flexmock(requester).should_receive(:find_tests).and_return([ 'features/some.feature' ])
354 | flexmock(HTTParty).should_receive(:post).with("http://127.0.0.1:2230/builds", any).and_return(response_with_build_id)
355 | flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, "results" => "job 1 done: ...." })
356 | flexmock(requester).should_receive(:sleep)
357 | flexmock(requester).should_receive(:print)
358 | flexmock(requester).should_receive(:puts)
359 | mock_file_sizes
360 |
361 | requester.run_tests(CucumberAdapter, 'features')
362 | end
363 |
364 | should "use another port for Test::Unit" do
365 | requester = Requester.new(:ssh_tunnel => true, :server_host => "somewhere")
366 | flexmock(requester).should_receive(:system)
367 |
368 | flexmock(SSHTunnel).should_receive(:new).once.with("somewhere", "testbot", 2231).and_return(ssh_tunnel = Object.new)
369 | flexmock(ssh_tunnel).should_receive(:open).once
370 |
371 | flexmock(requester).should_receive(:find_tests).and_return([ 'test/some_test.rb' ])
372 | flexmock(HTTParty).should_receive(:post).with("http://127.0.0.1:2231/builds", any).and_return(response_with_build_id)
373 | flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, "results" => "job 1 done: ...." })
374 | flexmock(requester).should_receive(:sleep)
375 | flexmock(requester).should_receive(:print)
376 | flexmock(requester).should_receive(:puts)
377 | mock_file_sizes
378 |
379 | requester.run_tests(TestUnitAdapter, 'test')
380 | end
381 |
382 | should "request a run with jruby if USE_JRUBY is set" do
383 | ENV['USE_JRUBY'] = "true"
384 | requester = Requester.new
385 | flexmock(requester).should_receive(:system)
386 |
387 | # This is quite ugly. I want something like hash_including instead...
388 | other_args = { :type=>"test", :available_runner_usage=>"100%",
389 | :root=>"testbot@:/tmp/testbot/#{ENV['USER']}", :files=>"test/some_test.rb",
390 | :sizes=>"0", :project=>"project" }
391 |
392 | flexmock(TestUnitAdapter).should_receive(:test_files).and_return([ 'test/some_test.rb' ])
393 | flexmock(HTTParty).should_receive(:post).with(any, :body => other_args.merge({ :jruby => true })).and_return(response_with_build_id)
394 | flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, "results" => "job 1 done: ...." })
395 | flexmock(requester).should_receive(:sleep)
396 | flexmock(requester).should_receive(:print)
397 | flexmock(requester).should_receive(:puts)
398 | mock_file_sizes
399 |
400 | requester.run_tests(TestUnitAdapter, 'test')
401 | end
402 |
403 | end
404 |
405 | end
406 |
407 | end
408 |
--------------------------------------------------------------------------------
/test/requester/testbot.yml:
--------------------------------------------------------------------------------
1 | server_host: hostname
2 | rsync_path: /path
3 | rsync_ignores: .git tmp
4 | project: appname
5 | ssh_tunnel: false
6 | server_user: user
7 | available_runner_usage: 50%
--------------------------------------------------------------------------------
/test/requester/testbot_with_erb.yml:
--------------------------------------------------------------------------------
1 | server_host: <%= "dynamic_host" %>
2 | available_runner_usage: "<%= 50 %>%"
--------------------------------------------------------------------------------
/test/runner/job_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/shared/testbot.rb'))
2 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/runner/job.rb'))
3 | require 'test/unit'
4 | require 'shoulda'
5 | require 'flexmock/test_unit'
6 |
7 | module Testbot::Runner
8 |
9 | class JobTest < Test::Unit::TestCase
10 |
11 | def expect_put_with(id, result_text, status, time = 0)
12 | expected_result = "\n#{`hostname`.chomp}:#{Dir.pwd}\n"
13 | expected_result += result_text
14 | flexmock(Server).should_receive(:put).once.with("/jobs/#{id}", :body =>
15 | { :result => expected_result, :status => status, :time => time })
16 | end
17 |
18 | def expect_put
19 | flexmock(Server).should_receive(:put).once
20 | end
21 |
22 | def expect_put_to_timeout
23 | flexmock(Server).should_receive(:put).and_raise(Timeout::Error)
24 | end
25 |
26 | def stub_duration(seconds)
27 | time ||= Time.now
28 | flexmock(Time).should_receive(:now).and_return(time, time + seconds)
29 | end
30 |
31 | should "be able to run a successful job" do
32 | job = Job.new(Runner.new({}), 10, "00:00", "project", "/tmp/testbot/user", "spec", "ruby", "spec/foo_spec.rb spec/bar_spec.rb")
33 | flexmock(job).should_receive(:puts)
34 | stub_duration(0)
35 |
36 | expect_put_with(10, "result text", "successful")
37 | flexmock(job).should_receive(:run_and_return_result).once.
38 | with("export RAILS_ENV=test; export TEST_ENV_NUMBER=; cd project; export RSPEC_COLOR=true; ruby -S bundle exec rspec spec/foo_spec.rb spec/bar_spec.rb").
39 | and_return('result text')
40 | flexmock(RubyEnv).should_receive(:bundler?).returns(true)
41 | flexmock(RubyEnv).should_receive(:rvm?).returns(false)
42 | job.run(0)
43 | end
44 |
45 | should "not raise an error when posting results time out" do
46 | job = Job.new(Runner.new({}), 10, "00:00", "project", "/tmp/testbot/user", "spec", "ruby", "spec/foo_spec.rb spec/bar_spec.rb")
47 | flexmock(job).should_receive(:puts)
48 |
49 | # We're using send here because triggering post_results though the rest of the
50 | # code requires very complex setup. The code need to be refactored to be more testable.
51 | expect_put
52 | job.send(:post_results, "result text")
53 | expect_put_to_timeout
54 | job.send(:post_results, "result text")
55 | end
56 |
57 | should "not be successful when the job fails" do
58 | job = Job.new(Runner.new({}), 10, "00:00", "project", "/tmp/testbot/user", "spec", "ruby", "spec/foo_spec.rb spec/bar_spec.rb")
59 | flexmock(job).should_receive(:puts)
60 | stub_duration(0)
61 |
62 | expect_put_with(10, "result text", "failed")
63 | flexmock(job).should_receive(:run_and_return_result).and_return('result text')
64 | flexmock(job).should_receive(:success?).and_return(false)
65 | job.run(0)
66 | end
67 |
68 | should "set an instance number when the instance is not 0" do
69 | job = Job.new(Runner.new({}), 10, "00:00", "project", "/tmp/testbot/user", "spec", "ruby", "spec/foo_spec.rb spec/bar_spec.rb")
70 | flexmock(job).should_receive(:puts)
71 | stub_duration(0)
72 |
73 | expect_put_with(10, "result text", "successful")
74 | flexmock(job).should_receive(:run_and_return_result).
75 | with(/TEST_ENV_NUMBER=2/).
76 | and_return('result text')
77 | flexmock(RubyEnv).should_receive(:rvm?).returns(false)
78 | job.run(1)
79 | end
80 |
81 | should "return test runtime in milliseconds" do
82 | job = Job.new(Runner.new({}), 10, "00:00", "project", "/tmp/testbot/user", "spec", "ruby", "spec/foo_spec.rb spec/bar_spec.rb")
83 | flexmock(job).should_receive(:puts)
84 |
85 | stub_duration(10.55)
86 | expect_put_with(10, "result text", "successful", 1055)
87 | flexmock(job).should_receive(:run_and_return_result).and_return('result text')
88 | flexmock(RubyEnv).should_receive(:rvm?).returns(false)
89 | job.run(0)
90 | end
91 |
92 | end
93 |
94 | end
95 |
--------------------------------------------------------------------------------
/test/runner/safe_result_text_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/shared/testbot.rb'))
2 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/runner/safe_result_text.rb'))
3 | require 'test/unit'
4 | require 'shoulda'
5 |
6 | module Testbot::Runner
7 |
8 | class SafeResultTextTest < Test::Unit::TestCase
9 |
10 | should "not break escape sequences" do
11 | assert_equal "^[[32m.^[[0m^[[32m.^[[0m", SafeResultText.clean("^[[32m.^[[0m^[[32m.^[[0m^[[32m.")
12 | assert_equal "^[[32m.^[[0m^[[32m.^[[0m", SafeResultText.clean("^[[32m.^[[0m^[[32m.^[[0m^[[3")
13 | assert_equal "^[[32m.^[[0m", SafeResultText.clean("^[[32m.^[[0m^[")
14 | assert_equal "[32m.[0m[32m.[0m[3", SafeResultText.clean("[32m.[0m[32m.[0m[3")
15 | assert_equal "...", SafeResultText.clean("...")
16 | end
17 |
18 | end
19 |
20 | end
21 |
--------------------------------------------------------------------------------
/test/server/group_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/server/group'))
2 | require 'test/unit'
3 | require 'shoulda'
4 | require 'flexmock/test_unit'
5 |
6 | module Testbot::Server
7 |
8 | class GroupTest < Test::Unit::TestCase
9 |
10 | context "self.build" do
11 |
12 | should "create file groups based on the number of instances" do
13 | groups = Group.build([ 'spec/models/car_spec.rb', 'spec/models/car2_spec.rb',
14 | 'spec/models/house_spec.rb', 'spec/models/house2_spec.rb' ], [ 1, 1, 1, 1 ], 2, 'spec')
15 |
16 | assert_equal 2, groups.size
17 | assert_equal [ 'spec/models/house2_spec.rb', 'spec/models/house_spec.rb' ], groups[0]
18 | assert_equal [ 'spec/models/car2_spec.rb', 'spec/models/car_spec.rb' ], groups[1]
19 | end
20 |
21 | should "create a small grop when there isn't enough specs to fill a normal one" do
22 | groups = Group.build(["spec/models/car_spec.rb", "spec/models/car2_spec.rb",
23 | "spec/models/house_spec.rb", "spec/models/house2_spec.rb",
24 | "spec/models/house3_spec.rb"], [ 1, 1, 1, 1, 1 ], 3, 'spec')
25 |
26 | assert_equal 3, groups.size
27 | assert_equal [ "spec/models/car_spec.rb" ], groups[2]
28 | end
29 |
30 | should "use sizes when building groups" do
31 | groups = Group.build([ 'spec/models/car_spec.rb', 'spec/models/car2_spec.rb',
32 | 'spec/models/house_spec.rb', 'spec/models/house2_spec.rb' ], [ 40, 10, 10, 20 ], 2, 'spec')
33 |
34 | assert_equal [ 'spec/models/car_spec.rb' ], groups[0]
35 | assert ![ 'spec/models/house2_spec.rb', 'spec/models/car2_spec.rb', 'spec/models/house_spec.rb' ].
36 | find { |file| !groups[1].include?(file) }
37 | end
38 |
39 | end
40 |
41 | end
42 |
43 | end
44 |
--------------------------------------------------------------------------------
/test/server/server_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/server/server'))
2 | require 'test/unit'
3 | require 'rack/test'
4 | require 'shoulda'
5 | require 'flexmock/test_unit'
6 |
7 | set :environment, :test
8 |
9 | module Testbot::Server
10 |
11 | class ServerTest < Test::Unit::TestCase
12 | include Rack::Test::Methods
13 |
14 | def setup
15 | Job.delete_all
16 | Runner.delete_all
17 | Build.delete_all
18 | end
19 |
20 | def app
21 | Sinatra::Application
22 | end
23 |
24 | context "POST /builds" do
25 |
26 | should "create a build and return its id" do
27 | flexmock(Runner).should_receive(:total_instances).and_return(2)
28 | post '/builds', :files => 'spec/models/car_spec.rb spec/models/house_spec.rb', :root => 'server:/path/to/project', :type => 'spec', :available_runner_usage => "100%", :project => 'things', :sizes => "10 20", :jruby => false
29 |
30 | first_build = Build.all.first
31 | assert last_response.ok?
32 |
33 | assert_equal first_build.id.to_s, last_response.body
34 | assert_equal 'spec/models/car_spec.rb spec/models/house_spec.rb', first_build.files
35 | assert_equal '10 20', first_build.sizes
36 | assert_equal 'server:/path/to/project', first_build.root
37 | assert_equal 'spec', first_build.type
38 | assert_equal 'things', first_build.project
39 | assert_equal 0, first_build.jruby
40 | assert_equal '', first_build.results
41 | assert_equal true, first_build.success
42 | end
43 |
44 | should "create jobs from the build based on the number of total instances" do
45 | flexmock(Runner).should_receive(:total_instances).and_return(2)
46 | flexmock(Group).should_receive(:build).with(["spec/models/car_spec.rb", "spec/models/car2_spec.rb", "spec/models/house_spec.rb", "spec/models/house2_spec.rb"], [ 1, 1, 1, 1 ], 2, 'spec').once.and_return([
47 | ["spec/models/car_spec.rb", "spec/models/car2_spec.rb"],
48 | ["spec/models/house_spec.rb", "spec/models/house2_spec.rb"]
49 | ])
50 |
51 | post '/builds', :files => 'spec/models/car_spec.rb spec/models/car2_spec.rb spec/models/house_spec.rb spec/models/house2_spec.rb', :root => 'server:/path/to/project', :type => 'spec', :available_runner_usage => "100%", :project => 'things', :sizes => "1 1 1 1", :jruby => true
52 |
53 | assert_equal 2, Job.count
54 | first_job, last_job = Job.all
55 | assert_equal 'spec/models/car_spec.rb spec/models/car2_spec.rb', first_job.files
56 | assert_equal 'spec/models/house_spec.rb spec/models/house2_spec.rb', last_job.files
57 |
58 | assert_equal 'server:/path/to/project', first_job.root
59 | assert_equal 'spec', first_job.type
60 | assert_equal 'things', first_job.project
61 | assert_equal 1, first_job.jruby
62 | assert_equal Build.all.first, first_job.build
63 | end
64 |
65 | should "return a 503 error if there are no known runners" do
66 | flexmock(Runner).should_receive(:total_instances).and_return(0)
67 | post '/builds', :files => 'spec/models/car_spec.rb spec/models/car2_spec.rb spec/models/house_spec.rb spec/models/house2_spec.rb', :root => 'server:/path/to/project', :type => 'spec', :available_runner_usage => "100%", :project => 'things', :sizes => "1 1 1 1", :jruby => true
68 | assert_equal 0, Job.count
69 | assert_equal 503, last_response.status
70 | assert_equal "No runners available", last_response.body
71 | end
72 |
73 | should "only use resources according to available_runner_usage" do
74 | flexmock(Runner).should_receive(:total_instances).and_return(4)
75 | flexmock(Group).should_receive(:build).with(["spec/models/car_spec.rb", "spec/models/car2_spec.rb", "spec/models/house_spec.rb", "spec/models/house2_spec.rb"], [ 1, 1, 1, 1 ], 2, 'spec').and_return([])
76 | post '/builds', :files => 'spec/models/car_spec.rb spec/models/car2_spec.rb spec/models/house_spec.rb spec/models/house2_spec.rb', :root => 'server:/path/to/project', :type => 'spec', :sizes => "1 1 1 1", :available_runner_usage => "50%"
77 | end
78 |
79 | end
80 |
81 | context "GET /builds/:id" do
82 |
83 | should 'return the build status' do
84 | build = Build.create(:done => false, :results => "testbot5\n..........\ncompleted", :success => false)
85 | get "/builds/#{build.id}"
86 | assert_equal true, last_response.ok?
87 | assert_equal ({ "done" => false, "results" => "testbot5\n..........\ncompleted", "success" => false }),
88 | JSON.parse(last_response.body)
89 | end
90 |
91 | should 'remove a build that is done' do
92 | build = Build.create(:done => true)
93 | get "/builds/#{build.id}"
94 | assert_equal true, JSON.parse(last_response.body)['done']
95 | assert_equal 0, Build.count
96 | end
97 |
98 | should 'remove all related jobs of a build that is done' do
99 | build = Build.create(:done => true)
100 | related_job = Job.create(:build => build)
101 | other_job = Job.create(:build => nil)
102 | get "/builds/#{build.id}"
103 | assert !Job.find(related_job.id)
104 | assert Job.find(other_job.id)
105 | end
106 |
107 | end
108 |
109 | context "GET /jobs/next" do
110 |
111 | should "be able to return a job and mark it as taken" do
112 | build = Build.create
113 | job1 = Job.create :files => 'spec/models/car_spec.rb', :root => 'server:/project', :type => 'spec', :build => build, :project => 'things', :jruby => 1
114 |
115 | get '/jobs/next', :version => Testbot.version
116 | assert last_response.ok?
117 |
118 | assert_equal [ job1.id, build.id, "things", "server:/project", "spec", "jruby", "spec/models/car_spec.rb" ].join(','), last_response.body
119 | assert job1.taken_at != nil
120 | end
121 |
122 | should "not return a job that has already been taken" do
123 | build = Build.create
124 | job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now, :type => 'spec', :build => build
125 | job2 = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build, :project => 'things', :jruby => 0
126 | get '/jobs/next', :version => Testbot.version
127 | assert last_response.ok?
128 | assert_equal [ job2.id, build.id, "things", "server:/project", "spec", "ruby", "spec/models/house_spec.rb" ].join(','), last_response.body
129 | assert job2.taken_at != nil
130 | end
131 |
132 | should "not return a job if there isnt any" do
133 | get '/jobs/next', :version => Testbot.version
134 | assert last_response.ok?
135 | assert_equal '', last_response.body
136 | end
137 |
138 | should "save which runner takes a job" do
139 | job = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => Build.create
140 | get '/jobs/next', :version => Testbot.version
141 | assert_equal Runner.first, job.taken_by
142 | end
143 |
144 | should "save information about the runners" do
145 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini.local', :uid => "00:01:...", :idle_instances => 2, :max_instances => 4
146 | runner = Runner.first
147 | assert_equal Testbot.version, runner.version
148 | assert_equal '127.0.0.1', runner.ip
149 | assert_equal 'macmini.local', runner.hostname
150 | assert_equal '00:01:...', runner.uid
151 | assert_equal 2, runner.idle_instances
152 | assert_equal 4, runner.max_instances
153 | assert (Time.now - 5) < runner.last_seen_at
154 | assert (Time.now + 5) > runner.last_seen_at
155 | end
156 |
157 | should "only create one record for the same mac" do
158 | get '/jobs/next', :version => Testbot.version, :uid => "00:01:..."
159 | get '/jobs/next', :version => Testbot.version, :uid => "00:01:..."
160 | assert_equal 1, Runner.count
161 | end
162 |
163 | should "not return anything to outdated clients" do
164 | Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project'
165 | get '/jobs/next', :version => "1", :uid => "00:..."
166 | assert last_response.ok?
167 | assert_equal '', last_response.body
168 | end
169 |
170 | should "only give jobs from the same source to a runner" do
171 | build = Build.create
172 | job1 = Job.create :files => 'spec/models/car_spec.rb', :type => 'spec', :build => build
173 | get '/jobs/next', :version => Testbot.version, :uid => "00:...", :build_id => build.id
174 |
175 | # Creating the second job here because of the random lookup.
176 | job2 = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build
177 | get '/jobs/next', :version => Testbot.version, :uid => "00:...", :build_id => build.id + 1
178 |
179 | assert last_response.ok?
180 | assert_equal '', last_response.body
181 | end
182 |
183 | should "not give more jruby jobs to an instance that can't take more" do
184 | build = Build.create
185 | job1 = Job.create :files => 'spec/models/car_spec.rb', :type => 'spec', :jruby => 1, :build => build
186 | get '/jobs/next', :version => Testbot.version, :uid => "00:..."
187 |
188 | # Creating the second job here because of the random lookup.
189 | job2 = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :jruby => 1, :build => build
190 | get '/jobs/next', :version => Testbot.version, :uid => "00:...", :no_jruby => "true"
191 |
192 | assert last_response.ok?
193 | assert_equal '', last_response.body
194 | end
195 |
196 | should "still return other jobs when the runner cant take more jruby jobs" do
197 | job1 = Job.create :files => 'spec/models/car_spec.rb', :type => 'spec', :jruby => 1, :build => Build.create
198 | get '/jobs/next', :version => Testbot.version, :uid => "00:..."
199 |
200 | # Creating the second job here because of the random lookup.
201 | job2 = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :jruby => 0, :build => Build.create
202 | get '/jobs/next', :version => Testbot.version, :uid => "00:...", :no_jruby => "true"
203 |
204 | assert last_response.ok?
205 | assert_equal job2.id.to_s, last_response.body.split(',')[0]
206 | end
207 |
208 | should "return the jobs in random order in order to start working for a new build right away" do
209 | build1, build2 = Build.create, Build.create
210 | 20.times { Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build1 }
211 |
212 | 20.times { Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build2 }
213 |
214 | build_ids = (0...10).map {
215 | get '/jobs/next', :version => Testbot.version, :uid => "00:..."
216 | last_response.body.split(',')[1]
217 | }
218 |
219 | assert build_ids.find { |build_id| build_id == build1.id.to_s }
220 | assert build_ids.find { |build_id| build_id == build2.id.to_s }
221 | end
222 |
223 | should "return the jobs randomly when passing build_id" do
224 | build = Build.create
225 | 20.times { Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build }
226 |
227 | 20.times { Job.create :files => 'spec/models/car_spec.rb', :root => 'server:/project', :type => 'spec', :build => build }
228 |
229 | files = (0...10).map {
230 | get '/jobs/next', :version => Testbot.version, :uid => "00:...", :build_id => build.id
231 | last_response.body.split(',').last
232 | }
233 |
234 | assert files.find { |file| file.include?('car') }
235 | assert files.find { |file| file.include?('house') }
236 | end
237 |
238 | should "return taken jobs to other runners if the runner hasn't been seen for 10 seconds or more" do
239 | missing_runner = Runner.create(:last_seen_at => Time.now - 15)
240 | build = Build.create
241 | old_taken_job = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build, :taken_by => missing_runner, :taken_at => Time.now - 30, :project => 'things'
242 |
243 | new_runner = Runner.create(:uid => "00:01")
244 | get '/jobs/next', :version => Testbot.version, :uid => "00:01"
245 | assert_equal new_runner, old_taken_job.taken_by
246 |
247 | assert last_response.ok?
248 | assert_equal [ old_taken_job.id, build.id.to_s, "things", "server:/project", "spec", "ruby", "spec/models/house_spec.rb" ].join(','), last_response.body
249 | end
250 |
251 | end
252 |
253 | context "/runners/outdated" do
254 |
255 | should "return a list of outdated runners" do
256 | get '/jobs/next', :version => "1", :hostname => 'macmini1.local', :uid => "00:01"
257 | get '/jobs/next', :version => "1", :hostname => 'macmini2.local', :uid => "00:02"
258 | get '/jobs/next'
259 | get '/jobs/next', :version => Testbot.version.to_s, :hostname => 'macmini3.local', :uid => "00:03"
260 | assert_equal 4, Runner.count
261 | get '/runners/outdated'
262 | assert last_response.ok?
263 | assert_equal "127.0.0.1 macmini1.local 00:01\n127.0.0.1 macmini2.local 00:02\n127.0.0.1", last_response.body
264 | end
265 |
266 | end
267 |
268 | context "GET /runners/available_runners" do
269 |
270 | should "return a list of available runners" do
271 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2, :username => 'user1'
272 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4, :username => 'user2'
273 | get '/runners/available'
274 | assert last_response.ok?
275 | assert_equal "127.0.0.1 macmini1.local 00:01 user1 2\n127.0.0.1 macmini2.local 00:02 user2 4", last_response.body
276 | end
277 |
278 | should "not return a runner as available when it hasnt pinged the server yet" do
279 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :username => 'user1'
280 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4, :username => 'user2'
281 | get '/runners/available'
282 | assert last_response.ok?
283 | assert_equal "127.0.0.1 macmini2.local 00:02 user2 4", last_response.body
284 | end
285 |
286 | should "not return runners as available when not seen the last 10 seconds" do
287 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2, :username => "user1"
288 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4
289 | Runner.find_by_uid("00:02").update(:last_seen_at => Time.now - 10)
290 | get '/runners/available'
291 | assert_equal "127.0.0.1 macmini1.local 00:01 user1 2", last_response.body
292 | end
293 |
294 | end
295 |
296 | context "GET /runners/available_instances" do
297 |
298 | should "return the number of available runner instances" do
299 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2
300 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4
301 | get '/runners/available_instances'
302 | assert last_response.ok?
303 | assert_equal "6", last_response.body
304 | end
305 |
306 | should "not return instances as available when not seen the last 10 seconds" do
307 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2
308 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4
309 | Runner.find_by_uid("00:02").update(:last_seen_at => Time.now - 10)
310 | get '/runners/available_instances'
311 | assert last_response.ok?
312 | assert_equal "2", last_response.body
313 | end
314 |
315 | end
316 |
317 | context "GET /runners/total_instances" do
318 |
319 | should "return the number of available runner instances" do
320 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :max_instances => 2, :idle_instances => 1
321 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :max_instances => 4, :idle_instances => 2
322 | get '/runners/total_instances'
323 | assert last_response.ok?
324 | assert_equal "6", last_response.body
325 | end
326 |
327 | should "not return instances as available when not seen the last 10 seconds" do
328 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :max_instances => 2, :idle_instances => 1
329 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :max_instances => 4, :idle_instances => 2
330 | Runner.find_by_uid("00:02").update(:last_seen_at => Time.now - 10)
331 | get '/runners/total_instances'
332 | assert last_response.ok?
333 | assert_equal "2", last_response.body
334 | end
335 |
336 | end
337 |
338 | context "GET /runners/ping" do
339 |
340 | should "update last_seen_at for the runner" do
341 | runner = Runner.create(:uid => 'aa:aa:aa:aa:aa:aa')
342 | get "/runners/ping", :uid => 'aa:aa:aa:aa:aa:aa', :version => Testbot.version
343 | assert last_response.ok?
344 | assert (Time.now - 5) < runner.last_seen_at
345 | assert (Time.now + 5) > runner.last_seen_at
346 | end
347 |
348 | should "update data on the runner" do
349 | build = Build.create
350 | runner = Runner.create(:uid => 'aa:aa:..')
351 | get "/runners/ping", :uid => 'aa:aa:..', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'jocke', :build_id => build.id
352 | assert last_response.ok?
353 | assert_equal 'aa:aa:..', runner.uid
354 | assert_equal 4, runner.max_instances
355 | assert_equal 2, runner.idle_instances
356 | assert_equal 'hostname1', runner.hostname
357 | assert_equal Testbot.version, runner.version
358 | assert_equal 'jocke', runner.username
359 | assert_equal build, runner.build
360 | end
361 |
362 | should "do nothing if the version does not match" do
363 | runner = Runner.create(:uid => 'aa:aa:..', :version => Testbot.version)
364 | get "/runners/ping", :uid => 'aa:aa:..', :version => "OLD"
365 | assert last_response.ok?
366 | assert_equal Testbot.version, runner.version
367 | end
368 |
369 | should "do nothing if the runners isnt known yet found" do
370 | get "/runners/ping", :uid => 'aa:aa:aa:aa:aa:aa', :version => Testbot.version
371 | assert last_response.ok?
372 | end
373 |
374 | should "return an order to stop the build if the build id does not exist anymore" do
375 | runner = Runner.create(:uid => 'aa:aa:..')
376 | get "/runners/ping", :uid => 'aa:aa:..', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'jocke', :build_id => 1
377 | assert_equal last_response.body, "stop_build,1"
378 | end
379 |
380 | should "not return an order to stop a build without an id" do
381 | runner = Runner.create(:uid => 'aa:aa:..')
382 | get "/runners/ping", :uid => 'aa:aa:..', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'jocke', :build_id => ''
383 | assert_equal last_response.body, ''
384 | get "/runners/ping", :uid => 'aa:aa:..', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'jocke', :build_id => nil
385 | assert_equal last_response.body, ''
386 | end
387 |
388 | end
389 |
390 | context "PUT /jobs/:id" do
391 |
392 | should "receive the results of a job" do
393 | job = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30
394 | put "/jobs/#{job.id}", :result => 'test run result', :status => "successful"
395 | assert last_response.ok?
396 | assert_equal 'test run result', job.result
397 | assert_equal 'successful', job.status
398 | end
399 |
400 | should "update the related build" do
401 | build = Build.create
402 | job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
403 | job2 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
404 | put "/jobs/#{job1.id}", :result => 'test run result 1\n', :status => "successful"
405 | put "/jobs/#{job2.id}", :result => 'test run result 2\n', :status => "successful"
406 | assert_equal 'test run result 1\ntest run result 2\n', build.results
407 | assert_equal true, build.success
408 | end
409 |
410 | should "make the related build done if there are no more jobs for the build" do
411 | build = Build.create
412 | job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
413 | job2 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
414 | put "/jobs/#{job1.id}", :result => 'test run result 1\n', :status => "successful"
415 | put "/jobs/#{job2.id}", :result => 'test run result 2\n', :status => "successful"
416 | assert_equal true, build.done
417 | end
418 |
419 | should "make the build fail if one of the jobs fail" do
420 | build = Build.create
421 | job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
422 | job2 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
423 | put "/jobs/#{job1.id}", :result => 'test run result 1\n', :status => "failed"
424 | put "/jobs/#{job2.id}", :result => 'test run result 2\n', :status => "successful"
425 | assert_equal false, build.success
426 | end
427 |
428 | should "be able to update from multiple result postings" do
429 | build = Build.create
430 | job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
431 | job2 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
432 | # maybe later:
433 | # put "/jobs/#{job.id}", :result => 'Preparing, db setup, etc.', :status => "preparing"
434 | put "/jobs/#{job1.id}", :result => 'Running tests..', :status => "running"
435 | put "/jobs/#{job2.id}", :result => 'Running other tests. done.', :status => "successful"
436 | put "/jobs/#{job1.id}", :result => 'Running tests....', :status => "running"
437 | assert_equal false, build.done
438 | assert_equal false, job1.done
439 | assert_equal "Running tests....", job1.result
440 | assert_equal "Running tests..Running other tests. done...", build.results
441 | end
442 |
443 | should "not break when updating without new results" do
444 | build = Build.create
445 | job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build
446 | put "/jobs/#{job1.id}", :result => 'Running tests..', :status => "running"
447 | put "/jobs/#{job1.id}", :result => '', :status => "successful"
448 | assert_equal "Running tests..", build.results
449 | end
450 |
451 | end
452 |
453 | context "GET /version" do
454 |
455 | should "return its version" do
456 | get '/version'
457 | assert last_response.ok?
458 | assert_equal Testbot.version.to_s, last_response.body
459 | end
460 |
461 | end
462 |
463 | context "GET /runners" do
464 |
465 | should "return runner information in json format" do
466 | get '/jobs/next', :version => Testbot.version, :uid => "00:01"
467 | get "/runners/ping", :uid => '00:01', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'testbot', :build_id => nil
468 | get '/runners'
469 |
470 | assert last_response.ok?
471 | assert_equal ([ { "version" => Testbot.version.to_s, "build" => nil, "hostname" => 'hostname1', "uid" => "00:01",
472 | "idle_instances" => 2, "max_instances" => 4, "username" => 'testbot',
473 | "ip" => "127.0.0.1", "last_seen_at" => Runner.first.last_seen_at.to_s } ]),
474 | JSON.parse(last_response.body)
475 | end
476 |
477 | should "not return instances when not seen the last 10 seconds" do
478 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2
479 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4
480 | Runner.find_by_uid("00:02").update(:last_seen_at => Time.now - 10)
481 | get '/runners'
482 | assert last_response.ok?
483 | parsed_body = JSON.parse(last_response.body)
484 | assert_equal 1, parsed_body.size
485 | assert_equal '00:01', parsed_body.first["uid"]
486 | end
487 |
488 | end
489 |
490 | context "GET /status" do
491 |
492 | should "return the contents of the status page" do
493 | get '/status'
494 | assert_equal true, last_response.body.include?('Testbot status')
495 | end
496 |
497 | end
498 |
499 | context "GET /status/:dir/:file" do
500 |
501 | should "return the file" do
502 | get "/status/javascripts/jquery-1.4.4.min.js"
503 | assert_equal true, last_response.body.include?('jQuery JavaScript Library v1.4.4')
504 | end
505 |
506 | end
507 |
508 | end
509 |
510 | end
511 |
512 |
--------------------------------------------------------------------------------
/test/shared/adapters/adapter_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../../lib/shared/adapters/adapter.rb'))
2 | require 'test/unit'
3 | require 'shoulda'
4 |
5 | class AdapterTest < Test::Unit::TestCase
6 |
7 | should "be able to find adapters" do
8 | assert_equal RspecAdapter, Adapter.find(:spec)
9 | assert_equal TestUnitAdapter, Adapter.find(:test)
10 | end
11 |
12 | should "find be able to find an adapter by string" do
13 | assert_equal RspecAdapter, Adapter.find("spec")
14 | assert_equal TestUnitAdapter, Adapter.find("test")
15 | end
16 |
17 | should "be able to return a list of adapters" do
18 | assert Adapter.all.include?(RspecAdapter)
19 | assert Adapter.all.include?(TestUnitAdapter)
20 | end
21 |
22 | end
23 |
--------------------------------------------------------------------------------
/test/shared/adapters/cucumber_adapter_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../../lib/shared/adapters/cucumber_adapter.rb'))
2 | require 'test/unit'
3 | require 'shoulda'
4 |
5 | class CucumberAdapterTest < Test::Unit::TestCase
6 |
7 | context "sum_results" do
8 |
9 | should "be able to parse and sum results" do
10 | results =< "script/spec", :bin => "rspec")
55 | end
56 |
57 | should "use bundler when available and use the binary when there is no script" do
58 | flexmock(RubyEnv).should_receive(:bundler?).once.with("path/to/project").and_return(true)
59 | flexmock(File).should_receive(:exists?).with("path/to/project/script/spec").and_return(false)
60 | assert_equal 'ruby -S bundle exec rspec', RubyEnv.ruby_command("path/to/project", :script => "script/spec", :bin => "rspec")
61 | end
62 |
63 | should "use the script when it exists when using bundler" do
64 | flexmock(RubyEnv).should_receive(:bundler?).and_return(true)
65 | flexmock(File).should_receive(:exists?).and_return(true)
66 | assert_equal 'ruby -S bundle exec script/spec', RubyEnv.ruby_command("path/to/project", :script => "script/spec", :bin => "rspec")
67 | end
68 |
69 | should "use the script when it exists when not using bundler" do
70 | flexmock(RubyEnv).should_receive(:bundler?).and_return(false)
71 | flexmock(File).should_receive(:exists?).and_return(true)
72 | assert_equal 'ruby -S script/spec', RubyEnv.ruby_command("path/to/project", :script => "script/spec", :bin => "rspec")
73 | end
74 |
75 | should "not look for a script when none is provided" do
76 | assert_equal 'ruby -S rspec', RubyEnv.ruby_command("path/to/project", :bin => "rspec")
77 | end
78 |
79 | should "be able to use jruby" do
80 | flexmock(RubyEnv).should_receive(:bundler?).and_return(false)
81 | flexmock(File).should_receive(:exists?).and_return(true)
82 | assert_equal 'jruby -S script/spec', RubyEnv.ruby_command("path/to/project", :script => "script/spec",
83 | :bin => "rspec", :ruby_interpreter => "jruby")
84 | end
85 |
86 | should "be able to use jruby with bundler" do
87 | flexmock(RubyEnv).should_receive(:bundler?).and_return(true)
88 | flexmock(File).should_receive(:exists?).and_return(true)
89 | assert_equal 'jruby -S bundle exec script/spec', RubyEnv.ruby_command("path/to/project", :script => "script/spec",
90 | :bin => "rspec", :ruby_interpreter => "jruby")
91 | end
92 |
93 | should "work when there is no binary specified and bundler is present" do
94 | flexmock(RubyEnv).should_receive(:bundler?).and_return(true)
95 | flexmock(File).should_receive(:exists?).and_return(false)
96 | assert_equal 'ruby -S bundle exec', RubyEnv.ruby_command("path/to/project")
97 | end
98 |
99 | should "work when there is no binary specified and bundler is not present" do
100 | flexmock(RubyEnv).should_receive(:bundler?).and_return(false)
101 | flexmock(File).should_receive(:exists?).and_return(false)
102 | assert_equal 'ruby -S', RubyEnv.ruby_command("path/to/project")
103 | end
104 |
105 |
106 | end
107 |
108 | end
109 |
--------------------------------------------------------------------------------
/test/shared/adapters/rspec_adapter_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../../lib/shared/adapters/rspec_adapter.rb'))
2 | require 'test/unit'
3 | require 'shoulda'
4 |
5 | class RspecAdapterTest < Test::Unit::TestCase
6 |
7 | context "sum_results" do
8 |
9 | should "be able to parse and sum results" do
10 | results =< "192.168.0.100",
15 | :rsync_path => nil,
16 | :rsync_ignores => '', :server_user => nil, :available_runner_usage => nil,
17 | :project => nil, :ssh_tunnel => nil }
18 | end
19 |
20 | end
21 |
22 | class CLITest < Test::Unit::TestCase
23 |
24 | include TestHelpers
25 |
26 | context "self.run" do
27 |
28 | context "with no args" do
29 | should "return false" do
30 | assert_equal false, CLI.run([])
31 | end
32 | end
33 |
34 | context "with --help" do
35 | should "return false" do
36 | assert_equal false, CLI.run([ '--help' ])
37 | end
38 | end
39 |
40 | context "with --version" do
41 | should "print version and return true" do
42 | flexmock(CLI).should_receive(:puts).once.with("Testbot #{Testbot.version}")
43 | assert_equal true, CLI.run([ '--version' ])
44 | end
45 | end
46 |
47 | context "with --server" do
48 | should "start a server" do
49 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::SERVER_PID)
50 | flexmock(SimpleDaemonize).should_receive(:start).once.with(any, Testbot::SERVER_PID, "testbot (server)")
51 | flexmock(CLI).should_receive(:puts).once.with("Testbot server started (pid: #{Process.pid})")
52 | assert_equal true, CLI.run([ "--server" ])
53 | end
54 |
55 | should "start a server when start is passed" do
56 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::SERVER_PID)
57 | flexmock(SimpleDaemonize).should_receive(:start).once
58 | flexmock(CLI).should_receive(:puts)
59 | assert_equal true, CLI.run([ "--server", "start" ])
60 | end
61 |
62 | should "stop a server when stop is passed" do
63 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::SERVER_PID).and_return(true)
64 | flexmock(CLI).should_receive(:puts).once.with("Testbot server stopped")
65 | assert_equal true, CLI.run([ "--server", "stop" ])
66 | end
67 |
68 | should "not print when SimpleDaemonize.stop returns false" do
69 | flexmock(SimpleDaemonize).should_receive(:stop).and_return(false)
70 | flexmock(CLI).should_receive(:puts).never
71 | CLI.run([ "--stop", "server" ])
72 | end
73 |
74 | should "start it in the foreground with run" do
75 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::SERVER_PID)
76 | flexmock(SimpleDaemonize).should_receive(:start).never
77 | flexmock(Sinatra::Application).should_receive(:run!).once.with(:environment => "production")
78 | assert_equal true, CLI.run([ "--server", 'run' ])
79 | end
80 | end
81 |
82 | context "with --runner" do
83 | should "start a runner" do
84 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::RUNNER_PID)
85 | flexmock(SimpleDaemonize).should_receive(:start).once.with(any, Testbot::RUNNER_PID, "testbot (runner)")
86 | flexmock(CLI).should_receive(:puts).once.with("Testbot runner started (pid: #{Process.pid})")
87 | assert_equal true, CLI.run([ "--runner", "--connect", "192.168.0.100", "--working_dir", "/tmp/testbot" ])
88 | end
89 |
90 | should "start a runner when start is passed" do
91 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::RUNNER_PID)
92 | flexmock(SimpleDaemonize).should_receive(:start).once
93 | flexmock(CLI).should_receive(:puts)
94 | assert_equal true, CLI.run([ "--runner", "start", "--connect", "192.168.0.100" ])
95 | end
96 |
97 | should "stop a runner when stop is passed" do
98 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::RUNNER_PID).and_return(true)
99 | flexmock(CLI).should_receive(:puts).once.with("Testbot runner stopped")
100 | assert_equal true, CLI.run([ "--runner", "stop" ])
101 | end
102 |
103 | should "return false without connect" do
104 | assert_equal false, CLI.run([ "--runner", "--connect" ])
105 | assert_equal false, CLI.run([ "--runner" ])
106 | end
107 |
108 | should "start it in the foreground with run" do
109 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::RUNNER_PID)
110 | flexmock(SimpleDaemonize).should_receive(:start).never
111 | flexmock(Runner::Runner).should_receive(:new).once.and_return(mock = Object.new)
112 | flexmock(mock).should_receive(:run!).once
113 | assert_equal true, CLI.run([ "--runner", 'run', '--connect', '192.168.0.100' ])
114 | end
115 | end
116 |
117 | Adapter.all.each do |adapter|
118 | context "with --#{adapter.type}" do
119 | should "start a #{adapter.name} requester and return true" do
120 | flexmock(Requester::Requester).should_receive(:new).once.
121 | with(requester_attributes).and_return(mock = Object.new)
122 | flexmock(mock).should_receive(:run_tests).once.with(adapter, adapter.base_path)
123 | assert_equal true, CLI.run([ "--#{adapter.type}", "--connect", "192.168.0.100" ])
124 | end
125 |
126 | should "accept a custom rsync_path" do
127 | flexmock(Requester::Requester).should_receive(:new).once.
128 | with(requester_attributes.merge({ :rsync_path => "/somewhere/else" })).
129 | and_return(mock = Object.new)
130 | flexmock(mock).should_receive(:run_tests)
131 | CLI.run([ "--#{adapter.type}", "--connect", "192.168.0.100", '--rsync_path', '/somewhere/else' ])
132 | end
133 |
134 | should "accept rsync_ignores" do
135 | flexmock(Requester::Requester).should_receive(:new).once.
136 | with(requester_attributes.merge({ :rsync_ignores => "tmp log" })).
137 | and_return(mock = Object.new)
138 | flexmock(mock).should_receive(:run_tests)
139 | CLI.run([ "--#{adapter.type}", "--connect", "192.168.0.100", '--rsync_ignores', 'tmp', 'log' ])
140 | end
141 |
142 | should "accept ssh tunnel" do
143 | flexmock(Requester::Requester).should_receive(:new).once.
144 | with(requester_attributes.merge({ :ssh_tunnel => true })).
145 | and_return(mock = Object.new)
146 | flexmock(mock).should_receive(:run_tests)
147 | CLI.run([ "--#{adapter.type}", "--connect", "192.168.0.100", '--ssh_tunnel' ])
148 | end
149 |
150 | should "accept a custom user" do
151 | flexmock(Requester::Requester).should_receive(:new).once.
152 | with(requester_attributes.merge({ :server_user => "cruise" })).
153 | and_return(mock = Object.new)
154 | flexmock(mock).should_receive(:run_tests)
155 | CLI.run([ "--#{adapter.type}", "--connect", "192.168.0.100", '--user', 'cruise' ])
156 | end
157 |
158 | should "accept a custom project name" do
159 | flexmock(Requester::Requester).should_receive(:new).once.
160 | with(requester_attributes.merge({ :project => "rspec" })).
161 | and_return(mock = Object.new)
162 | flexmock(mock).should_receive(:run_tests)
163 | CLI.run([ "--#{adapter.type}", "--connect", "192.168.0.100", '--project', 'rspec' ])
164 | end
165 | end
166 | end
167 | end
168 |
169 | context "self.parse_args" do
170 |
171 | should 'convert ARGV arguments to a hash' do
172 | hash = CLI.parse_args("--runner --connect http://127.0.0.1:#{Testbot::SERVER_PORT} --working_dir ~/testbot --ssh_tunnel user@testbot_server".split)
173 | assert_equal ({ :runner => true, :connect => "http://127.0.0.1:#{Testbot::SERVER_PORT}", :working_dir => "~/testbot", :ssh_tunnel => "user@testbot_server" }), hash
174 | end
175 |
176 | should "handle just a key without a value" do
177 | hash = CLI.parse_args([ "--server" ])
178 | assert_equal ({ :server => true }), hash
179 | end
180 |
181 | end
182 |
183 | end
184 |
185 | end
186 |
--------------------------------------------------------------------------------
/testbot.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | require File.expand_path(File.dirname(__FILE__) + '/lib/shared/version')
3 |
4 | Gem::Specification.new do |s|
5 | s.name = "testbot"
6 | s.version = Testbot.version
7 | s.authors = ["Joakim Kolsjö"]
8 | s.email = ["joakim.kolsjo@gmail.com"]
9 | s.homepage = "http://github.com/joakimk/testbot"
10 | s.summary = %q{A test distribution tool.}
11 | s.description = %q{Testbot is a test distribution tool that works with Rails, RSpec, RSpec2, Test::Unit and Cucumber.}
12 | s.bindir = "bin"
13 | s.executables = [ "testbot" ]
14 | s.files = Dir.glob("lib/**/*") + Dir.glob("test/**/*") + %w(Gemfile .gemtest Rakefile testbot.gemspec CHANGELOG README.markdown bin/testbot) +
15 | (File.exists?("DEV_VERSION") ? [ "DEV_VERSION" ] : [])
16 |
17 | s.add_dependency('sinatra', '~> 1.0')
18 | s.add_dependency('httparty', '>= 0.6.1')
19 | s.add_dependency('net-ssh', '>= 2.0.23')
20 | s.add_dependency('json_pure', '>= 1.4.6')
21 | s.add_dependency('daemons', '>= 1.0.10')
22 | s.add_dependency('acts_as_rails3_generator')
23 | s.add_dependency('posix-spawn', '>= 0.3.6')
24 |
25 | s.add_development_dependency("shoulda")
26 | s.add_development_dependency("rack-test")
27 | s.add_development_dependency("flexmock")
28 | s.add_development_dependency("rvm")
29 | s.add_development_dependency("rake", "0.8.7")
30 | s.add_development_dependency("bundler")
31 | s.add_development_dependency("guard")
32 | s.add_development_dependency("guard-test")
33 | end
34 |
35 |
--------------------------------------------------------------------------------