class Inspec::Reporters::CLI

Constants

COLORS

Most currently available Windows terminals have poor support for ANSI extended colors

INDICATORS

Most currently available Windows terminals have poor support for UTF-8 characters so use these boring indicators

MULTI_TEST_CONTROL_SUMMARY_MAX_LEN

Public Instance Methods

render() click to toggle source
# File lib/inspec/reporters/cli.rb, line 43
def render
  @src_extent_map = {}
  run_data[:profiles].each do |profile|
    if profile[:status] == "skipped"
      platform = run_data[:platform]
      output("Skipping profile: '#{profile[:name]}' on unsupported platform: '#{platform[:name]}/#{platform[:release]}'.")
      next
    end
    read_control_source(profile)
    @control_count = 0
    output("")
    print_profile_header(profile)
    print_standard_control_results(profile)
    print_anonymous_control_results(profile)
    if @control_count == 0
      output(format_message(
        indentation: 5,
        message: "No tests executed."
      ))
    end
  end

  output("")
  print_profile_summary
  print_tests_summary
end

Private Instance Methods

all_unique_controls() click to toggle source
# File lib/inspec/reporters/cli.rb, line 231
def all_unique_controls
  @unique_controls ||= begin # rubocop:disable Style/RedundantBegin
                         run_data[:profiles].flat_map do |profile|
                           profile[:controls]
                         end.uniq
                       end
end
anonymous_controls_from_profile(profile) click to toggle source
# File lib/inspec/reporters/cli.rb, line 339
def anonymous_controls_from_profile(profile)
  profile[:controls].select { |c| is_anonymous_control?(c) && !c[:results].nil? }
end
format_control_header(control) click to toggle source
# File lib/inspec/reporters/cli.rb, line 124
def format_control_header(control)
  impact = control.impact_string
  format_message(
    color: impact,
    indicator: impact,
    message: control.title_for_report
  )
end
format_control_source(control) click to toggle source
# File lib/inspec/reporters/cli.rb, line 133
def format_control_source(control)
  src = @control_source[control.id]
  message  = "Control Source from #{src[:path]}:#{src[:start]}..#{src[:end]}\n"
  message += src[:content]
  format_message(
    color: "skipped",
    indentation: 5,
    message: message
  )
end
format_message(message_info) click to toggle source
# File lib/inspec/reporters/cli.rb, line 211
def format_message(message_info)
  indicator = message_info[:indicator]
  color = message_info[:color]
  indentation = message_info.fetch(:indentation, 2)
  message = message_info[:message]

  message_to_format = ""
  message_to_format += "#{INDICATORS[indicator]}  " unless indicator.nil?
  message_to_format += message.to_s.lstrip.force_encoding(Encoding::UTF_8)

  format_with_color(color, indent_lines(message_to_format, indentation))
end
format_profile_name(profile) click to toggle source
# File lib/inspec/reporters/cli.rb, line 116
def format_profile_name(profile)
  if profile[:title].nil?
    (profile[:name] || "unknown").to_s
  else
    "#{profile[:title]} (#{profile[:name] || "unknown"})"
  end
end
format_result(control, result, type) click to toggle source
# File lib/inspec/reporters/cli.rb, line 189
def format_result(control, result, type)
  impact = control.impact_string_for_result(result)

  message = if result[:status] == "skipped"
              result[:skip_message]
            elsif type == :anonymous
              result[:expectation_message]
            else
              result[:code_desc]
            end

  # append any failure details to the message if they exist
  message += "\n#{result[:message]}" if result[:message]

  format_message(
    color: impact,
    indicator: impact,
    indentation: 5,
    message: message
  )
end
format_with_color(color_name, text) click to toggle source
# File lib/inspec/reporters/cli.rb, line 224
def format_with_color(color_name, text)
  return text if defined?(RSpec.configuration) && !RSpec.configuration.color
  return text unless COLORS.key?(color_name)

  "#{COLORS[color_name]}#{text}#{COLORS["reset"]}"
end
indent_lines(message, indentation) click to toggle source
# File lib/inspec/reporters/cli.rb, line 347
def indent_lines(message, indentation)
  message.lines.map { |line| " " * indentation + line }.join
end
is_anonymous_control?(control) click to toggle source
# File lib/inspec/reporters/cli.rb, line 343
def is_anonymous_control?(control)
  control[:id].start_with?("(generated from ")
end
print_anonymous_control_results(profile) click to toggle source
print_profile_header(profile) click to toggle source
print_profile_summary() click to toggle source
print_standard_control_results(profile) click to toggle source
print_tests_summary() click to toggle source
profile_summary() click to toggle source
# File lib/inspec/reporters/cli.rb, line 239
def profile_summary
  failed = 0
  skipped = 0
  passed = 0

  all_unique_controls.each do |control|
    next if control[:id].start_with? "(generated from "
    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,
    "failed" => failed,
    "skipped" => skipped,
    "passed" => passed,
  }
end
read_control_source(profile) click to toggle source
# File lib/inspec/reporters/cli.rb, line 144
def read_control_source(profile)
  return unless Inspec::Config.cached[:reporter_include_source]

  @control_source = {}
  src_extent_map = {}

  # First pass: build map of paths => ids => [start]
  all_unique_controls.each do |control|
    id = control[:id]
    path = control[:source_location][:ref]
    start = control[:source_location][:line]
    next if path.nil? || start.nil?

    src_extent_map[path] ||= []
    src_extent_map[path] << { start: start, id: id }
  end

  # Now sort the controls by their starting line in their control file
  src_extent_map.values.each do |extent_list|
    extent_list.sort! { |a, b| a[:start] <=> b[:start] }
  end

  # Third pass: Read in files and split into lines
  src_extent_map.keys.each do |path|
    control_file_lines = File.read(path).lines # TODO error handling
    last_line_in_file = control_file_lines.count
    extent_list = src_extent_map[path]
    extent_list.each_with_index do |extent, idx|
      if idx == extent_list.count - 1 # Last entry
        extent[:end] = last_line_in_file
      else
        extent[:end] = extent_list[idx + 1][:start] - 1
      end

      @control_source[extent[:id]] =
        {
          path: path,
          start: extent[:start],
          end: extent[:end],
          content: control_file_lines.slice(extent[:start] - 1, extent[:end] - extent[:start] + 1).join(""),
        }
    end
  end
end
standard_controls_from_profile(profile) click to toggle source
# File lib/inspec/reporters/cli.rb, line 335
def standard_controls_from_profile(profile)
  profile[:controls].reject { |c| is_anonymous_control?(c) }
end
tests_summary() click to toggle source
# File lib/inspec/reporters/cli.rb, line 267
def tests_summary
  total = 0
  failed = 0
  skipped = 0
  passed = 0

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

    control[:results].each do |result|
      if result[:status] == "failed"
        failed += 1
      elsif result[:status] == "skipped"
        skipped += 1
      else
        passed += 1
      end
    end
  end

  {
    "total" => total,
    "failed" => failed,
    "skipped" => skipped,
    "passed" => passed,
  }
end