bin/mu-run-tests
#!/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