cloudamatic/mu

View on GitHub
bin/mu-run-tests

Summary

Maintainability
Test Coverage
#!/usr/local/ruby-current/bin/ruby
# Copyright:: Copyright (c) 2019 eGlobalTech, Inc., all rights reserved
#
# Licensed under the BSD-3 license (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License in the root of the project or at
#
#     http://egt-labs.com/mu/LICENSE.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

bindir = File.realpath(File.expand_path(File.dirname(__FILE__)))
dir = File.realpath(File.expand_path(bindir+"/../modules/tests"))
Dir.chdir(dir)

require 'colorize'
require 'optimist'
require bindir+"/mu-load-config.rb"
require 'mu'

require 'rubygems'
require 'bundler/setup'
require 'json'
require 'erb'
require 'json-schema'

$opts = Optimist::options do
  banner <<-EOS
Usage:
#{$0} [-m <#>] [-f] [-v] [specific test BoK to run [...]]
  EOS
  opt :max_threads, "Environment to set on creation.", :require => false, :default => 3, :type => :integer
  opt :max_retries, "Number of times to retry failed tests in --dryrun mode.", :require => false, :default => 2, :type => :integer
  opt :full, "Actually run deploys, instead of --dryrun", :require => false, :default => false
  opt :verbose, "Show more information while running", :require => false, :default => false
  opt :clouds, "Select a subset of support cloud providers on which to test", :required => false, :type => :strings, :default => MU::Cloud.availableClouds.reject { |c| c == "CloudFormation" }
end

only = ARGV

files = Dir.glob("*.yaml", base: dir)
files.concat(Dir.glob("*.yml", base: dir))
valid_clouds = MU::Cloud.availableClouds.reject { |c| c == "CloudFormation" }
baseclouds = []
$opts[:clouds].each { |cloud|
  if !valid_clouds.include?(cloud)
    MU.log "'#{cloud}' isn't one of my available cloud providers", MU::ERR, details: valid_clouds
  else
    baseclouds << cloud
  end
}


commands = {}
failures = []
if only and only.size > 0
  files.reject! { |f| !only.include?(f) }
  if files.size == 0
    MU.log "No files in #{dir} matched requested list", MU::WARN, details: only
    exit 1
  end
end

