class Inspec::Formatters::Base

Attributes

backend[RW]
run_data[RW]

Public Class Methods

new(output) click to toggle source
Calls superclass method
# File lib/inspec/formatters/base.rb, line 10
def initialize(output)
  super(output)

  @run_data = {}
  @profiles = []
  @profiles_info = nil
  @backend = nil
end

Public Instance Methods

add_profile(profile) click to toggle source

Add the current profile to the list of executed profiles. Called by the runner during example collection.

# File lib/inspec/formatters/base.rb, line 78
def add_profile(profile)
  @profiles.push(profile)
end
dump_summary(summary) click to toggle source

RSpec Override: dump_summary

Supply run summary data, such as the InSpec version and the total duration.

# File lib/inspec/formatters/base.rb, line 22
def dump_summary(summary)
  run_data[:version] = Inspec::VERSION
  run_data[:statistics] = {
    duration: summary.duration,
    controls: statistics,
  }
end
results() click to toggle source

Return all the collected output to the caller

# File lib/inspec/formatters/base.rb, line 83
def results
  run_data
end
stop(notification) click to toggle source

RSpec Override: stop

Called at the end of a complete RSpec run. We use this to map tests to controls and flesh out the rest of the run_data hash to include details about the run, the platform, etc.

# File lib/inspec/formatters/base.rb, line 35
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.
  run_data[:controls] = notification.examples.map do |example|
    format_example(example).tap do |hash|
      e = example.exception
      next unless e

      if example.metadata[:sensitive]
        hash[:message] = "*** sensitive output suppressed ***"
      else
        hash[:message] = exception_message(e)
      end

      next if e.is_a? RSpec::Expectations::ExpectationNotMetError

      hash[:exception] = e.class.name
      hash[:backtrace] = e.backtrace
    end
  end

  # include any tests that were run that were not part of a control
  run_data[:other_checks] = examples_without_controls
  examples_with_controls.each do |example|
    control = example2control(example)
    move_example_into_control(example, control)
  end

  # flesh out the profiles key with additional profile information
  run_data[:profiles] = profiles_info

  # add the platform information for this particular target
  run_data[:platform] = {
    name: platform(:name),
    release: platform(:release),
    target: backend_target,
  }
end

Private Instance Methods

all_unique_controls() click to toggle source
# File lib/inspec/formatters/base.rb, line 89
def all_unique_controls
  unique_controls = Set.new
  run_data[:profiles].each do |profile|
    profile[:controls].map { |control| unique_controls.add(control) }
  end

  unique_controls
end
backend_target() click to toggle source
# File lib/inspec/formatters/base.rb, line 216
def backend_target
  return nil if @backend.nil?

  connection = @backend.backend
  connection.respond_to?(:uri) ? connection.uri : nil
end
example2control(example) click to toggle source
# File lib/inspec/formatters/base.rb, line 239
def example2control(example)
  profile = profile_from_example(example)
  return nil unless profile&.[](:controls)

  profile[:controls].find { |x| x[:id] == example[:id] }
end
examples() click to toggle source
# File lib/inspec/formatters/base.rb, line 223
def examples
  run_data[:controls]
end
examples_with_controls() click to toggle source
# File lib/inspec/formatters/base.rb, line 231
def examples_with_controls
  examples.find_all { |example| !example2control(example).nil? }
end
examples_without_controls() click to toggle source
# File lib/inspec/formatters/base.rb, line 227
def examples_without_controls
  examples.find_all { |example| example2control(example).nil? }
end
exception_message(exception) click to toggle source
# File lib/inspec/formatters/base.rb, line 131
def exception_message(exception)
  if exception.is_a?(RSpec::Core::MultipleExceptionError)
    exception.all_exceptions.map(&:message).uniq.join("\n\n")
  else
    exception.message
  end
end
find_resource_params(example) click to toggle source
# File lib/inspec/formatters/base.rb, line 185
def find_resource_params(example)
  if example.class.ancestors.include?(Inspec::Resource)
    example.instance_variable_get(:@resource_params)
  else
    []
  end
end
format_example(example) click to toggle source

RSpec Override: format_example

Called after test execution, this allows us to populate our own hash with data for this test that is necessary for the rest of our reports.

