Merge pull request #790 from chef/dr/default-formatter

introduce cli report formatter
This commit is contained in:
Christoph Hartmann 2016-06-15 17:36:21 +02:00 committed by GitHub
commit 7be522a9e1
6 changed files with 326 additions and 65 deletions

View file

@ -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

View file

@ -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']

View file

@ -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,

View 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

View file

@ -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

View 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