class RSpec::Puppet::Coverage

Attributes

instance[W]
filters[RW]
filters_regex[RW]

Public Class Methods

instance() click to toggle source
# File lib/rspec-puppet/coverage.rb, line 42
def instance
  @instance ||= new
end
new() click to toggle source
# File lib/rspec-puppet/coverage.rb, line 47
def initialize
  @collection = {}
  @filters = ['Stage[main]', 'Class[Settings]', 'Class[main]', 'Node[default]']
  @filters_regex = []
end

Public Instance Methods

add(resource) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 114
def add(resource)
  if !exists?(resource) && !filtered?(resource)
    @collection[resource.to_s] = ResourceWrapper.new(resource)
  end
end
add_filter(type, title) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 120
def add_filter(type, title)
  type = capitalize_name(type)

  if type == 'Class'
    title = capitalize_name(title)
  end

  @filters << "#{type}[#{title}]"
end
add_filter_regex(type, pattern) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 130
def add_filter_regex(type, pattern)
  raise ArgumentError.new('pattern argument must be a Regexp') unless pattern.is_a?(Regexp)

  type = capitalize_name(type)

  # avoid recompiling the regular expression during processing
  src = pattern.source

  # switch from anchors to wildcards since it is embedded into a larger pattern
  src = if src.start_with?('\\A', '^')
          src.gsub(/\A(?:\\A|\^)/, '')
        else
          # no anchor at the start
          ".*#{src}"
        end

  # match an even number of backslashes before the anchor - this indicates that the anchor was not escaped
  # note the necessity for the negative lookbehind `(?<!)` to assert that there is no backslash before this
  src = if src.match(/(?<!\\)(\\\\)*(?:\\[zZ]|\$)\z/)
          src.gsub(/(?:\\[zZ]|\$)\z/, '')
        else
          # no anchor at the end
          "#{src}.*"
        end

  @filters_regex << /\A#{Regexp.escape(type)}\[#{src}\]\z/
end
add_from_catalog(catalog, test_module) click to toggle source

add all resources from catalog declared in module test_module

# File lib/rspec-puppet/coverage.rb, line 159
def add_from_catalog(catalog, test_module)
  coverable_resources = catalog.to_a.reject { |resource| !test_module.nil? && filter_resource?(resource, test_module) }
  coverable_resources.each do |resource|
    add(resource)
  end
end
cover!(resource) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 173
def cover!(resource)
  if !filtered?(resource) && (wrapper = find(resource))
    wrapper.touch!
  end
end
coverage_test(coverage_desired, report) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 211
def coverage_test(coverage_desired, report)
  coverage_actual = report[:coverage]
  coverage_desired ||= 0

  if coverage_desired.is_a?(Numeric) && coverage_desired.to_f <= 100.00 && coverage_desired.to_f >= 0.0
    coverage_test = RSpec.describe("Code coverage")
    coverage_results = coverage_test.example("must cover at least #{coverage_desired}% of resources") do
      expect( coverage_actual.to_f ).to be >= coverage_desired.to_f
    end
    coverage_test.run(RSpec.configuration.reporter)

    status = if coverage_results.execution_result.respond_to?(:status)
               coverage_results.execution_result.status
             else
               coverage_results.execution_result[:status]
             end

    if status == :failed
      RSpec.world.non_example_failure = true
      RSpec.world.wants_to_quit = true
    end

    # This is not available on RSpec 2.x
    if coverage_results.execution_result.respond_to?(:pending_message)
      coverage_results.execution_result.pending_message = report[:text]
    end
  else
    puts "The desired coverage must be 0 <= x <= 100, not '#{coverage_desired.inspect}'"
  end
end
filtered?(resource) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 166
def filtered?(resource)
  return true if filters.include?(resource.to_s)
  return true if filters_regex.any? { |f| resource.to_s =~ f }

  false
end
load_filters(path) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 97
def load_filters(path)
  saved_filters = JSON.parse(File.read(path))
  saved_filters.each do |resource|
    @filters << resource
    @collection.delete(resource) if @collection.key?(resource)
  end
end
load_filters_regex(path) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 105
def load_filters_regex(path)
  saved_regex_filters = JSON.parse(File.read(path))
  saved_regex_filters.each do |pattern|
    regex = Regexp.new(pattern)
    @filters_regex << regex
    @collection.delete_if { |resource, _| resource =~ regex }
  end
end
load_results(path) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 89
def load_results(path)
  saved_results = JSON.parse(File.read(path))
  saved_results.each do |resource, data|
    add(resource)
    cover!(resource) if data['touched']
  end
end
merge_filters() click to toggle source
# File lib/rspec-puppet/coverage.rb, line 74
def merge_filters
  pattern = File.join(Dir.tmpdir, "rspec-puppet-filter-#{Digest::MD5.hexdigest(Dir.pwd)}-*")
  regex_filter_pattern = File.join(Dir.tmpdir, "rspec-puppet-filter_regex-#{Digest::MD5.hexdigest(Dir.pwd)}-*")

  Dir[pattern].each do |result_file|
    load_filters(result_file)
    FileUtils.rm(result_file)
  end

  Dir[regex_filter_pattern].each do |result_file|
    load_filters_regex(result_file)
    FileUtils.rm(result_file)
  end
