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 43 def instance @instance ||= new end
new()
click to toggle source
# File lib/rspec-puppet/coverage.rb, line 48 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 115 def add(resource) return unless !exists?(resource) && !filtered?(resource) @collection[resource.to_s] = ResourceWrapper.new(resource) end
add_filter(type, title)
click to toggle source
# File lib/rspec-puppet/coverage.rb, line 121 def add_filter(type, title) type = capitalize_name(type) title = capitalize_name(title) if type == 'Class' @filters << "#{type}[#{title}]" end
add_filter_regex(type, pattern)
click to toggle source
# File lib/rspec-puppet/coverage.rb, line 129 def add_filter_regex(type, pattern) raise ArgumentError, '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 /(?<!\\)(\\\\)*(?:\\[zZ]|\$)\z/.match?(src) 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 158 def add_from_catalog(catalog, test_module) coverable_resources = catalog.to_a.reject do |resource| !test_module.nil? && filter_resource?(resource, test_module) end coverable_resources.each do |resource| add(resource) end end
cover!(resource)
click to toggle source
# File lib/rspec-puppet/coverage.rb, line 174 def cover!(resource) return unless !filtered?(resource) && (wrapper = find(resource)) wrapper.touch! end
coverage_test(coverage_desired, report)
click to toggle source
# File lib/rspec-puppet/coverage.rb, line 212 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 167 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 98 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 106 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 90 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 75 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 67 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 195 def parallel_tests? !!ENV['TEST_ENV_NUMBER'] end
report!(coverage_desired = nil)
click to toggle source
# File lib/rspec-puppet/coverage.rb, line 180 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 243 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.positive? ? ((report[:touched].to_f / report[:total]) * 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]).positive? 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 199 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 54 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 327 def capitalize_name(name) name.split('::').map(&:capitalize).join('::') end
exists?(resource)
click to toggle source
# File lib/rspec-puppet/coverage.rb, line 323 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 291 def filter_resource?(resource, test_module) return true if filtered?(resource) if resource.type == 'Class' module_name = resource.title.split('::').first.downcase return true if module_name != test_module end if resource.file paths = module_paths(test_module) return true unless paths.any? { |path| resource.file.include?(path) } end false end
find(resource)
click to toggle source
# File lib/rspec-puppet/coverage.rb, line 319 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 310 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