Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate test labels for multi-test controls #879

Merged
merged 3 commits into from Aug 5, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
117 changes: 83 additions & 34 deletions lib/inspec/rspec_json_formatter.rb
Expand Up @@ -8,10 +8,17 @@
# Vanilla RSpec JSON formatter with a slight extension to show example IDs.
# TODO: Remove these lines when RSpec includes the ID natively
class InspecRspecVanilla < RSpec::Core::Formatters::JsonFormatter
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
RSpec::Core::Formatters.register self

private

# We are cheating and overriding a private method in RSpec's core JsonFormatter.
# This is to avoid having to repeat this id functionality in both dump_summary
# and dump_profile (both of which call format_example).
# See https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/formatters/json_formatter.rb
#
# rspec's example id here corresponds to an inspec test's control name -
# either explicitly specified or auto-generated by rspec itself.
def format_example(example)
res = super(example)
res[:id] = example.metadata[:id]
Expand All @@ -22,8 +29,11 @@ def format_example(example)
# Minimal JSON formatter for inspec. Only contains limited information about
# examples without any extras.
class InspecRspecMiniJson < RSpec::Core::Formatters::JsonFormatter
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
# Don't re-register all the call-backs over and over - we automatically
# inherit all callbacks registered by the parent class.
RSpec::Core::Formatters.register self, :dump_summary, :stop

# Called after stop has been called and the run is complete.
def dump_summary(summary)
@output_hash[:version] = Inspec::VERSION
@output_hash[:summary] = {
Expand All @@ -34,7 +44,12 @@ def dump_summary(summary)
}
end

# Called at the end of a complete RSpec run.
def stop(notification)
# This might be a bit confusing. The results are not actually organized
# by control. It is organized by test. So if a control has 3 tests, the
# output will have 3 control entries, each one with the same control id
# and different test results. An rspec example maps to an inspec test.
@output_hash[:controls] = notification.examples.map do |example|
format_example(example).tap do |hash|
e = example.exception
Expand Down Expand Up @@ -72,49 +87,59 @@ def format_example(example)
end

class InspecRspecJson < InspecRspecMiniJson
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
RSpec::Core::Formatters.register self, :start, :stop
attr_writer :backend

def initialize(*args)
super(*args)
@profiles = []
# Will be valid after "start" state is reached.
@profiles_info = nil
@backend = nil
end

# Called by the runner during example collection.
def add_profile(profile)
@profiles.push(profile)
end

# Called after all examples have been collected but before rspec
# test execution has begun.
def start(_notification)
# Note that the default profile may have no name - therefore
# the hash may have a valid nil => entry.
@profiles_info ||= Hash[@profiles.map { |x| profile_info(x) }]
end

def dump_one_example(example, control)
control[:results] ||= []
example.delete(:id)
example.delete(:profile_id)
control[:results].push(example)
end

def profile_info(profile)
info = profile.info.dup
[info[:name], info]
end

def dump_summary(summary)
super(summary)
def stop(notification)
super(notification)
examples = @output_hash.delete(:controls)
profiles = Hash[@profiles.map { |x| profile_info(x) }]
missing = []

examples.each do |example|
control = example2control(example, profiles)
control = example2control(example, @profiles_info)
next missing.push(example) if control.nil?
dump_one_example(example, control)
end

@output_hash[:profiles] = profiles
@output_hash[:profiles] = @profiles_info
@output_hash[:other_checks] = missing
end

private

def profile_info(profile)
info = profile.info.dup
[info[:name], info]
end

def example2control(example, profiles)
profile = profiles[example[:profile_id]]
return nil if profile.nil? || profile[:controls].nil?
Expand All @@ -130,7 +155,7 @@ def format_example(example)
end

class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
RSpec::Core::Formatters.register self, :close

STATUS_TYPES = {
'unknown' => -3,
Expand Down Expand Up @@ -169,6 +194,8 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
'empty' => ' ',
}.freeze

MULTI_TEST_CONTROL_SUMMARY_MAX_LEN = 60

def initialize(*args)
@colors = COLORS
@indicators = INDICATORS
Expand All @@ -181,10 +208,6 @@ def initialize(*args)
super(*args)
end

def start(_notification)
@profiles_info ||= Hash[@profiles.map { |x| profile_info(x) }]
end

def close(_notification)
flush_current_control
output.puts('') unless @current_control.nil?
Expand Down Expand Up @@ -236,24 +259,50 @@ def current_control_infos
[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
def current_control_title
title = @current_control[:title]
res = @current_control[:results]
if title
title
elsif res.length == 1
# If it's an anonymous control, just go with the only description
# available for the underlying test.
res[0][:code_desc].to_s
elsif res.length == 0
# Empty control block - if it's anonymous, there's nothing we can do.
# Is this case even possible?
'Empty anonymous control'
else
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider pulling the code inside this else out into a function with a signature like multi_test_summary(summary, results, fails, skips)

# Multiple tests - but no title. Do our best and generate some form of
# identifier or label or name.
title = (res.map { |r| r[:code_desc] }).join('; ')
max_len = MULTI_TEST_CONTROL_SUMMARY_MAX_LEN
title = title[0..(max_len-1)] + '...' if title.length > max_len
title
end
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
def current_control_summary(fails, skips)
title = current_control_title
res = @current_control[:results]
suffix =
if res.length == 1
# Single test - be nice and just print the exception message if the test
# failed. No need to say "1 failed".
fails.clear
skips.clear
res[0][:message].to_s
else
[
(fails.length > 0) ? "#{fails.length} failed" : nil,
(skips.length > 0) ? "#{skips.length} skipped" : nil,
].compact.join(' ')
end
if suffix == ''
title
else
title + ' (' + suffix + ')'
end
end

def format_line(fields)
Expand Down
7 changes: 4 additions & 3 deletions test/functional/inspec_exec_test.rb
Expand Up @@ -60,11 +60,12 @@

\e[32m ✔ working should eq \"working\"\e[0m
\e[37m ○ skippy This will be skipped intentionally.\e[0m
\e[31m ✖ failing should eq \"as intended\"
\e[31m ✖ failing should eq \"as intended\" (
expected: \"as intended\"
got: \"failing\"
\n (compared using ==)
\e[0m

(compared using ==)
)\e[0m

Summary: \e[32m1 successful\e[0m, \e[31m1 failures\e[0m, \e[37m1 skipped\e[0m
"
Expand Down