inspec/test/functional/helper.rb
Ryan Davis 1ed4128942 Functional helper let changes.
Moved exec_inspec to inspec_path.
Added new exec_inspec that invokes ruby w/ -Ilib (expanded).

Decouples from bundler and/or needing inspec-bin to be installed.

Signed-off-by: Ryan Davis <zenspider@chef.io>
2019-05-17 02:26:06 -07:00

278 lines
9.7 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# author: Dominik Richter
# author: Christoph Hartmann
require 'helper'
require 'rbconfig'
require 'json'
require 'fileutils'
require 'yaml'
require 'tmpdir'
require 'minitest/hell'
class Minitest::Test
parallelize_me!
end
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
module FunctionalHelper
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.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) {
# 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
FileUtils.rm(res.path)
TMP_CACHE[res.path] = res
}
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 = "cmd /C \"#{prefix} bundle exec ruby #{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
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
run_result.payload.json = JSON.parse(run_result.stdout)
rescue JSON::ParserError => e
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)
block.call(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
end