sensu-plugins/sensu-plugins-rspec

View on GitHub
bin/check-test-suite.rb

Summary

Maintainability
C
7 hrs
Test Coverage
#!/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