lib/chefspec/matchers/resource_matcher.rb
require "rspec/matchers/expecteds_for_multiple_diffs" require "rspec/expectations/fail_with" module ChefSpec::Matchers class ResourceMatcher def initialize(resource_name, expected_action, expected_identity) @resource_name = resource_name @expected_action = expected_action @expected_identity = expected_identity end def with(parameters = {}) params.merge!(parameters) self end def at_compile_time raise ArgumentError, "Cannot specify both .at_converge_time and .at_compile_time!" if @converge_time @compile_time = true self end def at_converge_time raise ArgumentError, "Cannot specify both .at_compile_time and .at_converge_time!" if @compile_time @converge_time = true self end # # Allow users to specify fancy #with matchers. # def method_missing(m, *args, &block) if m.to_s =~ /^with_(.+)$/ with($1.to_sym => args.first) self else super end end def description %Q{#{@expected_action} #{@resource_name} "#{@expected_identity}"} end def matches?(runner) @runner = runner if resource ChefSpec::Coverage.cover!(resource) unmatched_parameters.empty? && correct_phase? end end def failure_message if resource if unmatched_parameters.empty? if @compile_time %Q{expected "#{resource}" to be run at compile time} else %Q{expected "#{resource}" to be run at converge time} end else message = %Q{expected "#{resource}" to have parameters:} \ "\n\n" \ " " + unmatched_parameters.collect { |parameter, h| msg = "#{parameter} #{h[:expected].inspect}, was #{h[:actual].inspect}" diff = ::RSpec::Matchers::ExpectedsForMultipleDiffs.from(h[:expected]) \ .message_with_diff(message, ::RSpec::Expectations.differ, h[:actual]) msg += diff if diff msg }.join("\n ") end else %Q{expected "#{@resource_name}[#{@expected_identity}]"} \ " with action :#{@expected_action} to be in Chef run." \ " Other #{@resource_name} resources:" \ "\n\n" \ " " + similar_resources.map(&:to_s).join("\n ") + "\n " end end def failure_message_when_negated if resource message = %Q{expected "#{resource}" actions #{resource.performed_actions.inspect} to not exist} else message = %Q{expected "#{resource}" to not exist} end message << " at compile time" if @compile_time message << " at converge time" if @converge_time message end private def unmatched_parameters return @_unmatched_parameters if @_unmatched_parameters @_unmatched_parameters = {} params.each do |parameter, expected| unless matches_parameter?(parameter, expected) @_unmatched_parameters[parameter] = { expected: expected, actual: safe_send(parameter), } end end @_unmatched_parameters end def matches_parameter?(parameter, expected) value = safe_send(parameter) if parameter == :source # Chef 11+ stores the source parameter internally as an Array Array(expected) == Array(value) elsif expected.is_a?(Class) # Ruby can't compare classes with === expected == value else expected === value end end def correct_phase? if @compile_time resource.performed_action(@expected_action)[:compile_time] elsif @converge_time resource.performed_action(@expected_action)[:converge_time] else true end end def safe_send(parameter) resource.send(parameter) rescue NoMethodError nil end # # Any other resources in the Chef run that have the same resource # type. Used by {failure_message} to be ultra helpful. # # @return [Array<Chef::Resource>] # def similar_resources @_similar_resources ||= @runner.find_resources(@resource_name) end # # Find the resource in the Chef run by the given class name and # resource identity/name. # # @see ChefSpec::SoloRunner#find_resource # # @return [Chef::Resource, nil] # def resource @_resource ||= @runner.find_resource(@resource_name, @expected_identity, @expected_action) end # # The list of parameters passed to the {with} matcher. # # @return [Hash] # def params @_params ||= {} end end end