end
merge_results() click to toggle source
# File lib/rspec-puppet/coverage.rb, line 66
def merge_results
  pattern = File.join(Dir.tmpdir, "rspec-puppet-coverage-#{Digest::MD5.hexdigest(Dir.pwd)}-*")
  Dir[pattern].each do |result_file|
    load_results(result_file)
    FileUtils.rm(result_file)
  end
end
parallel_tests?() click to toggle source
# File lib/rspec-puppet/coverage.rb, line 194
def parallel_tests?
  !!ENV['TEST_ENV_NUMBER']
end
report!(coverage_desired = nil) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 179
def report!(coverage_desired = nil)
  if parallel_tests?
    require 'parallel_tests'

    if ParallelTests.first_process?
      ParallelTests.wait_for_other_processes_to_finish
      run_report(coverage_desired)
    else
      save_results
    end
  else
    run_report(coverage_desired)
  end
end
results() click to toggle source
# File lib/rspec-puppet/coverage.rb, line 242
def results
  report = {}

  @collection.delete_if { |name, _| filtered?(name) }

  report[:total] = @collection.size
  report[:touched] = @collection.count { |_, resource| resource.touched? }
  report[:untouched] = report[:total] - report[:touched]

  coverage = report[:total].to_f > 0 ? ((report[:touched].to_f / report[:total].to_f) * 100) : 100.0
  report[:coverage] = "%5.2f" % coverage

  report[:resources] = Hash[*@collection.map do |name, wrapper|
    [name, wrapper.to_hash]
  end.flatten]

  text = [
    "Total resources:   #{report[:total]}",
    "Touched resources: #{report[:touched]}",
    "Resource coverage: #{report[:coverage]}%",
  ]

  if report[:untouched] > 0
    text += ['', 'Untouched resources:']
    untouched_resources = report[:resources].reject { |_, r| r[:touched] }
    text += untouched_resources.map { |name, _| "  #{name}" }.sort
  end
  report[:text] = text.join("\n")

  report
end
run_report(coverage_desired = nil) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 198
def run_report(coverage_desired = nil)
  if parallel_tests?
    merge_filters
    merge_results
  end

  report = results

  coverage_test(coverage_desired, report)

  puts "\n\nCoverage Report:\n\n#{report[:text]}"
end
save_results() click to toggle source
# File lib/rspec-puppet/coverage.rb, line 53
def save_results
  slug = "#{Digest::MD5.hexdigest(Dir.pwd)}-#{Process.pid}"
  File.open(File.join(Dir.tmpdir, "rspec-puppet-filter-#{slug}"), 'w+') do |f|
    f.puts @filters.to_json
  end
  File.open(File.join(Dir.tmpdir, "rspec-puppet-filter_regex-#{slug}"), 'w+') do |f|
    f.puts @filters_regex.to_json
  end
  File.open(File.join(Dir.tmpdir, "rspec-puppet-coverage-#{slug}"), 'w+') do |f|
    f.puts @collection.to_json
  end
end

Private Instance Methods

capitalize_name(name) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 332
def capitalize_name(name)
  name.split('::').map { |subtitle| subtitle.capitalize }.join('::')
end
exists?(resource) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 328
def exists?(resource)
  !find(resource).nil?
end
filter_resource?(resource, test_module) click to toggle source

Should this resource be excluded from coverage reports?

The resource is not included in coverage reports if any of the conditions hold:

* The resource has been explicitly filtered out.
  * Examples: autogenerated resources such as 'Stage[main]'
* The resource is a class but does not belong to the module under test.
  * Examples: Class dependencies included from a fixture module
* The resource was declared in a file outside of the test module or site.pp
  * Examples: Resources declared in a dependency of this module.

@param resource [Puppet::Resource] The resource that may be filtered @param test_module [String] The name of the module under test @return [true, false]

# File lib/rspec-puppet/coverage.rb, line 290
def filter_resource?(resource, test_module)
  if filtered?(resource)
    return true
  end

  if resource.type == 'Class'
    module_name = resource.title.split('::').first.downcase
    if module_name != test_module
      return true
    end
  end

  if resource.file
    paths = module_paths(test_module)
    unless paths.any? { |path| resource.file.include?(path) }
      return true
    end
  end

  return false
end
find(resource) click to toggle source
# File lib/rspec-puppet/coverage.rb, line 324
def find(resource)
  @collection[resource.to_s]
end
module_paths(test_module) click to toggle source

Find all paths that may contain testable resources for a module.

@return [Array<String>]

# File lib/rspec-puppet/coverage.rb, line 315
def module_paths(test_module)
  adapter = RSpec.configuration.adapter
  paths = adapter.modulepath.map do |dir|
    File.join(dir, test_module, 'manifests')
  end
  paths << adapter.manifest if adapter.manifest
  paths
end