# File lib/inspec/formatters/base.rb, line 143
def format_example(example) # rubocop:disable Metrics/AbcSize
  if !example.metadata[:description_args].empty? && example.metadata[:skip]
    # For skipped profiles, rspec returns in full_description the skip_message as well. We don't want
    # to mix the two, so we pick the full_description from the example.metadata[:example_group] hash.
    code_description = example.metadata[:example_group][:description]
  else
    code_description = example.metadata[:full_description]
  end

  res = {
    id: example.metadata[:id],
    profile_id: example.metadata[:profile_id],
    status: example.execution_result.status.to_s,
    code_desc: code_description,
    run_time: example.execution_result.run_time,
    start_time: example.execution_result.started_at.to_datetime.rfc3339.to_s,
    resource_title: example.metadata[:described_class] || example.metadata[:example_group][:description],
    expectation_message: format_expectation_message(example),
    waiver_data: example.metadata[:waiver_data],
    # This enforces the resource name as expected based off of the class
    # name. However, if we wanted the `name` attribute against the class
    # to be canonical for this case (consider edge cases!) we would use
    # example.metadata[:described_class].instance_variable_get(:@__resource_name__)&.to_s
    resource_class: example.metadata[:described_class].class.superclass.name,
    # This is a raw grep of the text passed to the resource in any format,
    # and is used to enforce near-uniqueness against the resource.
    resource_params: find_resource_params(example.metadata[:described_class]),
  }

  unless (pid = example.metadata[:profile_id]).nil?
    res[:profile_id] = pid
  end

  if res[:status] == "pending"
    res[:status] = "skipped"
    res[:skip_message] = example.metadata[:description]
    res[:resource] = example.metadata[:described_class].to_s
  end

  res
end
format_expectation_message(example) click to toggle source
# File lib/inspec/formatters/base.rb, line 193
def format_expectation_message(example)
  if (example.metadata[:example_group][:description_args].first == example.metadata[:example_group][:described_class]) ||
      example.metadata[:example_group][:described_class].nil?
    example.metadata[:description]
  else
    "#{example.metadata[:example_group][:description]} #{example.metadata[:description]}"
  end
end
move_example_into_control(example, control) click to toggle source
# File lib/inspec/formatters/base.rb, line 273
def move_example_into_control(example, control)
  control[:results] ||= []
  example.delete(:id)
  example.delete(:profile_id)
  control[:results].push(example)

  # Waiver data, if available, is internally stored on a per-result
  # (that is, per-describe-block) basis, because that is the only granularity
  # available to us in the RSpec report data structure which we use as a vehicle.
  control[:waiver_data] ||= example[:waiver_data] || {}
end
platform(field) click to toggle source

This formatter runs for all reports and we cannot error on missing fields. Return nil if not found or Train error. If needed, we will raise an error inside the proper report.

# File lib/inspec/formatters/base.rb, line 205
def platform(field)
  return nil if @backend.nil?

  begin
    @backend.platform[field]
  rescue Train::Error => e
    Inspec::Log.warn(e.message)
    nil
  end
end
profile_contains_example?(profile, example) click to toggle source
# File lib/inspec/formatters/base.rb, line 250
def profile_contains_example?(profile, example)
  profile_name = profile[:name]
  example_profile_id = example[:profile_id]

  # if either the profile name is nil or the profile in the given example
  # is nil, assume the profile doesn't contain the example and default
  # to creating a new profile. Otherwise, for profiles that have no
  # metadata, this may incorrectly match a profile that does not contain
  # this example, leading to Ruby exceptions.
  return false if profile_name.nil? || example_profile_id.nil?

  # The correct profile is one where the name of the profile, and the profile
  # name in the example match. Additionally, the list of controls in the
  # profile must contain the example in question (which we match by ID).
  #
  # While the profile name match is usually good enough, we must also match by
  # the control ID in the case where an InSpec runner has multiple profiles of
  # the same name (i.e. when Test Kitchen is running concurrently using a
  # single test suite that uses the Flat source reader, in which case InSpec
  # creates a fake profile with a name like "tests from /path/to/tests")
  profile_name == example_profile_id && profile[:controls].any? { |control| control[:id] == example[:id] }
end
profile_from_example(example) click to toggle source
# File lib/inspec/formatters/base.rb, line 246
def profile_from_example(example)
  profiles_info.find { |p| profile_contains_example?(p, example) }
end
profiles_info() click to toggle source
# File lib/inspec/formatters/base.rb, line 235
def profiles_info
  @profiles_info ||= @profiles.map(&:info!).map(&:dup)
end
statistics() click to toggle source
# File lib/inspec/formatters/base.rb, line 98
def statistics
  failed = 0
  skipped = 0
  passed = 0

  all_unique_controls.each do |control|
    next unless control[:results]

    if control[:results].any? { |r| r[:status] == "failed" }
      failed += 1
    elsif control[:results].any? { |r| r[:status] == "skipped" }
      skipped += 1
    else
      passed += 1
    end
  end

  total = failed + passed + skipped

  {
    total: total,
    passed: {
      total: passed,
    },
    skipped: {
      total: skipped,
    },
    failed: {
      total: failed,
    },
  }
end