class Inspec::Runner


“‘
r.run
r.add_target(“url/to/some/profile”)
r.add_target(“/path/to/some/profile”)
r = Inspec::Runner.new()
“`
and then call the run method:
Users are expected to insantiate a runner, add targets to be run,
entry point to the application.
Inspec::Runner coordinates the running of tests and is the main

def add_resource(method_name, arg, opts, block)

def add_resource(method_name, arg, opts, block)
  case method_name
  when "describe"
    opts = { backend: @test_collector.backend }.merge opts
    @test_collector.example_group(*arg, opts, &block)
  when "expect"
    block.example_group
  when "describe.one"
    tests = arg.map do |x|
      @test_collector.example_group(x[1][0], block_source_info(x[2]), &x[2])
    end
    return nil if tests.empty?
    successful_tests = tests.find_all(&:run)
    # Return all tests if none succeeds; we will just report full failure
    return tests if successful_tests.empty?
    successful_tests
  else
    raise "A rule was registered with #{method_name.inspect}," \
          "which isn't understood and cannot be processed."
  end
end

def add_target(target, _opts = [])


@eturns [Inspec::ProfileContext]

@params _opts [Hash] Unused, but still here to avoid breaking kitchen-inspec
@params target [String] A path or URL to a profile or raw test.

the ProfileContext, the Profile, and Inspec::DSL
TODO: Deduplicate/clarify the loading code that exists in here,

responsible for actually running the tests.
registered with the @test_collector which is ultimately
query the profile for the full list of rules. Those rules are
Once the we've loaded all of the tests files in the profile, we

called using similar code in Inspec::DSL.
loaded on-demand when include_content or required_content are
If the profile depends on other profiles, those profiles will be

into the ProfileContext.
(libraries, tests, and inputs) from the Profile are loaded
target we generate a Profile and a ProfileContext. The content
A target is a path or URL that points to a profile. Using this

run when the user calls the run method.
add_target allows the user to add a target whose tests will be
def add_target(target, _opts = [])
  profile = Inspec::Profile.for_target(target,
                                       vendor_cache: @cache,
                                       backend: @backend,
                                       controls: @controls,
                                       tags: @tags,
                                       runner_conf: @conf)
  raise "Could not resolve #{target} to valid input." if profile.nil?
  @target_profiles << profile if supports_profile?(profile)
end

def all_rules

the same, we provide an #all_rules method here as well.
places we read it off of the profile context. To keep the API's
In some places we read the rules off of the runner, in other
def all_rules
  @rules
end

def attributes

def attributes
  Inspec.deprecate(:rename_attributes_to_inputs, "Don't call runner.attributes, call runner.inputs")
  inputs
end

def block_source_info(block)

def block_source_info(block)
  return {} if block.nil? || !block.respond_to?(:source_location)
  opts = {}
  file_path, line = block.source_location
  opts["file_path"] = file_path
  opts["line_number"] = line
  opts
end

def configure_transport

def configure_transport
  @backend = Inspec::Backend.create(@conf)
  @test_collector.backend = @backend
end

def eval_with_virtual_profile(command)

def eval_with_virtual_profile(command)
  require "inspec/fetcher/mock"
  add_target({ "inspec.yml" => "name: inspec-shell" })
  our_profile = @target_profiles.first
  ctx = our_profile.runner_context
  # Load local profile dependencies. This is used in inspec shell
  # to provide access to local profiles that add resources.
  @depends.each do |dep|
    # support for windows paths
    dep = dep.tr("\\", "/")
    Inspec::Profile.for_path(dep, { profile_context: ctx }).load_libraries
  end
  ctx.load(command)
end

def get_check_example(method_name, arg, block)

def get_check_example(method_name, arg, block)
  opts = block_source_info(block)
  return nil if arg.empty?
  resource = arg[0]
  # check to see if we are using a filtertable object
  resource = resource.resource if resource.is_a? FilterTable::Table
  if resource.respond_to?(:resource_skipped?) && resource.resource_skipped?
    return rspec_skipped_block(arg, opts, resource.resource_exception_message)
  end
  if resource.respond_to?(:resource_failed?) && resource.resource_failed?
    return rspec_failed_block(arg, opts, resource.resource_exception_message)
  end
  # If neither skipped nor failed then add the resource
  add_resource(method_name, arg, opts, block)
end

def initialize(conf = {})

def initialize(conf = {})
  @rules = []
  # If we were handed a Hash config (by audit cookbook or kitchen-inspec),
  # upgrade it to a proper config. This handles a lot of config finalization,
  # like reporter parsing.
  @conf = conf.is_a?(Hash) ? Inspec::Config.new(conf) : conf
  @conf[:logger] ||= Logger.new(nil)
  @target_profiles = []
  @controls = @conf[:controls] || []
  @tags = @conf[:tags] || []
  @depends = @conf[:depends] || []
  @create_lockfile = @conf[:create_lockfile]
  @cache = Inspec::Cache.new(@conf[:vendor_cache])
  @test_collector = @conf.delete(:test_collector) || begin
    RunnerRspec.new(@conf)
  end
  if @conf[:waiver_file]
    @conf[:waiver_file].each do |file|
      unless File.file?(file)
        raise Inspec::Exceptions::WaiversFileDoesNotExist, "Waiver file #{file} does not exist."
      end
    end
  end
  # About reading inputs:
  #   @conf gets passed around a lot, eventually to
  # Inspec::InputRegistry.register_external_inputs.
  #
  #   @conf may contain the key :attributes or :inputs, which is to be a Hash
  # of values passed in from the Runner API.
  # This is how kitchen-inspec and the audit_cookbook pass in inputs.
  #
  #   @conf may contain the key :attrs or :input_file, which is to be an Array
  # of file paths, each a YAML file. This how --input-file works.
  configure_transport
end

def load

def load
  all_controls = []
  @target_profiles.each do |profile|
    @test_collector.add_profile(profile)
    next unless profile.supports_platform?
    write_lockfile(profile) if @create_lockfile
    profile.locked_dependencies
    profile.load_gem_dependencies
    profile_context = profile.load_libraries
    profile_context.dependencies.list.values.each do |requirement|
      unless requirement.profile.supports_platform?
        Inspec::Log.warn "Skipping profile: '#{requirement.profile.name}'" \
         " on unsupported platform: '#{@backend.platform.name}/#{@backend.platform.release}'."
        next
      end
      @test_collector.add_profile(requirement.profile)
    end
    begin
      tests = profile.collect_tests
      all_controls += tests unless tests.nil?
    rescue Inspec::Exceptions::ProfileLoadFailed => e
      Inspec::Log.error "Failed to load profile #{profile.name}: #{e}"
      profile.set_status_message e.to_s
      next
    end
  end
  controls_count = 0
  control_checks_count_map = {}
  all_controls.each do |rule|
    unless rule.nil?
      register_rule(rule)
      total_checks = 0
      control_describe_checks = ::Inspec::Rule.prepare_checks(rule)
      examples = control_describe_checks.flat_map do |m, a, b|
        get_check_example(m, a, b)
      end.compact
      examples.map { |example| total_checks += example.examples.count }
      unless control_describe_checks.empty?
        # controls with empty tests are avoided
        # checks represent tests within control
        controls_count += 1 if control_checks_count_map[rule.to_s].nil?
        control_checks_count_map[rule.to_s] = control_checks_count_map[rule.to_s].to_i + total_checks
      end
    end
  end
  # this sets data via runner-rspec into base RSpec formatter object, which gets used up within streaming plugins
  @test_collector.set_controls_count(controls_count)
  @test_collector.set_control_checks_count_map(control_checks_count_map)
end

def register_rule(rule)

def register_rule(rule)
  Inspec::Log.debug "Registering rule #{rule}"
  @rules << rule
  checks = ::Inspec::Rule.prepare_checks(rule)
  examples = checks.flat_map do |m, a, b|
    get_check_example(m, a, b)
  end.compact
  examples.each { |e| @test_collector.add_test(e, rule) }
end

def register_rules(ctx)

def register_rules(ctx)
  new_tests = false
  ctx.rules.each do |rule_id, rule|
    next if block_given? && !(yield rule_id, rule)
    new_tests = true
    register_rule(rule)
  end
  new_tests
end

def render_output(run_data)

def render_output(run_data)
  return if @conf["reporter"].nil?
  @conf["reporter"].each do |reporter|
    result = Inspec::Reporters.render(reporter, run_data, @conf["enhanced_outcomes"])
    raise Inspec::ReporterError, "Error generating reporter '#{reporter[0]}'" if result == false
  end
end

def report

def report
  Inspec::Reporters.report(@conf["reporter"].first, @run_data)
end

def reset

def reset
  @test_collector.reset
  @target_profiles.each do |profile|
    profile.runner_context.rules = {}
  end
  @rules = []
end

def rspec_failed_block(arg, opts, message)

def rspec_failed_block(arg, opts, message)
  @test_collector.example_group(*arg, opts) do
    # Send custom `it` block to RSpec
    it "" do
      # Raising here to fail the test and get proper formatting
      raise Inspec::Exceptions::ResourceFailed, message
    end
  end
end

def rspec_skipped_block(arg, opts, message)

def rspec_skipped_block(arg, opts, message)
  @test_collector.example_group(*arg, opts) do
    # Send custom `it` block to RSpec
    it message
  end
end

def run(with = nil)

def run(with = nil)
  Inspec::Log.debug "Starting run with targets: #{@target_profiles.map(&:to_s)}"
  load
  run_tests(with)
end

def run_tests(with = nil)

def run_tests(with = nil)
  @run_data = @test_collector.run(with)
  # dont output anything if we want a report
  render_output(@run_data) unless @conf["report"]
  @test_collector.exit_code
end

def supports_profile?(profile)

def supports_profile?(profile)
  unless profile.supports_runtime?
    raise "This profile requires #{Inspec::Dist::PRODUCT_NAME} version "\
         "#{profile.metadata.inspec_requirement}. You are running "\
         "#{Inspec::Dist::PRODUCT_NAME} v#{Inspec::VERSION}.\n"
  end
  true
end

def tests

def tests
  @test_collector.tests
end

def write_lockfile(profile)

def write_lockfile(profile)
  return false unless profile.writable?
  if profile.lockfile_exists?
    Inspec::Log.debug "Using existing lockfile #{profile.lockfile_path}"
  else
    Inspec::Log.debug "Creating lockfile: #{profile.lockfile_path}"
    lockfile = profile.generate_lockfile
    File.write(profile.lockfile_path, lockfile.to_yaml)
  end
end