diff --git a/.gitignore b/.gitignore index 2803e566..af66731a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,6 @@ /junit/ /log/ /pkg/ -/spec/fixtures/manifests/ /spec/fixtures/modules/* /tmp/ /vendor/ diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 03a9e5c4..7bfabab7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,11 +1,18 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2025-10-01 08:51:22 UTC using RuboCop version 1.73.2. +# on 2025-10-08 10:43:51 UTC using RuboCop version 1.73.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +RSpec/ExpectActual: + Exclude: + - '**/spec/routing/**/*' + - 'spec/acceptance/deferred_spec.rb' + # Offense count: 2 # Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. # Include: **/*_spec.rb diff --git a/spec/acceptance/deferred_spec.rb b/spec/acceptance/deferred_spec.rb new file mode 100644 index 00000000..0222ab4c --- /dev/null +++ b/spec/acceptance/deferred_spec.rb @@ -0,0 +1,78 @@ +# spec/acceptance/deferred_spec.rb +# frozen_string_literal: true + +require 'spec_helper_acceptance' + +def read_fixture(name) + File.read(File.join(__dir__, '..', 'fixtures', 'manifests', name)) +end + +def read_win_file_if_exists(path) + # Use a script block with literals; avoid $variables to prevent transport/quoting expansion + # Also keep exit 0 regardless of existence so run_shell doesn't raise. + ps = %{& { if (Test-Path -LiteralPath '#{path}') { Get-Content -Raw -LiteralPath '#{path}' } else { '<<>>' } } } + r = run_shell(%(powershell.exe -NoProfile -NonInteractive -Command "#{ps}")) + body = (r.stdout || '').to_s + exists = !body.include?('<<>>') + { exists: exists, content: exists ? body : '' } +end + +describe 'deferred values with dsc_lite' do + let(:control_manifest) { read_fixture('01_file_deferred.pp') } + let(:dsc_deferred_direct) { read_fixture('02_dsc_deferred_direct.pp') } + let(:dsc_deferred_inline) { read_fixture('02b_dsc_deferred_inline.pp') } # <— NEW + let(:dsc_deferred_stringified) { read_fixture('03a_dsc_deferred_stringified.pp') } + let(:dsc_deferred_bad_unwrap) { read_fixture('03b_dsc_deferred_bad_unwrap.pp') } + + it 'control (01): native file + Deferred resolves to hello-file' do + result = idempotent_apply(control_manifest) + expect(result.exit_code).to eq(0) + out = read_win_file_if_exists('C:/Temp/deferred_ok.txt') + expect(out[:exists]).to be(true) + expect(out[:content].strip).to eq('hello-file') + end + + it '02: passing Deferred via variable to DSC resolves to hello-dsc (otherwise flag bug)' do + apply = apply_manifest(dsc_deferred_direct) + out = read_win_file_if_exists('C:/Temp/from_dsc.txt') + content = out[:content].strip + if out[:exists] && content == 'hello-dsc' + expect(true).to be(true) + elsif out[:exists] && content =~ %r{Deferred\s*\(|Puppet::Pops::Types::Deferred}i + raise "BUG: 02 wrote stringified Deferred: #{content.inspect}\nApply:\n#{apply.stdout}#{apply.stderr}" + else + raise "Unexpected 02 outcome. Exists=#{out[:exists]} Content=#{content.inspect}\nApply:\n#{apply.stdout}#{apply.stderr}" + end + end + + # NEW 02b: inline Deferred on the DSC property (no variable intermediary) + it '02b: passing Deferred inline to DSC resolves to hello-dsc-inline (otherwise flag bug)' do + apply = apply_manifest(dsc_deferred_inline) + out = read_win_file_if_exists('C:/Temp/from_dsc_inline.txt') + content = out[:content].strip + if out[:exists] && content == 'hello-dsc-inline' + expect(true).to be(true) + elsif out[:exists] && content =~ %r{Deferred\s*\(|Puppet::Pops::Types::Deferred}i + raise "BUG: 02b wrote stringified Deferred: #{content.inspect}\nApply:\n#{apply.stdout}#{apply.stderr}" + else + raise "Unexpected 02b outcome. Exists=#{out[:exists]} Content=#{content.inspect}\nApply:\n#{apply.stdout}#{apply.stderr}" + end + end + + it '03a: stringifying a Deferred writes the function form (reproduces customer report)' do + apply_manifest(dsc_deferred_stringified) + out = read_win_file_if_exists('C:/Temp/from_dsc_var_string.txt') + expect(out[:exists]).to be(true) + expect(out[:content]).to match(%r{Deferred\s*\(|Puppet::Pops::Types::Deferred}i) + expect(out[:content]).not_to match(%r{\bhello-var\b}) + end + + it '03b: unwrap on a non‑Sensitive is a no‑op; also writes the function form' do + apply_manifest(dsc_deferred_bad_unwrap) + out = read_win_file_if_exists('C:/Temp/from_dsc_var_bad_unwrap.txt') + out = read_win_file_if_exists('C:/Temp/from_dsc_var.txt') unless out[:exists] + expect(out[:exists]).to be(true) + expect(out[:content]).to match(%r{Deferred\s*\(|Puppet::Pops::Types::Deferred}i) + expect(out[:content]).not_to match(%r{\bhello-var\b}) + end +end diff --git a/spec/fixtures/manifests/01_file_deferred.pp b/spec/fixtures/manifests/01_file_deferred.pp new file mode 100644 index 00000000..4f12b74e --- /dev/null +++ b/spec/fixtures/manifests/01_file_deferred.pp @@ -0,0 +1,12 @@ +# spec/fixtures/manifests/01_file_deferred.pp +file { 'C:/Temp': + ensure => directory, +} + +$deferred = Deferred('join', [['hello','-','file'], '']) + +file { 'C:/Temp/deferred_ok.txt': + ensure => file, + content => $deferred, + require => File['C:/Temp'], +} diff --git a/spec/fixtures/manifests/02_dsc_deferred_direct.pp b/spec/fixtures/manifests/02_dsc_deferred_direct.pp new file mode 100644 index 00000000..3fac8289 --- /dev/null +++ b/spec/fixtures/manifests/02_dsc_deferred_direct.pp @@ -0,0 +1,18 @@ +# spec/fixtures/manifests/02_dsc_deferred_direct.pp +file { 'C:/Temp': + ensure => directory, +} + +$deferred = Deferred('join', [['hello','-','dsc'], '']) + +dsc { 'WriteFileViaDSC': + resource_name => 'File', + module => 'PSDesiredStateConfiguration', + properties => { + 'DestinationPath' => 'C:\Temp\from_dsc.txt', + 'Type' => 'File', + 'Ensure' => 'Present', + 'Contents' => $deferred, + }, + require => File['C:/Temp'], +} diff --git a/spec/fixtures/manifests/02b_dsc_deferred_inline.pp b/spec/fixtures/manifests/02b_dsc_deferred_inline.pp new file mode 100644 index 00000000..6acd41f7 --- /dev/null +++ b/spec/fixtures/manifests/02b_dsc_deferred_inline.pp @@ -0,0 +1,16 @@ +# spec/fixtures/manifests/02b_dsc_deferred_inline.pp +file { 'C:/Temp': + ensure => directory, +} + +dsc { 'WriteFileViaDSCInline': + resource_name => 'File', + module => 'PSDesiredStateConfiguration', + properties => { + 'DestinationPath' => 'C:\Temp\from_dsc_inline.txt', + 'Type' => 'File', + 'Ensure' => 'Present', + 'Contents' => Deferred('join', [['hello','-','dsc-inline'], '']), + }, + require => File['C:/Temp'], +} diff --git a/spec/fixtures/manifests/03a_dsc_deferred_stringified.pp b/spec/fixtures/manifests/03a_dsc_deferred_stringified.pp new file mode 100644 index 00000000..819abf8d --- /dev/null +++ b/spec/fixtures/manifests/03a_dsc_deferred_stringified.pp @@ -0,0 +1,19 @@ +# spec/fixtures/manifests/03a_dsc_deferred_stringified.pp +file { 'C:/Temp': ensure => directory } + +$deferred = Deferred('join', [['hello','-','var'], '']) + +# WRONG on purpose: coerces Deferred to a String at compile time +$stringified = String($deferred) + +dsc { 'WriteFileViaDSCVarStringified': + resource_name => 'File', + module => 'PSDesiredStateConfiguration', + properties => { + 'DestinationPath' => 'C:\Temp\from_dsc_var_string.txt', + 'Type' => 'File', + 'Ensure' => 'Present', + 'Contents' => $stringified, + }, + require => File['C:/Temp'], +} diff --git a/spec/fixtures/manifests/03b_dsc_deferred_bad_unwrap.pp b/spec/fixtures/manifests/03b_dsc_deferred_bad_unwrap.pp new file mode 100644 index 00000000..c908f74a --- /dev/null +++ b/spec/fixtures/manifests/03b_dsc_deferred_bad_unwrap.pp @@ -0,0 +1,19 @@ +# spec/fixtures/manifests/03b_dsc_deferred_bad_unwrap.pp +file { 'C:/Temp': ensure => directory } + +$deferred = Deferred('join', [['hello','-','var'], '']) + +# WRONG: unwrap applies to Sensitive, not Deferred; this should compile-fail +$unwrapped_deferred = String($deferred.unwrap) + +dsc { 'WriteFileViaDSCVarBadUnwrap': + resource_name => 'File', + module => 'PSDesiredStateConfiguration', + properties => { + 'DestinationPath' => 'C:\Temp\from_dsc_var_bad_unwrap.txt', + 'Type' => 'File', + 'Ensure' => 'Present', + 'Contents' => $unwrapped_deferred, + }, + require => File['C:/Temp'], +}