From 1bb0f9b3951e1065898565c7761e411d4dc43a09 Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Fri, 21 Aug 2026 16:49:26 -0700 Subject: [PATCH] Render the creating resource when a path has several resources The render_file matcher looked up the resource for a path by trying cookbook_file, then file, then template, and stopped at the first match regardless of the action that resource performed. A recipe that renders a config file with a template and removes it with a guarded file resource therefore matched the file[:delete] resource and reported the file as unrendered, even though create_template matched and the template really was rendered. Prefer whichever candidate resource actually performs a create-like action and fall back to the first match so failure messages are unchanged when nothing creates the file. Fixes chefspec/chefspec#858 Signed-off-by: Tim Smith --- examples/render_file/recipes/default.rb | 10 ++++++++ examples/render_file/spec/default_spec.rb | 7 ++++++ lib/chefspec/matchers/render_file_matcher.rb | 26 +++++++++++++++++--- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/examples/render_file/recipes/default.rb b/examples/render_file/recipes/default.rb index c761390b..788e8954 100644 --- a/examples/render_file/recipes/default.rb +++ b/examples/render_file/recipes/default.rb @@ -13,3 +13,13 @@ template '/tmp/partial' do source 'partial.erb' end + +# A template and a file resource for the same path, as produced by a recipe +# that either renders or removes a config file depending on an attribute. +template '/tmp/template_or_delete' do + source 'template.erb' +end + +file '/tmp/template_or_delete' do + action :delete +end diff --git a/examples/render_file/spec/default_spec.rb b/examples/render_file/spec/default_spec.rb index 15c15c4e..1f1d3c56 100644 --- a/examples/render_file/spec/default_spec.rb +++ b/examples/render_file/spec/default_spec.rb @@ -195,4 +195,11 @@ it { is_expected.to_not render_file('/tmp/partial').with_content(/^Not(.+)$/) } end end + + context 'a path with both a template and a file resource' do + describe 'renders the template rather than matching the delete' do + it { is_expected.to render_file('/tmp/template_or_delete') } + it { is_expected.to render_file('/tmp/template_or_delete').with_content('This is content!') } + end + end end diff --git a/lib/chefspec/matchers/render_file_matcher.rb b/lib/chefspec/matchers/render_file_matcher.rb index 4bd029c3..8eeb03b1 100644 --- a/lib/chefspec/matchers/render_file_matcher.rb +++ b/lib/chefspec/matchers/render_file_matcher.rb @@ -86,9 +86,18 @@ def expected_content_message end def resource - @resource ||= @runner.find_resource(:cookbook_file, @path) || - @runner.find_resource(:file, @path) || - @runner.find_resource(:template, @path) + return @resource if defined?(@resource) + + candidates = %i{cookbook_file file template}.filter_map do |type| + @runner.find_resource(type, @path) + end + + # A recipe may declare more than one resource for the same path, most + # commonly a template that creates the file and a file resource that + # deletes it, with guards selecting between them. Prefer whichever + # resource actually creates the file so that we do not report the file as + # unrendered just because a sibling delete resource was found first. + @resource = candidates.find { |candidate| create_action?(candidate) } || candidates.first end # @@ -99,6 +108,17 @@ def resource # @return [true, false] # def has_create_action? + create_action?(resource) + end + + # + # Determines if the given resource has a create-like action. + # + # @param [Chef::Resource] resource + # + # @return [true, false] + # + def create_action?(resource) %i{create create_if_missing}.any? { |action| resource.performed_action?(action) } end