Merge pull request #879 from chef/ksubrama/json_fix

Generate test labels for multi-test controls
This commit is contained in:
Kartik Null Cating-Subramanian 2016-08-05 10:11:15 -04:00 committed by GitHub
commit 31a7d58473
2 changed files with 87 additions and 37 deletions

View file

@ -8,10 +8,17 @@ require 'rspec/core/formatters/json_formatter'
# Vanilla RSpec JSON formatter with a slight extension to show example IDs. # Vanilla RSpec JSON formatter with a slight extension to show example IDs.
# TODO: Remove these lines when RSpec includes the ID natively # TODO: Remove these lines when RSpec includes the ID natively
class InspecRspecVanilla < RSpec::Core::Formatters::JsonFormatter class InspecRspecVanilla < RSpec::Core::Formatters::JsonFormatter
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close RSpec::Core::Formatters.register self
private 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) def format_example(example)
res = super(example) res = super(example)
res[:id] = example.metadata[:id] res[:id] = example.metadata[:id]
@ -22,8 +29,11 @@ end
# Minimal JSON formatter for inspec. Only contains limited information about # Minimal JSON formatter for inspec. Only contains limited information about
# examples without any extras. # examples without any extras.
class InspecRspecMiniJson < RSpec::Core::Formatters::JsonFormatter 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) def dump_summary(summary)
@output_hash[:version] = Inspec::VERSION @output_hash[:version] = Inspec::VERSION
@output_hash[:summary] = { @output_hash[:summary] = {
@ -34,7 +44,12 @@ class InspecRspecMiniJson < RSpec::Core::Formatters::JsonFormatter
} }
end end
# Called at the end of a complete RSpec run.
def stop(notification) 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| @output_hash[:controls] = notification.examples.map do |example|
format_example(example).tap do |hash| format_example(example).tap do |hash|
e = example.exception e = example.exception
@ -72,19 +87,30 @@ class InspecRspecMiniJson < RSpec::Core::Formatters::JsonFormatter
end end
class InspecRspecJson < InspecRspecMiniJson 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 attr_writer :backend
def initialize(*args) def initialize(*args)
super(*args) super(*args)
@profiles = [] @profiles = []
# Will be valid after "start" state is reached.
@profiles_info = nil
@backend = nil @backend = nil
end end
# Called by the runner during example collection.
def add_profile(profile) def add_profile(profile)
@profiles.push(profile) @profiles.push(profile)
end 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) def dump_one_example(example, control)
control[:results] ||= [] control[:results] ||= []
example.delete(:id) example.delete(:id)
@ -92,29 +118,28 @@ class InspecRspecJson < InspecRspecMiniJson
control[:results].push(example) control[:results].push(example)
end end
def profile_info(profile) def stop(notification)
info = profile.info.dup super(notification)
[info[:name], info]
end
def dump_summary(summary)
super(summary)
examples = @output_hash.delete(:controls) examples = @output_hash.delete(:controls)
profiles = Hash[@profiles.map { |x| profile_info(x) }]
missing = [] missing = []
examples.each do |example| examples.each do |example|
control = example2control(example, profiles) control = example2control(example, @profiles_info)
next missing.push(example) if control.nil? next missing.push(example) if control.nil?
dump_one_example(example, control) dump_one_example(example, control)
end end
@output_hash[:profiles] = profiles @output_hash[:profiles] = @profiles_info
@output_hash[:other_checks] = missing @output_hash[:other_checks] = missing
end end
private private
def profile_info(profile)
info = profile.info.dup
[info[:name], info]
end
def example2control(example, profiles) def example2control(example, profiles)
profile = profiles[example[:profile_id]] profile = profiles[example[:profile_id]]
return nil if profile.nil? || profile[:controls].nil? return nil if profile.nil? || profile[:controls].nil?
@ -130,7 +155,7 @@ class InspecRspecJson < InspecRspecMiniJson
end end
class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength 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 = { STATUS_TYPES = {
'unknown' => -3, 'unknown' => -3,
@ -169,6 +194,8 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
'empty' => ' ', 'empty' => ' ',
}.freeze }.freeze
MULTI_TEST_CONTROL_SUMMARY_MAX_LEN = 60
def initialize(*args) def initialize(*args)
@colors = COLORS @colors = COLORS
@indicators = INDICATORS @indicators = INDICATORS
@ -181,10 +208,6 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
super(*args) super(*args)
end end
def start(_notification)
@profiles_info ||= Hash[@profiles.map { |x| profile_info(x) }]
end
def close(_notification) def close(_notification)
flush_current_control flush_current_control
output.puts('') unless @current_control.nil? output.puts('') unless @current_control.nil?
@ -236,24 +259,50 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
[fails, skips, STATUS_TYPES.key(summary_status)] [fails, skips, STATUS_TYPES.key(summary_status)]
end end
def current_control_summary(fails, skips) def current_control_title
sum_info = [ title = @current_control[:title]
(fails.length > 0) ? "#{fails.length} failed" : nil, res = @current_control[:results]
(skips.length > 0) ? "#{skips.length} skipped" : nil, if title
].compact title
elsif res.length == 1
summary = @current_control[:title] # If it's an anonymous control, just go with the only description
unless summary.nil? # available for the underlying test.
return summary + ' (' + sum_info.join(' ') + ')' unless sum_info.empty? res[0][:code_desc].to_s
return summary 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
# 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
end
return sum_info.join(' ') if @current_control[:results].length != 1 def current_control_summary(fails, skips)
title = current_control_title
fails.clear res = @current_control[:results]
skips.clear suffix =
c = @current_control[:results][0] if res.length == 1
c[:code_desc].to_s + c[:message].to_s # 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 end
def format_line(fields) def format_line(fields)

View file

@ -60,11 +60,12 @@ Target: local://
\e[32m working should eq \"working\"\e[0m \e[32m working should eq \"working\"\e[0m
\e[37m skippy This will be skipped intentionally.\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\" expected: \"as intended\"
got: \"failing\" 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 Summary: \e[32m1 successful\e[0m, \e[31m1 failures\e[0m, \e[37m1 skipped\e[0m
" "