inspec/test/functional/helper.rb

297 lines
10 KiB
Ruby
Raw Normal View History

require "helper"
require "train"
2016-03-25 00:31:19 +00:00
ENV["CHEF_LICENSE"] = "accept-no-persist"
CMD = Train.create("local", command_runner: :generic).connection
2016-03-25 00:31:19 +00:00
class Module
include Minitest::Spec::DSL
end
module Inspec
class FuncTestRunResult
attr_reader :train_result
attr_reader :payload
extend Forwardable
def_delegator :train_result, :stdout
def_delegator :train_result, :stderr
def_delegator :train_result, :exit_status
def initialize(train_result)
@train_result = train_result
@payload = OpenStruct.new
end
# Intentional failure to cause CI to print output
def diagnose!
msg = ""
msg += "\nInvocation:\n" + payload.invocation
msg += "\nSTDOUT:\n" + stdout
msg += "\nSTDERR:\n" + stderr
msg.must_equal ""
end
def stderr_ignore_deprecations
suffix = stderr.end_with?("\n") ? "\n" : ""
stderr.split("\n").reject { |l| l.include? " DEPRECATION: " }.join("\n") + suffix
end
def stdout_ignore_deprecations
suffix = stdout.end_with?("\n") ? "\n" : ""
stdout.split("\n").reject { |l| l.include? " DEPRECATION: " }.join("\n") + suffix
end
# This works if you use json: true on an exec call
def must_have_all_controls_passing
# Strategy: assemble an array of tests that failed or skipped, and insist it is empty
# result.payload.json['profiles'][0]['controls'][0]['results'][0]['status']
failed_tests = []
payload.json["profiles"].each do |profile_struct|
profile_name = profile_struct["name"]
profile_struct["controls"].each do |control_struct|
control_name = control_struct["id"]
control_struct["results"].compact.each do |test_struct|
test_desc = test_struct["code_desc"]
if test_struct["status"] != "passed"
failed_tests << "#{profile_name}/#{control_name}/#{test_desc}"
end
end
end
end
failed_tests.must_be_empty
end
end
end
2016-03-25 00:31:19 +00:00
module FunctionalHelper
extend Minitest::Spec::DSL
let(:repo_path) do
path = File.expand_path(File.join( __FILE__, "..", "..", ".."))
# fix for vagrant repo pathing
path.gsub!("//vboxsrv", "C:") if is_windows?
path
end
let(:inspec_path) { File.join(repo_path, "inspec-bin", "bin", "inspec") }
libdir = File.expand_path "lib"
let(:exec_inspec) { [Gem.ruby, "-I#{libdir}", inspec_path].join " " }
let(:mock_path) { File.join(repo_path, "test", "unit", "mock") }
let(:profile_path) { File.join(mock_path, "profiles") }
let(:examples_path) { File.join(profile_path, "old-examples") }
let(:integration_test_path) { File.join(repo_path, "test", "integration", "default") }
let(:example_profile) { File.join(examples_path, "profile") }
let(:meta_profile) { File.join(examples_path, "meta-profile") }
let(:example_control) { File.join(example_profile, "controls", "example-tmp.rb") }
let(:inheritance_profile) { File.join(examples_path, "inheritance") }
let(:failure_control) { File.join(profile_path, "failures", "controls", "failures.rb") }
let(:simple_inheritance) { File.join(profile_path, "simple-inheritance") }
let(:sensitive_profile) { File.join(examples_path, "profile-sensitive") }
let(:config_dir_path) { File.join(mock_path, "config_dirs") }
let(:dst) do
2016-03-25 00:31:19 +00:00
# create a temporary path, but we only want an auto-clean helper
# so remove the file and give back the path
res = Tempfile.new("inspec-shred")
res.close
2016-03-25 00:31:19 +00:00
FileUtils.rm(res.path)
TMP_CACHE[res.path] = res
end
2016-03-25 00:31:19 +00:00
ROOT_LICENSE_PATH = "/etc/chef/accepted_licenses/inspec".freeze
def without_license
ENV.delete "CHEF_LICENSE"
FileUtils.rm_f ROOT_LICENSE_PATH
yield
FileUtils.rm_f ROOT_LICENSE_PATH
ensure
ENV["CHEF_LICENSE"] = "accept-no-persist"
end
def assert_exit_code(exp, cmd)
exp = 1 if windows? && (exp != 0)
assert_equal exp, cmd.exit_status
end
def convert_windows_output(text)
text = text.force_encoding("UTF-8")
text.gsub!("[PASS]", "")
text.gsub!("\033[0;1;32m", "\033[38;5;41m")
text.gsub!("[SKIP]", "")
text.gsub!("\033[0;37m", "\033[38;5;247m")
text.gsub!("[FAIL]", "×")
text.gsub!("\033[0;1;31m", "\033[38;5;9m")
end
def self.is_windows?
RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/
end
def is_windows?
FunctionalHelper.is_windows?
end
def inspec(commandline, prefix = nil)
if is_windows?
invocation = "/windows/system32/cmd /C \"#{prefix} #{exec_inspec} #{commandline}\""
result = CMD.run_command(invocation)
result.stdout.encode!(universal_newline: true)
result.stderr.encode!(universal_newline: true)
convert_windows_output(result.stdout)
# remove the CLIXML header trash in windows
result.stderr.gsub!("#< CLIXML\n", "")
ftrr = Inspec::FuncTestRunResult.new(result)
else
invocation = "#{prefix} #{exec_inspec} #{commandline}"
ftrr = Inspec::FuncTestRunResult.new(CMD.run_command(invocation))
end
ftrr.payload.invocation = invocation
ftrr
2016-03-25 00:31:19 +00:00
end
def inspec_with_env(commandline, env = {})
inspec(commandline, assemble_env_prefix(env))
end
# This version allows additional options.
# @param String command_line Invocation, without the word 'inspec'
# @param Hash opts Additonal options, see below
# :env Hash A hash of environment vars to expose to the invocation.
# :prefix String A string to prefix to the invocation. Prefix + env + invocation is the order.
# :cwd String A directory to change to. Implemented as 'cd CWD && ' + prefix
# :lock Boolean Default false. If false, add `--no-create-lockfile`.
# :json Boolean Default false. If true, add `--reporter json` and parse the output, which is stored in payload.json .
# :tmpdir Boolean default true. If true, wrap execution in a Dir.tmpdir block. Use pre_run and post_run to trigger actions.
# :pre_run: Proc(tmp_dir_path) - optional setup block.
# tmp_dir will exist and be empty.
# :post_run: Proc(FuncTestRunResult, tmp_dir_path) - optional result capture block.
# run_result will be populated, but you can add more to the ostruct .payload
# tmp_dir will still exist (for a moment!)
# @return FuncTestRunResult. Includes attrs exit_status, stderr, stdout, payload (an openstruct which may be used in many ways)
def run_inspec_process(command_line, opts = {})
raise "Do not use tmpdir and cwd in the same invocation" if opts[:cwd] && opts[:tmpdir]
prefix = opts[:cwd] ? "cd " + opts[:cwd] + " && " : ""
prefix += opts[:prefix] || ""
prefix += assemble_env_prefix(opts[:env])
command_line += " --reporter json " if opts[:json] && command_line =~ /\bexec\b/
command_line += " --no-create-lockfile " if (!opts[:lock]) && command_line =~ /\bexec\b/
run_result = nil
if opts[:tmpdir]
Dir.mktmpdir do |tmp_dir|
opts[:pre_run].call(tmp_dir) if opts[:pre_run]
# Do NOT Dir.chdir here - chdir / pwd is per-process, and we are in the
# test harness process, which will be multithreaded because we parallelize the tests.
# Instead, make the spawned process change dirs using a cd prefix.
prefix = "cd " + tmp_dir + " && " + prefix
run_result = inspec(command_line, prefix)
opts[:post_run].call(run_result, tmp_dir) if opts[:post_run]
end
else
run_result = inspec(command_line, prefix)
end
if opts[:ignore_rspec_deprecations]
# RSpec keeps issuing a deprecation count to stdout when .should is called explicitly
# See https://github.com/inspec/inspec/pull/3560
run_result.stdout.sub!("\n1 deprecation warning total\n", "")
end
if opts[:json]
begin
payload = JSON.parse(run_result.stdout)
run_result.payload.json = payload
rescue JSON::ParserError => e
warn "JSON PARSE ERROR: %s" % [e.message]
warn "OUT: <<%s>>" % [run_result.stdout]
warn "ERR: <<%s>>" % [run_result.stderr]
warn "XIT: %p" % [run_result.exit_status]
run_result.payload.json = {}
run_result.payload.json_error = e
end
end
run_result
end
# Copy all examples to a temporary directory for functional tests.
# You can provide an optional directory which will be handed to your
# test block with its absolute path. If nothing is provided you will
# get the path of the examples directory in the tmp environment.
#
# @param dir = nil [String] optional directory you want to test
# @param &block [Type] actual test block
def prepare_examples(dir = nil, &block)
Dir.mktmpdir do |tmpdir|
FileUtils.cp_r(examples_path, tmpdir)
bn = File.basename(examples_path)
yield(File.join(tmpdir, bn, dir.to_s))
end
end
private
def assemble_env_prefix(env = {})
if is_windows?
env_prefix = env.to_a.map { |assignment| "set #{assignment[0]}=#{assignment[1]}" }.join("&& ")
env_prefix += "&& " unless env_prefix.empty?
else
env_prefix = env.to_a.map { |assignment| "#{assignment[0]}=#{assignment[1]}" }.join(" ")
env_prefix += " "
end
env_prefix
end
end
#=========================================================================================#
# Plugin Support
#=========================================================================================#
module PluginFunctionalHelper
include FunctionalHelper
def run_inspec_with_plugin(command, opts)
pre = Proc.new do |tmp_dir|
content = JSON.generate(__make_plugin_file_data_structure_with_path(opts[:plugin_path]))
File.write(File.join(tmp_dir, "plugins.json"), content)
end
opts.merge!({
pre_run: pre,
tmpdir: true,
json: true,
env: {
"INSPEC_CONFIG_DIR" => ".", # We're in tmpdir
},
})
run_inspec_process(command, opts)
end
def __make_plugin_file_data_structure_with_path(path)
# TODO: dry this up, refs #3350
plugin_name = File.basename(path, ".rb")
data = __make_empty_plugin_file_data_structure
data["plugins"] << {
"name" => plugin_name,
"installation_type" => "path",
"installation_path" => path,
}
data
end
def __make_empty_plugin_file_data_structure
# TODO: dry this up, refs #3350
{
"plugins_config_version" => "1.0.0",
"plugins" => [],
}
end
2016-03-25 00:31:19 +00:00
end