files.each { |f|
  clouds = baseclouds.dup
  groomer_match = true
  File.open(dir+"/"+f).readlines.each { |l|
    l.chomp!
    if l.match(/^\s*#\s*clouds: (.*)/)
      clouds = []
      cloudstr = Regexp.last_match[1]
      cloudstr.split(/\s*,\s*/).each { |c|
        baseclouds.each { |cloud|
          if cloud.match(/^#{Regexp.quote(c)}$/i)
            clouds << cloud
          end
        }
      }
    elsif l.match(/^\s*#\s*groomers: (.*)/)
      groomerstr = Regexp.last_match[1]
      groomerstr.split(/\s*,\s*/).each { |g|
        if !MU::Groomer.availableGroomers.include?(g)
          MU.log "#{f} requires groomer #{g}, which is not available. This test will be skipped.", MU::NOTICE
          groomer_match = false
        end
      }
    end
  }
  if !groomer_match
    next
  end

  clouds.each { |cloud|
    cmd = "mu-deploy #{f} --cloud #{cloud} #{$opts[:full] ? "" : "--dryrun"}"
    commands[cmd] = {
      "file" => f,
      "cloud" => cloud,
    }
    if $opts[:full]
      $environment = "dev"
      begin
        conf_engine = MU::Config.new(f, cloud: cloud)
      rescue StandardError => e
        MU.log e.message+" parsing "+f+" with cloud "+cloud, MU::WARN, details: e.backtrace
        failures << f+" ["+commands[cmd]["cloud"]+"] - "+e.class.name+"\n\t"+e.message.gsub(/\n/, "\t\n")
        next 
      end
      parsed = MU::Config.stripConfig(conf_engine.config)
      types = []
      MU::Cloud.resource_types.values.each { |attrs|
        if parsed.has_key?(attrs[:cfg_plural])
          types << attrs[:cfg_plural]
        end
      }
      commands[cmd]["parsed"] = parsed
      commands[cmd]["types"] = types
    end
  }
}

puts "Running #{commands.size.to_s.bold} #{$opts[:full] ? "full deploy" : "parse"} tests from #{files.size.to_s.bold} Baskets of Kittens across #{baseclouds.size.to_s.bold} clouds"

@output_semaphore = Mutex.new

def execCommand(cmd, results_stash)
  @output_semaphore.synchronize {
    puts cmd if $opts[:verbose]
  }

  ok = true
  retries = 0
  begin
    output = %x{#{cmd} 2>&1}
    if $?.exitstatus != 0
      ok = false
      retries += 1
      if $opts[:verbose] and !$opts[:full] and retries <= $opts[:max_retries]
        puts "#{cmd} RETRY #{retries.to_s}".light_red
      end
    else
      ok = true
    end
  end while !ok and !$opts[:full] and retries <= $opts[:max_retries]

  results_stash["output"] += output

  @output_semaphore.synchronize {
    if ok
      if $opts[:verbose]
        puts "#{cmd} SUCCEEDED".green
      else
        print ".".green
      end
    else
      if $opts[:verbose]
        puts "#{cmd} FAILED:".light_red
        puts output
      else
        print ".".light_red
      end
    end
  }

  ok
end

threads = []
results = {}
commands.keys.each { |cmd|
  if threads.size >= $opts[:max_threads]
    begin
      threads.each { |t| t.join(0.1) }
      threads.reject! { |t| t.nil? or !t.status }
      sleep 1 if threads.size >= $opts[:max_threads]
    end while threads.size >= $opts[:max_threads]
  end

  threads << Thread.new(cmd) { |cmd_thr|
    results[cmd_thr] = { "output" => "", "failed" => [] }
    if !execCommand(cmd_thr, results[cmd_thr])
      results[cmd_thr]["failed"] << "main"
    end

    if $opts[:full] and results[cmd_thr]["output"].to_s.match(/deploy - Deployment id: .*? \((.*?)\)/)
      deploy_id = Regexp.last_match[1]
      adoptdir = Dir.mktmpdir(commands[cmd_thr]["file"].gsub(/[^a-z0-9]|yaml$/i, ""))
      if commands[cmd_thr]["types"] and commands[cmd_thr]["types"].size > 0
        adopt = "cd #{adoptdir} && mu-adopt --appname adoptone --grouping omnibus  --clouds #{commands[cmd_thr]["cloud"]} --types #{commands[cmd_thr]["types"].join(" ")} 2>&1"
        if !execCommand(adopt, results[cmd_thr])
          results[cmd_thr]["failed"] << "adopt"
        end
      end

      if File.exist?(dir+"/regrooms/"+commands[cmd_thr]["file"])
        regroom = "mu-deploy regrooms/#{commands[cmd_thr]["file"]} --cloud #{commands[cmd_thr]["cloud"]} --update #{deploy_id} 2>&1"
        if !execCommand(regroom, results[cmd_thr])
          results[cmd_thr]["failed"] << "regroom"
        end
        if commands[cmd_thr]["types"] and commands[cmd_thr]["types"].size > 0
          re_adopt = "cd #{adoptdir} && mu-adopt --appname adopttwo --grouping omnibus --clouds #{commands[cmd_thr]["cloud"]} --types #{commands[cmd_thr]["types"].join(" ")} 2>&1"
          if !execCommand(re_adopt, results[cmd_thr])
            results[cmd_thr]["failed"] << "second adopt"
          end
        end
# TODO big flex is to read back both adopted BoKs and .diff them, but without
# all resources having implemented adoption this isn't much of a test yet
      end

      FileUtils.remove_entry(adoptdir)
      cleanup = %Q{mu-cleanup #{deploy_id} --skipsnapshots}
      if !execCommand(cleanup, results[cmd_thr])
        results[cmd_thr]["failed"] << "cleanup"
      end
    end
  }
}
threads.each { |t|
  t.join
}
puts ""

results.keys.sort { |a, b|
      results[b]["failed"].size <=> results[a]["failed"].size
    }.each { |cmd|
  if results[cmd]["failed"].size > 0
    puts cmd+" failed:".light_red
    puts results[cmd]["output"].yellow
    puts "^ #{cmd}".light_red
    failures << commands[cmd]["file"]+" in "+commands[cmd]["cloud"]+" ("+results[cmd]["failed"].join(", ")+")"
  else
    puts cmd+" passed".green
  end
}

if failures.size > 0
  puts "\n#{failures.size.to_s.bold} failure#{failures.size == 1 ? "" : "s"} in:\n"+failures.uniq.map { |f| f.light_red }.join("\n")
  exit 1
end