153 lines
4.6 KiB
Ruby
Executable File
153 lines
4.6 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
|
|
$stdout.sync = $stderr.sync = true
|
|
|
|
require 'test_queue'
|
|
require 'test_queue/runner/rspec'
|
|
|
|
class CanvasSpecRunner < TestQueue::Runner::RSpec
|
|
def initialize(*)
|
|
detect_instance_termination if ENV["TEST_QUEUE_DETECT_INSTANCE_TERMINATION"]
|
|
# make sure we're applying our config 'n stuff right away
|
|
require ::RSpec::Core::RubyProject.root + "/spec/spec_helper"
|
|
|
|
super
|
|
end
|
|
|
|
def detect_instance_termination
|
|
File.delete("/tmp/instance_terminating") if File.exist?("/tmp/instance_terminating")
|
|
exit if instance_terminating?
|
|
|
|
Thread.new do
|
|
loop do
|
|
break if instance_terminating?
|
|
sleep 5
|
|
end
|
|
puts "Received spot instance termination warning, shutting down relay"
|
|
end
|
|
end
|
|
|
|
def instance_terminating?
|
|
system("curl -f -s -o /tmp/instance_terminating http://169.254.169.254/latest/meta-data/spot/termination-time")
|
|
end
|
|
|
|
def after_fork(num)
|
|
# for the runtime logger
|
|
ENV["TEST_ENV_NUMBER"] = num > 1 ? num.to_s : ""
|
|
ENV["SELINIMUM_BATCH_NAME"] = "#{ENV["SERVER_NUMBER"]}.#{ENV["TEST_ENV_NUMBER"]}"
|
|
Canvas::Reloader.reload!
|
|
TestDatabaseUtils.reconnect!
|
|
TestDatabaseUtils.reset_database!
|
|
Canvas.reconnect_redis
|
|
end
|
|
|
|
# test-queue does `exit!` in a couple places; override so at_exit hooks run (like simplecov)
|
|
# also preserve rspec exit codes
|
|
def summarize
|
|
output_profile_data if ::RSpec.configuration.profile_examples?
|
|
|
|
error_statuses = @completed.map { |worker| worker.status.exitstatus } - [0]
|
|
|
|
# a percentage of workers can abort/fail prior to running any specs
|
|
# (e.g. due to selenium/headless/firefox woes, etc)
|
|
allowed_sadness = 0.2
|
|
sadness_exit_status = 98
|
|
max_sadnesses = allowed_sadness * @completed.size
|
|
num_sadnesses = error_statuses.count(sadness_exit_status)
|
|
if num_sadnesses > 0 && num_sadnesses <= max_sadnesses
|
|
puts "Warning: #{num_sadnesses} workers failed prior to running specs, still below threshold"
|
|
error_statuses -= [sadness_exit_status]
|
|
end
|
|
|
|
error_statuses << 1 if @timed_out
|
|
error_statuses.uniq!
|
|
|
|
if error_statuses.empty?
|
|
# yay
|
|
exit 0
|
|
elsif error_statuses == [::RSpec.configuration.failure_exit_code]
|
|
# :'( but we can retry
|
|
exit ::RSpec.configuration.failure_exit_code
|
|
else
|
|
# this shouldn't happen, crap is broken
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
def run_worker(iterator)
|
|
@run_worker_ret = super
|
|
end
|
|
|
|
def cleanup_worker
|
|
Kernel.exit @run_worker_ret || 0
|
|
end
|
|
|
|
def output_profile_data
|
|
e = "(?:\e\\[\\d+m)?"
|
|
examples = raw_profile_output.scan(/^( .*\n #{e}([\d.]+)#{e} #{e}seconds#{e} \..*\n)/)
|
|
.sort_by { |e| e.last.to_f }
|
|
.map(&:first)
|
|
.reverse
|
|
.first(::RSpec.configuration.profile_examples)
|
|
puts "\nTop #{examples.count} slowest examples:"
|
|
puts examples.join
|
|
|
|
groups = raw_profile_output.scan(/^( .*\n #{e}([\d.]+)#{e} #{e}seconds#{e} average .*\n)/)
|
|
.sort_by { |e| e.last.to_f }
|
|
.map(&:first)
|
|
.reverse
|
|
.first(::RSpec.configuration.profile_examples)
|
|
puts "\nTop #{groups.count} slowest example groups:"
|
|
puts groups.join
|
|
puts
|
|
end
|
|
|
|
def raw_profile_output
|
|
@raw_profile_output ||= ""
|
|
end
|
|
|
|
def worker_completed(worker)
|
|
return if @aborting
|
|
if ::RSpec.configuration.profile_examples? && profile_output = worker.output.match(/^Top \d+ slowest.*\n(?=Finished in )/m)
|
|
profile_output = profile_output[0]
|
|
raw_profile_output << profile_output
|
|
# take it out of the output, if we're displaying it
|
|
worker.output.sub!(profile_output, "") if ENV['TEST_QUEUE_VERBOSE'] || worker.status.exitstatus != 0
|
|
end
|
|
super
|
|
end
|
|
end
|
|
|
|
TestQueue::Iterator.prepend(Module.new {
|
|
def query(*)
|
|
super
|
|
ensure
|
|
if !@done && instance_terminating?
|
|
puts "Received spot instance termination warning, shutting down worker"
|
|
@done = true
|
|
end
|
|
end
|
|
|
|
def instance_terminating?
|
|
File.exist?("/tmp/instance_terminating")
|
|
end
|
|
})
|
|
|
|
TestQueue::Runner::RSpec::LazyGroups::BackgroundLoaderProxy.singleton_class.prepend(Module.new {
|
|
def order_files(files)
|
|
files = super
|
|
puts "Running files according to descending cost (slowest spec + hooks)"
|
|
puts "Any file with a cost of Infinity either 1. is brand new or 2. has no specs and could maybe be removed"
|
|
files.each do |file|
|
|
base_cost = (file_group_map[file] || [])
|
|
.map { |group| stats[group] }
|
|
.compact
|
|
.max || Float::INFINITY
|
|
puts "#{file} cost: #{base_cost}, file size: #{File.size(file)}"
|
|
end
|
|
files
|
|
end
|
|
}) if defined?(TestQueue::Runner::RSpec::LazyGroups) && ENV["TEST_QUEUE_VERBOSE"]
|
|
|
|
CanvasSpecRunner.new.execute
|