bin/check-test-suite.rb
#!/usr/bin/env ruby
# encoding: UTF-8
# check-test-suite
#
# DESCRIPTION:
# This plugin attempts to run rspec and return the results of the run to the handler.
#
# When a run begins, it creates a file cache that it will reference on the next run to
# prevent false positives from being returned to the handler. If the run fails a particular
# codebase twice, then it will return critical to the handler, not on the first fail.
#
# OUTPUT:
# plain text
#
# PLATFORMS:
# Linux
#
# DEPENDENCIES:
# gem: sensu-plugin
# gem: rspec
# gem: fileutils
#
# USAGE:
# Recommended usage:
# sudo /opt/sensu/embedded/bin/ruby /etc/sensu/plugins/check-test-suite.rb -p codebase1,codebase2,codebase3 -b /path/to/codebase's/ruby
#
# NOTES:
# codebase should be a full path to the root of the codebase (current folder ideally)
# sudo is preferred but may not be necessary
# Depending on your test suite, the sensu user may not have write permissions to the directories where the code is to deal with things like coverage gem)
# The codebases must be managed through git to be effective, the check relies on being able to find the commits in the filesystem.
#
# LICENSE:
# Louis Alridge louis@socialcentiv.com (loualrid@gmail.com)
# Released under the same terms as Sensu (the MIT license); see LICENSE
# for details.
require 'json'
require 'rspec'
require 'fileutils'
require 'sensu-plugin/check/cli'
#
# CheckTestSuite Class
#
class CheckTestSuite < Sensu::Plugin::Check::CLI
option :paths,
description: 'Paths to run the tests, comma delimited',
short: '-p PATHS',
long: '--path PATHS'
option :ruby_bin,
description: 'Location of ruby bin, it is highly recommended to use the rvm gemset ruby if utilizing rvm',
short: '-b ruby',
long: '--ruby-bin ruby',
default: 'ruby'
option :environment_variables,
description: 'Optional additional environmental variables to pass to ruby and/or rspec',
short: '-e aws_access_key_id=XXX',
long: '--env-var aws_access_key_id=XXX',
required: false
option :test_suite,
description: 'Test suite to test against, defaults to rspec',
short: '-t SUITE',
long: '--test-suite SUITE',
default: 'rspec'
option :suite_arguments,
description: 'Optional args to pass to rspec, defaults to --fail-fast. Enclose args in quotes or else the plugin will error',
short: '-a "RSPEC_ARGS"',
long: '--args "RSPEC_ARGS"',
default: '--fail-fast'
option :gem_home,
description: 'Attempts to utilize the bundle stored in vendor/bundle. Cookbooks not using the standard gem location will need to set this',
short: '-d GEM_HOME',
long: '--gem-home GEM_HOME',
default: 'vendor/bundle'
def initialize_file_cache(branch, commit)
commit_file_directory = "/var/log/sensu/check-test-suite-#{branch}"
FileUtils.mkdir_p commit_file_directory
write_file_cache_message "#{commit_file_directory}/#{commit}", 'verified'
end
def write_file_cache_message(location, message)
if !File.exist?(location)
File.open(location, 'w') { |f| f.write(message) }
else
File.open(location, 'a') { |f| f.puts(message) }
end
end
def run #rubocop:disable all
full_start = Time.now
tests = {}
successful_tests = {}
final_gem_home = config[:gem_home]
config[:paths].split(',').each do |path|
start = Time.now
tests[path] = {}
tests[path]['commit'] = `/bin/readlink #{ path }`.split('/').last.chomp.strip
tests[path]['branch'] = `cd #{ path } && /usr/bin/git branch -r --contains #{ tests[path]['commit'] }`.split("\n").last.chomp.strip.split('origin/').last
initialize_file_cache tests[path]['branch'], tests[path]['commit']
commit_file = "/var/log/sensu/check-test-suite-#{tests[path]['branch']}/#{tests[path]['commit']}"
next if File.exist?(commit_file) && File.read(commit_file).include?('successful')
if config[:gem_home] == 'vendor/bundle'
target_ruby = ''
target_rubies = Dir.entries("#{path}/#{config[:gem_home]}/ruby").select { |item| item =~ /(\d+\.\d+\.\d+)/ }
target_rubies.each do |ruby|
target_rubies.each do |other_ruby|
target_ruby = if ruby != other_ruby
ruby if Gem::Version.new(ruby) > Gem::Version.new(other_ruby)
elsif target_rubies.count == 1
ruby
end
end
end
final_gem_home = `/bin/readlink #{ path }`.chomp.strip + "/#{config[:gem_home]}/ruby/#{target_ruby}"
end
ENV['GEM_HOME'] = final_gem_home
test_suite_args = [
"cd #{path};",
"#{config[:environment_variables]} #{config[:ruby_bin]} -S #{config[:test_suite]}",
"#{config[:suite_arguments]} --failure-exit-code 2"
]
tests[path]['test_suite_out'] = `#{ test_suite_args.join(' ') }`
tests[path]['runtime'] = Time.now - start
tests[path]['exitstatus'] = $CHILD_STATUS.exitstatus
tests[path]['commit'] = `/bin/readlink #{ path }`.split('/').last
target_branch = `cd #{ path } && /usr/bin/git branch -r --contains #{ tests[path]['commit'] }`
tests[path]['branch'] = target_branch.split("\n").last.chomp.strip.split('origin/').last
tests[path]['metadata'] = `cd #{ path } && /usr/bin/git show #{ tests[path]['commit'] }`
case tests[path]['exitstatus']
when 2
test_suite_lines = tests[path]['test_suite_out'].split("\n")
test_suite_out_fail_line = test_suite_lines.index(test_suite_lines.find { |line| line.include?('Failures:') })
write_file_cache_message commit_file, 'failure'
# To eliminate false positives, we run a failing suite twice before sending the response
next if File.read(commit_file).scan(/failure/).count < 2
critical_out = [
"CRITICAL! Rspec returned failed tests for #{tests[path]['branch']}!",
"#{tests[path]['metadata']}#{test_suite_lines[test_suite_out_fail_line..(test_suite_lines.count)].join("\n")}",
"Error'd in #{tests[path]['runtime']} seconds."
]
critical critical_out.join("\n\n")
when 0
successful_tests[path] = tests[path]
write_file_cache_message commit_file, 'successful'
if config[:paths].split(',').length == 1
ok "OK! Rspec returned no failed tests for #{tests[path]['branch']}.\n\n#{tests[path]['metadata']}\n\nCompleted in #{tests[path]['runtime']}"
end
else
unknown "Strange exit status detected for rspec on #{tests[path]['branch']}.\n\n#{tests[path]['test_suite_out']}"
end
end
successful_branches = []
successful_tests.each_pair { |_key, hash| successful_branches << hash['branch'] }
ok "OK! Rspec returned no failed tests for #{successful_branches.join(', ')}.\nCompleted in #{full_start - Time.now} seconds."
rescue StandardError => e
critical "Error message: #{e}\n#{e.backtrace.join("\n")}"
end
end