# encoding: utf-8 # author: Dominik Richter # author: Christoph Hartmann require 'rspec/core' require 'rspec/its' require 'inspec/formatters' # There be dragons!! Or borgs, or something... # This file and all its contents cannot be unit-tested. both test-suits # collide and disable all unit tests that have been added. module Inspec class RunnerRspec def initialize(conf) @conf = conf @formatter = nil reset end # Create a new RSpec example group from arguments and block. # # @param [Type] *args list of arguments for this example # @param [Type] &block the block associated with this example group # @return [RSpecExampleGroup] def example_group(*args, &block) RSpec::Core::ExampleGroup.describe(*args, &block) end # Add a full profile to the runner. Only pulls in metadata # # @param [Inspec::Profile] profile # @return [nil] def add_profile(profile) RSpec.configuration.formatters .find_all { |c| c.is_a?(Inspec::Formatters::Base) } .each do |fmt| fmt.add_profile(profile) end end # Configure the backend of the runner. # # @param [Inspec::Backend] backend # @return [nil] def backend=(backend) RSpec.configuration.formatters .find_all { |c| c.is_a?(Inspec::Formatters::Base) } .each do |fmt| fmt.backend = backend end end # Add an example group to the list of registered tests. # # @param [RSpecExampleGroup] example test # @param [String] rule_id the ID associated with this check # @return [nil] def add_test(example, rule) set_rspec_ids(example, rule) @tests.example_groups.push(example) end # Retrieve the list of tests that have been added. # # @return [Array] full list of tests def tests @tests.ordered_example_groups end # Run all registered tests with an optional test runner. # # @param [RSpecRunner] with is an optional RSpecRunner # @return [int] 0 if all went well; otherwise nonzero def run(with = nil) with ||= RSpec::Core::Runner.new(nil) @rspec_exit_code = with.run_specs(tests) @formatter.results end # Return a proper exit code to the runner # # @return [int] exit code def exit_code return @rspec_exit_code if @formatter.results.empty? stats = @formatter.results[:statistics][:controls] if stats[:failed][:total] == 0 && stats[:skipped][:total] == 0 0 elsif stats[:failed][:total] > 0 100 elsif stats[:skipped][:total] > 0 101 else @rspec_exit_code end end # Empty the list of registered tests. # # @return [nil] def reset @tests = RSpec::Core::World.new # resets "pending examples" in reporter RSpec.configuration.reset configure_output end private # Set optional formatters and output # # def set_optional_formatters return if @conf['reporter'].nil? if @conf['reporter'].key?('json-rspec') # We cannot pass in a nil output path. Rspec only accepts a valid string or a IO object. if @conf['reporter']['json-rspec']&.[]('file').nil? RSpec.configuration.add_formatter(Inspec::Formatters::RspecJson) else RSpec.configuration.add_formatter(Inspec::Formatters::RspecJson, @conf['reporter']['json-rspec']['file']) end @conf['reporter'].delete('json-rspec') end formats = @conf['reporter'].select { |k, _v| %w{documentation progress html}.include?(k) } formats.each do |k, v| # We cannot pass in a nil output path. Rspec only accepts a valid string or a IO object. if v&.[]('file').nil? RSpec.configuration.add_formatter(k.to_sym) else RSpec.configuration.add_formatter(k.to_sym, v['file']) end @conf['reporter'].delete(k) end end # Configure the output formatter and stream to be used with RSpec. # # @return [nil] def configure_output RSpec.configuration.output_stream = $stdout @formatter = RSpec.configuration.add_formatter(Inspec::Formatters::Base) RSpec.configuration.add_formatter(Inspec::Formatters::ShowProgress, $stderr) if @conf[:show_progress] set_optional_formatters RSpec.configuration.color = @conf['color'] end # Make sure that all RSpec example groups use the provided ID. # At the time of creation, we didn't yet have full ID support in RSpec, # which is why they were added to metadata directly. This is evaluated # by the InSpec adjusted json formatter (rspec_json_formatter). # # @param [RSpecExampleGroup] example object which contains a check # @return [Type] description of returned object def set_rspec_ids(example, rule) assign_rspec_ids(example.metadata, rule) example.filtered_examples.each do |e| assign_rspec_ids(e.metadata, rule) end example.children.each do |child| set_rspec_ids(child, rule) end end def assign_rspec_ids(metadata, rule) metadata[:id] = ::Inspec::Rule.rule_id(rule) metadata[:profile_id] = ::Inspec::Rule.profile_id(rule) metadata[:impact] = rule.impact metadata[:title] = rule.title metadata[:desc] = rule.desc metadata[:code] = rule.instance_variable_get(:@__code) metadata[:source_location] = rule.instance_variable_get(:@__source_location) end end end