mirror of
https://github.com/inspec/inspec
synced 2025-02-17 06:28:40 +00:00
Merge pull request #790 from chef/dr/default-formatter
introduce cli report formatter
This commit is contained in:
commit
7be522a9e1
6 changed files with 326 additions and 65 deletions
|
@ -74,18 +74,16 @@ end
|
|||
class InspecRspecJson < InspecRspecMiniJson
|
||||
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
|
||||
|
||||
def initialize(*args)
|
||||
super(*args)
|
||||
@profiles = []
|
||||
end
|
||||
|
||||
def add_profile(profile)
|
||||
@profiles ||= []
|
||||
@profiles.push(profile)
|
||||
end
|
||||
|
||||
def dump_one_example(example, profiles, missing)
|
||||
profile = profiles[example[:profile_id]]
|
||||
return missing.push(example) if profile.nil? || profile[:controls].nil?
|
||||
|
||||
control = profile[:controls][example[:id]]
|
||||
return missing.push(example) if control.nil?
|
||||
|
||||
def dump_one_example(example, control)
|
||||
control[:results] ||= []
|
||||
example.delete(:id)
|
||||
example.delete(:profile_id)
|
||||
|
@ -99,13 +97,14 @@ class InspecRspecJson < InspecRspecMiniJson
|
|||
|
||||
def dump_summary(summary)
|
||||
super(summary)
|
||||
@profiles ||= []
|
||||
examples = @output_hash.delete(:controls)
|
||||
profiles = Hash[@profiles.map { |x| profile_info(x) }]
|
||||
missing = []
|
||||
|
||||
examples.each do |example|
|
||||
dump_one_example(example, profiles, missing)
|
||||
control = example2control(example, profiles)
|
||||
next missing.push(example) if control.nil?
|
||||
dump_one_example(example, control)
|
||||
end
|
||||
|
||||
@output_hash[:profiles] = profiles
|
||||
|
@ -114,6 +113,12 @@ class InspecRspecJson < InspecRspecMiniJson
|
|||
|
||||
private
|
||||
|
||||
def example2control(example, profiles)
|
||||
profile = profiles[example[:profile_id]]
|
||||
return nil if profile.nil? || profile[:controls].nil?
|
||||
profile[:controls][example[:id]]
|
||||
end
|
||||
|
||||
def format_example(example)
|
||||
super(example).tap do |res|
|
||||
res[:run_time] = example.execution_result.run_time
|
||||
|
@ -121,3 +126,191 @@ class InspecRspecJson < InspecRspecMiniJson
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
|
||||
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
|
||||
|
||||
STATUS_TYPES = {
|
||||
'unknown' => -3,
|
||||
'passed' => -2,
|
||||
'skipped' => -1,
|
||||
'minor' => 1,
|
||||
'major' => 2,
|
||||
'failed' => 2.5,
|
||||
'critical' => 3,
|
||||
}.freeze
|
||||
|
||||
COLORS = {
|
||||
'critical' => "\033[31;1m",
|
||||
'major' => "\033[31m",
|
||||
'minor' => "\033[33m",
|
||||
'failed' => "\033[31m",
|
||||
'passed' => "\033[32m",
|
||||
'skipped' => "\033[37m",
|
||||
'reset' => "\033[0m",
|
||||
}.freeze
|
||||
|
||||
INDICATORS = {
|
||||
'critical' => '[CRIT] ',
|
||||
'major' => '[FAIL] ',
|
||||
'minor' => '[WARN] ',
|
||||
'failed' => '[FAIL] ',
|
||||
'skipped' => '[SKIP] ',
|
||||
'passed' => '[PASS] ',
|
||||
'unknown' => '[ ?? ] ',
|
||||
'empty' => ' ',
|
||||
}.freeze
|
||||
|
||||
TEST_INDICATORS = {
|
||||
'failed' => ' fail: ',
|
||||
'skipped' => ' skip: ',
|
||||
'empty' => ' ',
|
||||
}.freeze
|
||||
|
||||
def initialize(*args)
|
||||
@colors = COLORS
|
||||
@indicators = INDICATORS
|
||||
@test_indicators = TEST_INDICATORS
|
||||
|
||||
@format = '%color%indicator%id %summary'
|
||||
@current_control = nil
|
||||
@missing_controls = []
|
||||
super(*args)
|
||||
end
|
||||
|
||||
def start(_notification)
|
||||
output.puts ''
|
||||
|
||||
@profiles_info ||= Hash[@profiles.map { |x| profile_info(x) }]
|
||||
profiles = @profiles_info.values.find_all { |x| !x[:title].nil? || !x[:name].nil? }
|
||||
profiles.each do |profile|
|
||||
if profile[:title].nil?
|
||||
output.puts "Profile: #{profile[:name] || 'unknown'}"
|
||||
else
|
||||
output.puts "Profile: #{profile[:title]} (#{profile[:name] || 'unknown'})"
|
||||
end
|
||||
|
||||
output.puts 'Version: ' + (profile[:version] || 'unknown')
|
||||
output.puts ''
|
||||
end
|
||||
output.puts '' unless profiles.empty?
|
||||
end
|
||||
|
||||
def close(_notification)
|
||||
flush_current_control
|
||||
|
||||
output.puts '' unless @current_control.nil?
|
||||
res = @output_hash[:summary]
|
||||
passed = res[:example_count] - res[:failure_count] - res[:skip_count]
|
||||
s = format('Summary: %3d successful %3d failures %3d skipped',
|
||||
passed, res[:failure_count], res[:skip_count])
|
||||
output.puts(s)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def status_type(data, control)
|
||||
status = data[:status]
|
||||
return status if status != 'failed' || control[:impact].nil?
|
||||
if control[:impact] > 0.7
|
||||
'critical'
|
||||
elsif control[:impact] > 0.4
|
||||
'major'
|
||||
elsif control[:impact] > 0.0
|
||||
'minor'
|
||||
else
|
||||
'failed'
|
||||
end
|
||||
end
|
||||
|
||||
def current_control_infos
|
||||
summary_status = STATUS_TYPES['unknown']
|
||||
skips = []
|
||||
fails = []
|
||||
@current_control[:results].each do |r|
|
||||
i = STATUS_TYPES[r[:status_type]]
|
||||
summary_status = i if i > summary_status
|
||||
fails.push(r) if i > 0
|
||||
skips.push(r) if i == STATUS_TYPES['skipped']
|
||||
end
|
||||
[fails, skips, STATUS_TYPES.key(summary_status)]
|
||||
end
|
||||
|
||||
def current_control_summary(fails, skips)
|
||||
sum_info = [
|
||||
(fails.length > 0) ? "#{fails.length} failed" : nil,
|
||||
(skips.length > 0) ? "#{skips.length} skipped" : nil,
|
||||
].compact
|
||||
|
||||
summary = @current_control[:title]
|
||||
unless summary.nil?
|
||||
return summary + ' (' + sum_info.join(' ') + ')' unless sum_info.empty?
|
||||
return summary
|
||||
end
|
||||
|
||||
return sum_info.join(' ') if @current_control[:results].length != 1
|
||||
|
||||
fails.clear
|
||||
skips.clear
|
||||
c = @current_control[:results][0]
|
||||
c[:code_desc].to_s + c[:message].to_s
|
||||
end
|
||||
|
||||
def format_line(fields)
|
||||
@format.gsub(/%\w+/) do |x|
|
||||
term = x[1..-1]
|
||||
fields.key?(term.to_sym) ? fields[term.to_sym].to_s : x
|
||||
end + @colors['reset']
|
||||
end
|
||||
|
||||
def flush_current_control
|
||||
return if @current_control.nil?
|
||||
fails, skips, summary_indicator = current_control_infos
|
||||
summary = current_control_summary(fails, skips)
|
||||
|
||||
control_id = @current_control[:id]
|
||||
control_id = nil if control_id.start_with? '(generated from '
|
||||
|
||||
f = format_line(
|
||||
color: @colors[summary_indicator] || '',
|
||||
indicator: @indicators[summary_indicator] || @indicators['unknown'],
|
||||
summary: summary,
|
||||
id: control_id,
|
||||
profile: @current_control[:profile_id],
|
||||
)
|
||||
output.puts(f)
|
||||
|
||||
all_lines = (fails + skips)
|
||||
all_lines.each do |x|
|
||||
indicator = @test_indicators[x[:status]]
|
||||
indicator = @test_indicators['empty'] if all_lines.length == 1 || indicator.nil?
|
||||
f = format_line(
|
||||
color: @colors[summary_indicator] || '',
|
||||
indicator: indicator,
|
||||
summary: x[:message] || x[:skip_message] || x[:code_desc],
|
||||
id: nil, profile: nil
|
||||
)
|
||||
output.puts(f)
|
||||
end
|
||||
end
|
||||
|
||||
def format_example(example)
|
||||
data = super(example)
|
||||
control = example2control(data, @profiles_info) || {}
|
||||
control[:id] = data[:id]
|
||||
control[:profile_id] = data[:profile_id]
|
||||
|
||||
data[:status_type] = status_type(data, control)
|
||||
dump_one_example(data, control)
|
||||
|
||||
@current_control ||= control
|
||||
if control[:id].nil?
|
||||
@missing_controls.push(data)
|
||||
elsif @current_control[:id] != control[:id]
|
||||
flush_current_control
|
||||
@current_control = control
|
||||
end
|
||||
|
||||
data
|
||||
end
|
||||
end
|
||||
|
|
|
@ -90,6 +90,7 @@ module Inspec
|
|||
'json-min' => 'InspecRspecMiniJson',
|
||||
'json' => 'InspecRspecJson',
|
||||
'json-rspec' => 'InspecRspecVanilla',
|
||||
'cli' => 'InspecRspecCli',
|
||||
}.freeze
|
||||
|
||||
# Configure the output formatter and stream to be used with RSpec.
|
||||
|
@ -102,7 +103,7 @@ module Inspec
|
|||
RSpec.configuration.output_stream = @conf['output']
|
||||
end
|
||||
|
||||
format = FORMATTERS[@conf['format']] || @conf['format'] || 'progress'
|
||||
format = FORMATTERS[@conf['format']] || @conf['format'] || FORMATTERS['cli']
|
||||
@formatter = RSpec.configuration.add_formatter(format)
|
||||
RSpec.configuration.color = @conf['color']
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ module Inspec
|
|||
option :controls, type: :array,
|
||||
desc: 'A list of controls to run. Ignore all other tests.'
|
||||
option :format, type: :string,
|
||||
desc: 'Which formatter to use: progress, documentation, json'
|
||||
desc: 'Which formatter to use: cli, progress, documentation, json, json-min'
|
||||
option :color, type: :boolean, default: true,
|
||||
desc: 'Use colors in output.'
|
||||
option :attrs, type: :array,
|
||||
|
|
59
test/functional/inspec_exec_jsonmin_test.rb
Normal file
59
test/functional/inspec_exec_jsonmin_test.rb
Normal file
|
@ -0,0 +1,59 @@
|
|||
# encoding: utf-8
|
||||
# author: Dominik Richter
|
||||
# author: Christoph Hartmann
|
||||
|
||||
require 'functional/helper'
|
||||
|
||||
describe 'inspec exec' do
|
||||
include FunctionalHelper
|
||||
|
||||
it 'can execute the profile with the mini json formatter' do
|
||||
out = inspec('exec ' + example_profile + ' --format json-min')
|
||||
out.stderr.must_equal ''
|
||||
out.exit_status.must_equal 0
|
||||
JSON.load(out.stdout).must_be_kind_of Hash
|
||||
end
|
||||
|
||||
it 'can execute a simple file with the mini json formatter' do
|
||||
out = inspec('exec ' + example_control + ' --format json-min')
|
||||
out.stderr.must_equal ''
|
||||
out.exit_status.must_equal 0
|
||||
JSON.load(out.stdout).must_be_kind_of Hash
|
||||
end
|
||||
|
||||
describe 'execute a profile with mini json formatting' do
|
||||
let(:json) { JSON.load(inspec('exec ' + example_profile + ' --format json-min').stdout) }
|
||||
let(:controls) { json['controls'] }
|
||||
let(:ex1) { controls.find{|x| x['id'] == 'tmp-1.0'} }
|
||||
let(:ex2) { controls.find{|x| x['id'] =~ /generated/} }
|
||||
let(:ex3) { controls.find{|x| x['id'] == 'gordon-1.0'} }
|
||||
|
||||
it 'must have 5 examples' do
|
||||
json['controls'].length.must_equal 5
|
||||
end
|
||||
|
||||
it 'has an id' do
|
||||
controls.find { |ex| !ex.key? 'id' }.must_be :nil?
|
||||
end
|
||||
|
||||
it 'has a profile_id' do
|
||||
controls.find { |ex| !ex.key? 'profile_id' }.must_be :nil?
|
||||
end
|
||||
|
||||
it 'has a code_desc' do
|
||||
ex1['code_desc'].must_equal 'File /tmp should be directory'
|
||||
controls.find { |ex| !ex.key? 'code_desc' }.must_be :nil?
|
||||
end
|
||||
|
||||
it 'has a status' do
|
||||
ex1['status'].must_equal 'passed'
|
||||
ex3['status'].must_equal 'skipped'
|
||||
end
|
||||
|
||||
it 'has a skip_message' do
|
||||
ex1['skip_message'].must_be :nil?
|
||||
ex3['skip_message'].must_equal "Can't find file \"/tmp/gordon/config.yaml\""
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -11,71 +11,68 @@ describe 'inspec exec' do
|
|||
out = inspec('exec ' + example_profile)
|
||||
out.stderr.must_equal ''
|
||||
out.exit_status.must_equal 0
|
||||
out.stdout.must_match /^Pending: /
|
||||
out.stdout.must_include '5 examples, 0 failures, 1 pending'
|
||||
out.stdout.must_include "\n\e[32m[PASS] ssh-1 Allow only SSH Protocol 2\e[0m\n"
|
||||
out.stdout.must_include "\n\e[32m[PASS] tmp-1.0 Create /tmp directory\e[0m\n"
|
||||
out.stdout.must_include "\n\e[37m[SKIP] gordon-1.0 Verify the version number of Gordon (1 skipped)\e[0m\n"
|
||||
out.stdout.must_include "\nSummary: 4 successful 0 failures 1 skipped\n"
|
||||
end
|
||||
|
||||
it 'executes a minimum metadata-only profile' do
|
||||
out = inspec('exec ' + File.join(profile_path, 'simple-metadata'))
|
||||
out.stderr.must_equal ''
|
||||
out.exit_status.must_equal 0
|
||||
out.stdout.must_equal "
|
||||
Profile: yumyum profile
|
||||
Version: unknown
|
||||
|
||||
|
||||
Summary: 0 successful 0 failures 0 skipped
|
||||
"
|
||||
end
|
||||
|
||||
it 'executes a metadata-only profile' do
|
||||
out = inspec('exec ' + File.join(profile_path, 'complete-metadata'))
|
||||
out.stderr.must_equal ''
|
||||
out.exit_status.must_equal 0
|
||||
out.stdout.must_equal "
|
||||
Profile: title (name)
|
||||
Version: 1.2.3
|
||||
|
||||
|
||||
Summary: 0 successful 0 failures 0 skipped
|
||||
"
|
||||
end
|
||||
|
||||
it 'executes a specs-only profile' do
|
||||
out = inspec('exec ' + File.join(profile_path, 'spec_only'))
|
||||
out.stderr.must_equal ''
|
||||
out.exit_status.must_equal 1
|
||||
out.stdout.must_equal "
|
||||
\e[32m[PASS] working should eq \"working\"\e[0m
|
||||
\e[37m[SKIP] skippy This will be skipped intentionally.\e[0m
|
||||
\e[31m[FAIL] failing should eq \"as intended\"
|
||||
expected: \"as intended\"
|
||||
got: \"failing\"
|
||||
|
||||
(compared using ==)
|
||||
\e[0m
|
||||
|
||||
Summary: 1 successful 1 failures 1 skipped
|
||||
"
|
||||
end
|
||||
|
||||
it 'executes only specified controls' do
|
||||
out = inspec('exec ' + example_profile + ' --controls tmp-1.0')
|
||||
out.stderr.must_equal ''
|
||||
out.exit_status.must_equal 0
|
||||
out.stdout.must_include '1 example, 0 failures'
|
||||
end
|
||||
|
||||
it 'can execute the profile with the mini json formatter' do
|
||||
out = inspec('exec ' + example_profile + ' --format json-min')
|
||||
out.stderr.must_equal ''
|
||||
out.exit_status.must_equal 0
|
||||
JSON.load(out.stdout).must_be_kind_of Hash
|
||||
out.stdout.must_include "\nSummary: 1 successful 0 failures 0 skipped\n"
|
||||
end
|
||||
|
||||
it 'can execute a simple file with the default formatter' do
|
||||
out = inspec('exec ' + example_control)
|
||||
out.stderr.must_equal ''
|
||||
out.exit_status.must_equal 0
|
||||
out.stdout.must_include '2 examples, 0 failures'
|
||||
end
|
||||
|
||||
it 'can execute a simple file with the mini json formatter' do
|
||||
out = inspec('exec ' + example_control + ' --format json-min')
|
||||
out.stderr.must_equal ''
|
||||
out.exit_status.must_equal 0
|
||||
JSON.load(out.stdout).must_be_kind_of Hash
|
||||
end
|
||||
|
||||
describe 'execute a profile with mini json formatting' do
|
||||
let(:json) { JSON.load(inspec('exec ' + example_profile + ' --format json-min').stdout) }
|
||||
let(:controls) { json['controls'] }
|
||||
let(:ex1) { controls.find{|x| x['id'] == 'tmp-1.0'} }
|
||||
let(:ex2) { controls.find{|x| x['id'] =~ /generated/} }
|
||||
let(:ex3) { controls.find{|x| x['id'] == 'gordon-1.0'} }
|
||||
|
||||
it 'must have 5 examples' do
|
||||
json['controls'].length.must_equal 5
|
||||
end
|
||||
|
||||
it 'has an id' do
|
||||
controls.find { |ex| !ex.key? 'id' }.must_be :nil?
|
||||
end
|
||||
|
||||
it 'has a profile_id' do
|
||||
controls.find { |ex| !ex.key? 'profile_id' }.must_be :nil?
|
||||
end
|
||||
|
||||
it 'has a code_desc' do
|
||||
ex1['code_desc'].must_equal 'File /tmp should be directory'
|
||||
controls.find { |ex| !ex.key? 'code_desc' }.must_be :nil?
|
||||
end
|
||||
|
||||
it 'has a status' do
|
||||
ex1['status'].must_equal 'passed'
|
||||
ex3['status'].must_equal 'skipped'
|
||||
end
|
||||
|
||||
it 'has a skip_message' do
|
||||
ex1['skip_message'].must_be :nil?
|
||||
ex3['skip_message'].must_equal "Can't find file \"/tmp/gordon/config.yaml\""
|
||||
end
|
||||
out.stdout.must_include 'Summary: 2 successful 0 failures 0 skipped'
|
||||
end
|
||||
|
||||
describe 'with a profile that is not supported on this OS/platform' do
|
||||
|
|
11
test/unit/mock/profiles/spec_only/specfile.rb
Normal file
11
test/unit/mock/profiles/spec_only/specfile.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
describe 'working' do
|
||||
it { should eq 'working' }
|
||||
end
|
||||
|
||||
describe 'skippy' do
|
||||
skip 'This will be skipped intentionally.'
|
||||
end
|
||||
|
||||
describe 'failing' do
|
||||
it { should eq 'as intended' }
|
||||
end
|
Loading…
Add table
Reference in a new issue