From ee628f3912ed4eef84be142449f3ae422c811d1f Mon Sep 17 00:00:00 2001 From: Gavin Didrichsen Date: Mon, 26 Oct 2026 12:00:12 +0000 Subject: [PATCH 01/10] (MAINT) Update module to the latest pdk-templates (PDK 4.0.1) Run `pdk update` to move template-ref from tags/3.2.0 to heads/main (de9a08b). The update is mechanical and touches only the managed files: .gitignore, .pdkignore, .puppet-lint.rc, .rubocop.yml, Gemfile and metadata.json. The part that matters for CI is the Gemfile. pdk-templates PR #662 added puppet_floor_version, which defaults PUPPET_GEM_VERSION to '~> 8.17' on Ruby 3.1-3.3. Without it Bundler resolves multi_json to 1.21 first, which puppet 8.17+ cannot use, and silently backtracks puppet to 8.16.0. That backtrack is what breaks the Puppet 8 spec job (FrozenError in puppet/util/monkey_patches.rb). Co-Authored-By: Claude Sonnet 5.5 --- .gitignore | 7 +++ .pdkignore | 9 ++- .puppet-lint.rc | 8 +++ .rubocop.yml | 164 ++++++++++++++++++++++++++++++++++++++++-------- Gemfile | 141 +++++++++++++++++++++++++++-------------- metadata.json | 8 +-- 6 files changed, 257 insertions(+), 80 deletions(-) diff --git a/.gitignore b/.gitignore index 3f155121..2803e566 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ /spec/fixtures/modules/* /tmp/ /vendor/ +/.vendor/ /convert_report.txt /update_report.txt .DS_Store @@ -26,3 +27,9 @@ .envrc /inventory.yaml /spec/fixtures/litmus_inventory.yaml +.resource_types +.modules +.task_cache.json +.plan_cache.json +.rerun.json +bolt-debug.log diff --git a/.pdkignore b/.pdkignore index 862847a7..e758847e 100644 --- a/.pdkignore +++ b/.pdkignore @@ -19,6 +19,7 @@ /spec/fixtures/modules/* /tmp/ /vendor/ +/.vendor/ /convert_report.txt /update_report.txt .DS_Store @@ -26,6 +27,12 @@ .envrc /inventory.yaml /spec/fixtures/litmus_inventory.yaml +.resource_types +.modules +.task_cache.json +.plan_cache.json +.rerun.json +bolt-debug.log /.fixtures.yml /Gemfile /.gitattributes @@ -36,7 +43,7 @@ /Rakefile /rakelib/ /.rspec -/..yml +/.rubocop.yml /.yardopts /spec/ /.vscode/ diff --git a/.puppet-lint.rc b/.puppet-lint.rc index cc96ece0..9e15c6e0 100644 --- a/.puppet-lint.rc +++ b/.puppet-lint.rc @@ -1 +1,9 @@ +--fail-on-warnings --relative +--no-80chars-check +--no-140chars-check +--no-class_inherits_from_params_class-check +--no-autoloader_layout-check +--no-documentation-check +--no-single_quote_string_with_variables-check +--ignore-paths=.vendor/**/*.pp,.bundle/**/*.pp,pkg/**/*.pp,spec/**/*.pp,tests/**/*.pp,types/**/*.pp,vendor/**/*.pp diff --git a/.rubocop.yml b/.rubocop.yml index 102a072d..86228843 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,30 +1,142 @@ -inherit_from: .rubocop_todo.yml - -require: - - rubocop-performance - - rubocop-rspec - +--- +plugins: +- rubocop-performance +- rubocop-hash_inspect +- rubocop-rspec +- rubocop-rspec_rails +- rubocop-factory_bot +- rubocop-capybara AllCops: - Exclude: - - Gemfile - - Rakefile - - spec/fixtures/**/* - - vendor/bundle/**/* NewCops: enable - SuggestExtensions: false - TargetRubyVersion: '3.1' - -# Disabled -Style/ClassAndModuleChildren: - Enabled: false - + DisplayCopNames: true + TargetRubyVersion: 3.1 + Include: + - "**/*.rb" + Exclude: + - bin/* + - ".vendor/**/*" + - "**/Gemfile" + - "**/Rakefile" + - pkg/**/* + - spec/fixtures/**/* + - vendor/**/* + - "**/Puppetfile" + - "**/Vagrantfile" + - "**/Guardfile" Layout/LineLength: - Max: 196 - -#################################################### -# Cops below here due for deprecation -#################################################### -# ``Rspec/FilePath`` is going to be deprecated in the next major release of rubocop >=3.0.0: see -# As the new cops are already present, e.g., Rspec/SpecFilePathPathFormat, then disabling this in preparation -RSpec/FilePath: + Description: People have wide screens, use them. + Max: 200 +Layout/EndOfLine: + Description: We enforce lf to ensure consistency across platforms. + EnforcedStyle: lf +RSpec/BeforeAfterAll: + Description: Beware of using after(:all) as it may cause state to leak between tests. + A necessary evil in acceptance testing. + Exclude: + - spec/acceptance/**/*.rb +RSpec/HookArgument: + Description: Prefer explicit :each argument, matching existing module's style + EnforcedStyle: each +RSpec/DescribeSymbol: + Exclude: + - spec/unit/facter/**/*.rb +Style/BlockDelimiters: + Description: Prefer braces for chaining. Mostly an aesthetical choice. Better to + be consistent then. + EnforcedStyle: braces_for_chaining +Style/ClassAndModuleChildren: + Description: Compact style reduces the required amount of indentation. + EnforcedStyle: compact +Style/EmptyElse: + Description: Enforce against empty else clauses, but allow `nil` for clarity. + EnforcedStyle: empty +Style/FormatString: + Description: Following the main puppet project's style, prefer the % format format. + EnforcedStyle: percent +Style/FormatStringToken: + Description: Following the main puppet project's style, prefer the simpler template + tokens over annotated ones. + EnforcedStyle: template +Style/Lambda: + Description: Prefer the keyword for easier discoverability. + EnforcedStyle: literal +Style/RegexpLiteral: + Description: Community preference. See https://github.com/voxpupuli/modulesync_config/issues/168 + EnforcedStyle: percent_r +Style/TernaryParentheses: + Description: Checks for use of parentheses around ternary conditions. Enforce parentheses + on complex expressions for better readability, but seriously consider breaking + it up. + EnforcedStyle: require_parentheses_when_complex +Style/TrailingCommaInArguments: + Description: Prefer always trailing comma on multiline argument lists. This makes + diffs, and re-ordering nicer. + EnforcedStyleForMultiline: comma +Style/TrailingCommaInArrayLiteral: + Description: Prefer always trailing comma on multiline literals. This makes diffs, + and re-ordering nicer. + EnforcedStyleForMultiline: comma +Style/SymbolArray: + Description: Using percent style obscures symbolic intent of array's contents. + EnforcedStyle: brackets +Style/WordArray: + Description: Using percent style obscures symbolic intent of array's contents. + EnforcedStyle: brackets +Style/TrailingCommaInHashLiteral: + Description: PDK generates multiline hashes with trailing commas. Align with Style/TrailingCommaInArguments + and Style/TrailingCommaInArrayLiteral (both comma) so hash literals follow the + same project style. + EnforcedStyleForMultiline: comma +RSpec/DescribeClass: + Description: rspec-puppet describes its subject by string class/define name (no + Ruby constant exists to reference), so this cop is never satisfiable for puppet + specs. This is structurally inapplicable, not a style preference -- distinct from + D-08 which governs style cops. Enabled: false +RSpec/MessageSpies: + Description: PDK-generated specs and the rspec-puppet convention use the mock-style + `expect(...).to receive(...)` idiom throughout, not RuboCop's default spy-style + `have_received`. Restores the setting the old `strict` profile applied globally + (no Exclude needed -- provider/transport specs already comply). + EnforcedStyle: receive +RSpec/ExampleLength: + Description: The resource-API provider specs PDK generates contain multi-line examples + covering multiple CRUD operations. Scoped to PDK-generated provider and transport + spec paths only (D-08). + Exclude: + - spec/unit/puppet/provider/**/*_spec.rb + - spec/unit/puppet/transport/**/*_spec.rb +RSpec/MultipleExpectations: + Description: The resource-API provider specs PDK generates use multiple expectations + per example to cover resource attribute checks. Scoped to PDK-generated provider + and transport spec paths only (D-08). + Exclude: + - spec/unit/puppet/provider/**/*_spec.rb + - spec/unit/puppet/transport/**/*_spec.rb +RSpec/VerifiedDoubleReference: + Description: The resource-API provider and transport specs PDK generates use string-referenced + instance doubles (the Puppet provider constant is not loaded in the spec context). + Scoped to PDK-generated provider and transport spec paths only (D-08). + Exclude: + - spec/unit/puppet/provider/**/*_spec.rb + - spec/unit/puppet/transport/**/*_spec.rb +RSpec/ContextWording: + Description: rspec-puppet-facts uses the `context "on + Prefixes: + - when + - with + - without + - 'on' +Style/MixinUsage: + Description: rspec-puppet-facts requires the top-level `include RspecPuppetFacts` + idiom in spec_helper, which cannot be cleanly restructured without breaking the + facts test harness. Scoped to spec_helper only (D-07). + Exclude: + - spec/spec_helper.rb +Lint/EmptyClass: + Description: The generated transport device file uses the idiomatic Ruby namespace + forward-declaration `class Puppet::Util::NetworkDevice; end`. An empty class is + the standard namespace-shim idiom here and is not non-idiomatic code. Scoped to + the network_device path only (D-08). + Exclude: + - lib/puppet/util/network_device/**/*.rb diff --git a/Gemfile b/Gemfile index 86650708..324bf6f4 100644 --- a/Gemfile +++ b/Gemfile @@ -1,75 +1,118 @@ -# For puppetcore, set GEM_SOURCE_PUPPETCORE = 'https://rubygems-puppetcore.puppet.com' +# frozen_string_literal: true + +# Defaults to puppetcore regardless of PUPPET_FORGE_TOKEN/network state. +# Opt out: bundle config set gemsource.public true gemsource_default = ENV['GEM_SOURCE'] || 'https://rubygems.org' -gemsource_puppetcore = if ENV['PUPPET_FORGE_TOKEN'] && !ENV['PUPPET_FORGE_TOKEN'].empty? - 'https://rubygems-puppetcore.puppet.com' - else - ENV['GEM_SOURCE_PUPPETCORE'] || gemsource_default - end +gemsource_public_opt_out = Bundler.settings['gemsource.public'] +gemsource_puppetcore = if gemsource_public_opt_out + gemsource_default +else + 'https://rubygems-puppetcore.puppet.com' +end source gemsource_default -gem "rexml", require: false +unless ENV['PUPPET_FORGE_TOKEN'] || ENV['BUNDLE_RUBYGEMS___PUPPETCORE__PUPPET__COM'] || + Bundler.settings['gemsource.airgapped'] || gemsource_public_opt_out + Bundler.ui.warn <<~WARNING + No puppetcore credentials detected. + On an airgapped install, run `bundle config set gemsource.airgapped true` to suppress this warning. + To use public gems instead, run `bundle config set gemsource.public true`. + WARNING +end + +def location_for(place_or_constraint, fake_constraint = nil, opts = {}) + git_url_regex = /\A(?(?:https?|git)[:@][^#]*)(?:#(?.*))?/ + file_url_regex = %r{\Afile://(?.*)} + + if place_or_constraint && (git_url = place_or_constraint.match(git_url_regex)) + # Git source → ignore :source, keep fake_constraint + [fake_constraint, { git: git_url[:url], branch: git_url[:branch], require: false }].compact -def location_for(place_or_version, fake_version = nil, opts = {}) - git_url_regex = %r{\A(?(https?|git)[:@][^#]*)(#(?.*))?} - file_url_regex = %r{\Afile:\/\/(?.*)} + elsif place_or_constraint && (file_url = place_or_constraint.match(file_url_regex)) + # File source → ignore :source, keep fake_constraint or default >= 0 + [fake_constraint || '>= 0', { path: File.expand_path(file_url[:path]), require: false }] - if place_or_version && (git_url = place_or_version.match(git_url_regex)) - [fake_version, { git: git_url[:url], branch: git_url[:branch], require: false }].compact - elsif place_or_version && (file_url = place_or_version.match(file_url_regex)) - ['>= 0', { path: File.expand_path(file_url[:path]), require: false }] else - [place_or_version, { require: false }.merge(opts)] + # Plain version constraint → merge opts (including :source if provided) + [place_or_constraint, { require: false }.merge(opts)] + end +end + +# Works around a bundler multi_json/puppet resolution bug - see pdk-templates/docs/adr/0001. +def puppet_floor_version(puppetcore_source) + return nil if puppetcore_source == 'https://rubygems.org' + + ruby_version = Gem::Version.new(RUBY_VERSION.dup) + return '~> 9.0' if Gem::Requirement.create('>= 3.4.0').satisfied_by?(ruby_version) + return '~> 8.17' if Gem::Requirement.create('>= 3.1.0').satisfied_by?(ruby_version) + + nil # puppet 8+ requires ruby >= 3.1; leave older rubies unconstrained +end + +# Print debug information if DEBUG_GEMS or VERBOSE is set +def print_gem_statement_for(gems) + puts 'DEBUG: Gem definitions that will be generated:' + gems.each do |gem_name, gem_params| + puts "DEBUG: gem #{([gem_name.inspect] + gem_params.map(&:inspect)).join(', ')}" end end group :development do gem "json", '= 2.6.1', require: false if Gem::Requirement.create(['>= 3.1.0', '< 3.1.3']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) gem "json", '= 2.6.3', require: false if Gem::Requirement.create(['>= 3.2.0', '< 4.0.0']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) - gem "deep_merge", '~> 1.0', require: false - gem "voxpupuli-puppet-lint-plugins", '~> 7.0', require: false - gem "facterdb", '~> 4.0', require: false + gem "json", '= 2.18.0', require: false if Gem::Requirement.create(['>= 4.0.0', '< 5.0.0']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) + gem "racc", '~> 1.4.0', require: false if Gem::Requirement.create(['>= 2.7.0', '< 3.0.0']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) + gem "deep_merge", '~> 1.2.2', require: false + gem "voxpupuli-puppet-lint-plugins", '~> 7.0', require: false if gemsource_puppetcore != "https://rubygems.org" + gem "voxpupuli-puppet-lint-plugins", '~> 6.0', require: false if gemsource_puppetcore == "https://rubygems.org" + gem "facterdb", '~> 2.1', require: false if Gem::Requirement.create(['< 3.0.0']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) + gem "facterdb", '~> 3.0', require: false if Gem::Requirement.create(['>= 3.0.0']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) gem "metadata-json-lint", '~> 4.0', require: false - gem "rspec-puppet-facts", '~> 6.0', require: false + gem "json-schema", '< 5.1.1', require: false + gem "rspec-puppet-facts", '~> 4.0', require: false if Gem::Requirement.create(['< 3.0.0']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) + gem "rspec-puppet-facts", '~> 5.0', require: false if Gem::Requirement.create(['>= 3.0.0']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) gem "dependency_checker", '~> 1.0.0', require: false - gem "parallel_tests", '3.13.0', require: false + gem "parallel_tests", '= 3.12.1', require: false gem "pry", '~> 0.10', require: false gem "simplecov-console", '~> 0.9', require: false - gem "puppet-debugger", '~> 1.0', require: false - gem "rubocop", '~> 1.70.0', require: false - gem "rubocop-performance", '1.22.1', require: false - gem "rubocop-rspec", '= 2.19.0', require: false - gem "rb-readline", '= 0.5.5', require: false, platforms: [:mswin, :mingw, :x64_mingw] + gem "puppet-debugger", '~> 1.6', require: false + gem "rubocop", '~> 1.73.0', require: false + gem "rubocop-performance", '~> 1.24.0', require: false + gem "rubocop-hash_inspect", '~> 0.2', require: false + gem "rubocop-rspec", '~> 3.5.0', require: false + gem "rubocop-rspec_rails", '~> 2.31.0', require: false + gem "rubocop-factory_bot", '~> 2.27.0', require: false + gem "rubocop-capybara", '~> 2.22.0', require: false + gem "rb-readline", '= 0.5.5', require: false, platforms: [:windows] + gem "bigdecimal", '< 3.2.2', require: false, platforms: [:windows] end group :development, :release_prep do - gem "puppet-strings", '~> 4.0', require: false - gem "puppetlabs_spec_helper", '~> 9.0', require: false + gem "puppet-strings", '>= 4.0', '< 6.0', require: false + gem "puppetlabs_spec_helper", '~> 9.0', require: false if gemsource_puppetcore != "https://rubygems.org" + gem "puppetlabs_spec_helper", '~> 8.0', require: false if gemsource_puppetcore == "https://rubygems.org" + gem "puppet-blacksmith", '>= 7.0', '< 10.0', require: false end group :system_tests do - gem "CFPropertyList", '< 3.0.7', require: false, platforms: [:mswin, :mingw, :x64_mingw] - gem "ffi", require: false, platforms: [:mswin, :mingw, :x64_mingw] + gem "puppet_litmus", '~> 2.5', require: false + gem "faraday", '~> 2.5', require: false + gem "CFPropertyList", '< 3.0.7', require: false if RUBY_PLATFORM.include?('darwin') gem "serverspec", '~> 2.41', require: false gem "net-ssh", '< 8.0', require: false end -puppet_version = ENV['PUPPET_GEM_VERSION'] -facter_version = ENV['FACTER_GEM_VERSION'] -hiera_version = ENV['HIERA_GEM_VERSION'] - gems = {} +bolt_version = ENV.fetch('BOLT_GEM_VERSION', nil) +puppet_version = ENV.fetch('PUPPET_GEM_VERSION', puppet_floor_version(gemsource_puppetcore)) +facter_version = ENV.fetch('FACTER_GEM_VERSION', nil) +hiera_version = ENV.fetch('HIERA_GEM_VERSION', nil) -gemsource_facter = if Gem.ruby_version >= Gem::Version.new('4.0') - gemsource_puppetcore - else - gemsource_default - end - +gems['bolt'] = location_for(bolt_version, nil, { source: gemsource_puppetcore }) gems['puppet'] = location_for(puppet_version, nil, { source: gemsource_puppetcore }) -gems['facter'] = location_for(facter_version, nil, { source: gemsource_facter }) - -# If a hiera version has been specified via the environment variable - -gems['hiera'] = location_for(hiera_version) if hiera_version +gems['facter'] = location_for(facter_version, nil, { source: gemsource_puppetcore }) +gems['hiera'] = location_for(hiera_version, nil, {}) if hiera_version +# Generate the gem definitions +print_gem_statement_for(gems) if ENV['DEBUG'] gems.each do |gem_name, gem_params| gem gem_name, *gem_params end @@ -77,12 +120,14 @@ end # Evaluate Gemfile.local and ~/.gemfile if they exist extra_gemfiles = [ "#{__FILE__}.local", - File.join(Dir.home, '.gemfile'), + File.join(Dir.home, '.gemfile') ] extra_gemfiles.each do |gemfile| - if File.file?(gemfile) && File.readable?(gemfile) - eval(File.read(gemfile), binding) - end + next unless File.file?(gemfile) && File.readable?(gemfile) + + # rubocop:disable Security/Eval + eval(File.read(gemfile), binding) + # rubocop:enable Security/Eval end # vim: syntax=ruby diff --git a/metadata.json b/metadata.json index 8ec653bd..9607229e 100644 --- a/metadata.json +++ b/metadata.json @@ -7,9 +7,7 @@ "source": "https://github.com/puppetlabs/ruby-pwsh", "project_page": "https://github.com/puppetlabs/ruby-pwsh/blob/main/pwshlib.md", "issues_url": "https://github.com/puppetlabs/ruby-pwsh/issues", - "dependencies": [ - - ], + "dependencies": [], "operatingsystem_support": [ { "operatingsystem": "Windows", @@ -81,7 +79,7 @@ "version_requirement": ">= 8.0.0 < 10.0.0" } ], - "pdk-version": "3.0.1", + "pdk-version": "4.0.1", "template-url": "https://github.com/puppetlabs/pdk-templates.git#main", - "template-ref": "tags/3.2.0-0-gb257ef1" + "template-ref": "heads/main-0-gde9a08b" } From 512f646dcb0eef08342e5e14fe524bd7beef3567 Mon Sep 17 00:00:00 2001 From: Gavin Didrichsen Date: Mon, 26 Oct 2026 12:07:44 +0000 Subject: [PATCH 02/10] Autocorrect rubocop lint errors Signed-off-by: Gavin Didrichsen --- .../dsc_base_provider/dsc_base_provider.rb | 86 ++-- lib/pwsh.rb | 14 +- lib/pwsh/util.rb | 236 ++++++----- lib/pwsh/windows_powershell.rb | 158 ++++--- spec/acceptance/dsc/basic.rb | 40 +- spec/acceptance/dsc/cim_instances.rb | 12 +- spec/acceptance/dsc/class.rb | 32 +- spec/acceptance/dsc/complex.rb | 10 +- spec/spec_helper.rb | 2 +- .../dsc_base_provider_spec.rb | 392 +++++++++--------- spec/unit/pwsh/util_spec.rb | 78 ++-- spec/unit/pwsh_spec.rb | 72 ++-- 12 files changed, 560 insertions(+), 572 deletions(-) diff --git a/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb b/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb index f33dea86..171de739 100644 --- a/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb +++ b/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb @@ -32,10 +32,10 @@ def initialize # @param hashes [Array] the list of hashes to search the cache for # @return [Array] an array containing the matching hashes for the search condition, if any def fetch_cached_hashes(cache, hashes) - cache.reject do |item| + cache.reject { |item| matching_hash = hashes.select { |hash| (item.to_a - hash.to_a).empty? || (hash.to_a - item.to_a).empty? } matching_hash.empty? - end.flatten + }.flatten end # Implements the canonicalize feature of the Resource API; this method is called first against any resources @@ -54,7 +54,7 @@ def canonicalize(context, resources) resources.collect do |r| # During RSAPI refresh runs mandatory parameters are stripped and not available; # Instead of checking again and failing, search the cache for a namevar match. - namevarized_r = r.select { |k, _v| namevar_attributes(context).include?(k) } + namevarized_r = r.slice(*namevar_attributes(context)) cached_result = fetch_cached_hashes(@cached_canonicalized_resource, [namevarized_r]).first if cached_result.nil? # If the resource is meant to be absent, skip canonicalization and rely on the manifest @@ -76,7 +76,7 @@ def canonicalize(context, resources) canonicalized = r.dup @cached_canonicalized_resource << r.dup else - parameters = r.select { |name, _properties| parameter_attributes(context).include?(name) } + parameters = r.slice(*parameter_attributes(context)) canonicalized.merge!(parameters) canonicalized[:name] = r[:name] if r[:dsc_psdscrunascredential].nil? @@ -162,9 +162,7 @@ def get(context, names = nil) mandatory_properties = {} else canonicalized_resource = @cached_canonicalized_resource[0].dup - mandatory_properties = canonicalized_resource.select do |attribute, _value| - (mandatory_get_attributes(context) - namevar_attributes(context)).include?(attribute) - end + mandatory_properties = canonicalized_resource.slice(*(mandatory_get_attributes(context) - namevar_attributes(context))) # If dsc_psdscrunascredential was specified, re-add it here. mandatory_properties[:dsc_psdscrunascredential] = canonicalized_resource[:dsc_psdscrunascredential] if canonicalized_resource.key?(:dsc_psdscrunascredential) end @@ -306,7 +304,7 @@ def perform_fresh_get(context, name, should) next unless valid_attributes.include?(type_key.to_s) # Sanitize .NET serialization artifacts (e.g., "System.Object[]") - if value.is_a?(String) && value =~ /^System\.\w+\[\]$/ + if value.is_a?(String) && value =~ %r{^System\.\w+\[\]$} result[type_key] = nil else # Normalize CIM instances the same way invoke_get_method does, @@ -403,7 +401,7 @@ def invoke_dsc_resource(context, name_hash, props, method) if output[:stdout].nil? message = 'Nothing returned.' - message += " #{output[:errormessage]}" if output[:errormessage]&.match?(/PowerShell module timeout \(\d+ ms\) exceeded while executing/) + message += " #{output[:errormessage]}" if output[:errormessage]&.match?(%r{PowerShell module timeout \(\d+ ms\) exceeded while executing}) context.err(message) return nil end @@ -415,7 +413,7 @@ def invoke_dsc_resource(context, name_hash, props, method) return nil end context.debug("raw data received: #{data.inspect}") - collision_error_matcher = /The Invoke-DscResource cmdlet is in progress and must return before Invoke-DscResource can be invoked/ + collision_error_matcher = %r{The Invoke-DscResource cmdlet is in progress and must return before Invoke-DscResource can be invoked} error = data['errormessage'] @@ -530,7 +528,7 @@ def insync?(context, name, property_name, _is_hash, should_hash) # insync? again for the same property. Return nil so default comparison handles it. cache_key = name.is_a?(Hash) ? name[:name] : name property_key = "#{cache_key}_#{property_name}" - return nil if @insync_property_cache.delete(property_key) + return false if @insync_property_cache.delete(property_key) if should_hash[:validation_mode] == 'resource' insync_resource_mode(context, name, property_name, should_hash) @@ -654,7 +652,7 @@ def invoke_get_method(context, name_hash) # @return [Hash] returns a hash representing the DSC resource munged to the representation the Puppet Type expects def invoke_get_method_for_canonicalize(context, name_hash) # Check the canonicalize-specific cache first - cached = fetch_cached_hashes(@cached_canonicalize_results, [name_hash.select { |k, _v| namevar_attributes(context).include?(k) }]) + cached = fetch_cached_hashes(@cached_canonicalize_results, [name_hash.slice(*namevar_attributes(context))]) return cached.first unless cached.empty? # Call invoke_get_method which will do the actual DSC Get call. @@ -665,7 +663,7 @@ def invoke_get_method_for_canonicalize(context, name_hash) # Remove the entry that invoke_get_method just added to @cached_query_results # so that get() will do its own fresh DSC Get call and produce independent "is" data. @cached_query_results.select! do |item| - matching = fetch_cached_hashes([item], [name_hash.select { |k, _v| namevar_attributes(context).include?(k) }]) + matching = fetch_cached_hashes([item], [name_hash.slice(*namevar_attributes(context))]) matching.empty? end @@ -684,7 +682,7 @@ def canonicalize_get_value!(context, data, type_key, query_props) end # Convert DateTime back to appropriate type - if context.type.attributes[type_key][:mof_type] =~ /DateTime/i && !data[type_key].nil? + if context.type.attributes[type_key][:mof_type] =~ %r{DateTime}i && !data[type_key].nil? data[type_key] = begin Puppet::Pops::Time::Timestamp.parse(data[type_key]) rescue ArgumentError, TypeError => e @@ -700,7 +698,7 @@ def canonicalize_get_value!(context, data, type_key, query_props) # Sanitize .NET serialization artifacts (e.g., "System.Object[]") to nil. def sanitize_dotnet_artifacts!(context, data) data.each do |key, value| - next unless value.is_a?(String) && value =~ /^System\.\w+\[\]$/ + next unless value.is_a?(String) && value =~ %r{^System\.\w+\[\]$} context.debug("Sanitizing .NET serialization artifact for #{key}: #{value} -> nil") data[key] = nil @@ -727,7 +725,7 @@ def preserve_namevar_values!(context, data, name_hash) def invoke_set_method(context, name, should) context.debug("Invoking Set Method for '#{name}' with #{should.inspect}") - apply_props = should.select { |k, _v| k.to_s =~ /^dsc_/ } + apply_props = should.select { |k, _v| k.to_s =~ %r{^dsc_} } invoke_dsc_resource(context, should, apply_props, 'set') # TODO: Implement this functionality for notifying a DSC reboot? @@ -745,7 +743,7 @@ def invoke_test_method(context, name, should) context.debug("Relying on DSC Test method for validating if '#{name}' is in the desired state") context.debug("Invoking Test Method for '#{name}' with #{should.inspect}") - test_props = should.select { |k, _v| k.to_s =~ /^dsc_/ } + test_props = should.select { |k, _v| k.to_s =~ %r{^dsc_} } data = invoke_dsc_resource(context, name, test_props, 'test') # Something went wrong with Invoke-DscResource; fall back on property state comparisons return nil if data.nil? @@ -770,7 +768,7 @@ def invoke_test_method(context, name, should) def invocable_resource(should, context, dsc_invoke_method) resource = {} resource[:parameters] = {} - %i[name dscmeta_resource_friendly_name dscmeta_resource_name dscmeta_resource_implementation dscmeta_module_name dscmeta_module_version].each do |k| + [:name, :dscmeta_resource_friendly_name, :dscmeta_resource_name, :dscmeta_resource_implementation, :dscmeta_module_name, :dscmeta_module_version].each do |k| resource[k] = context.type.definition[k] end should.each do |k, v| @@ -780,7 +778,7 @@ def invocable_resource(should, context, dsc_invoke_method) resource[:parameters][k] = {} resource[:parameters][k][:value] = v - %i[mof_type mof_is_embedded].each do |ky| + [:mof_type, :mof_is_embedded].each do |ky| resource[:parameters][k][ky] = context.type.definition[:attributes][k][ky] end end @@ -841,9 +839,9 @@ def puppetize_name(name) # Puppet module names must be lower case name = name.downcase # Puppet module names must only include lowercase letters, digits and underscores - name = name.gsub(/[^a-z0-9_]/, '_') + name = name.gsub(%r{[^a-z0-9_]}, '_') # Puppet module names must not start with a number so if it does, append the letter 'a' to the name. Eg: '7zip' becomes 'a7zip' - name = name.match?(/^\d/) ? "a#{name}" : name # rubocop:disable Lint/UselessAssignment + name = "a#{name}" if name.match?(%r{^\d}) # rubocop:disable Lint/UselessAssignment end # Return a UUID with the dashes turned into underscores to enable the specifying of guaranteed-unique @@ -1021,7 +1019,7 @@ def enum_values(context, attribute) return [] unless type_string&.include?('Enum[') # Extract the enum values from the type string - enum_content = type_string.match(/Enum\[(.*?)\]/)&.[](1) + enum_content = type_string.match(%r{Enum\[(.*?)\]})&.[](1) # Return an empty array if we couldn't find the enum values return [] if enum_content.nil? @@ -1073,20 +1071,20 @@ def munge_psmodulepath(resource) # @return [String] An array of lines of PowerShell to instantiate PSCredentialObjects and store them in variables def prepare_credentials(resource) credentials_block = [] - resource[:parameters].each do |_property_name, property_hash| + resource[:parameters].each_value do |property_hash| next unless property_hash[:mof_type] == 'PSCredential' next if property_hash[:value].nil? variable_name = random_variable_name credential_hash = { 'user' => property_hash[:value]['user'], - 'password' => escape_quotes(unwrap_string(property_hash[:value]['password'])) + 'password' => escape_quotes(unwrap_string(property_hash[:value]['password'])), } credentials_block << format_pscredential(variable_name, credential_hash) instantiated_variables.merge!(variable_name => credential_hash) end credentials_block.join("\n") - credentials_block == [] ? '' : credentials_block + (credentials_block == []) ? '' : credentials_block end # Write a line of PowerShell which creates a PSCredential object and assigns it to a variable @@ -1108,7 +1106,7 @@ def format_pscredential(variable_name, credential_hash) # @return [String] An array of lines of PowerShell to instantiate CIM Instances and store them in variables def prepare_cim_instances(resource) cim_instances_block = [] - resource[:parameters].each do |_property_name, property_hash| + resource[:parameters].each_value do |property_hash| next unless property_hash[:mof_is_embedded] next if property_hash[:mof_type] == 'PSCredential' # Credentials are handled separately @@ -1122,13 +1120,13 @@ def prepare_cim_instances(resource) cim_instance_hashes.each do |instance| variable_name = random_variable_name class_name = instance['cim_instance_type'] - properties = instance.reject { |k, _v| k == 'cim_instance_type' } + properties = instance.except('cim_instance_type') cim_instances_block << format_ciminstance(variable_name, class_name, properties) instantiated_variables.merge!(variable_name => instance) end end # We have to handle arrays of CIM instances slightly differently - if /\[\]$/.match?(property_hash[:mof_type]) + if %r{\[\]$}.match?(property_hash[:mof_type]) class_name = property_hash[:mof_type].gsub('[]', '') property_hash[:value].each do |hash| variable_name = random_variable_name @@ -1142,7 +1140,7 @@ def prepare_cim_instances(resource) instantiated_variables.merge!(variable_name => property_hash[:value]) end end - cim_instances_block == [] ? '' : cim_instances_block.join("\n") + (cim_instances_block == []) ? '' : cim_instances_block.join("\n") end # Recursively search for and return CIM instances nested in an enumerable @@ -1189,7 +1187,7 @@ def invoke_params(resource) # rubocop:disable Metrics/MethodLength params = { Name: resource[:dscmeta_resource_friendly_name], Method: resource[:dsc_invoke_method], - Property: {} + Property: {}, } if resource.key?(:dscmeta_module_version) params[:ModuleName] = {} @@ -1208,14 +1206,14 @@ def invoke_params(resource) # rubocop:disable Metrics/MethodLength next if property_name == :dsc_timeout # strip dsc_ from the beginning of the property name declaration - name = property_name.to_s.gsub(/^dsc_/, '').to_sym + name = property_name.to_s.gsub(%r{^dsc_}, '').to_sym params[:Property][name] = case property_hash[:mof_type] when 'PSCredential' # format can't unwrap Sensitive strings nested in arbitrary hashes/etc, so make # the Credential hash interpolable as it will be replaced by a variable reference. { 'user' => property_hash[:value]['user'], - 'password' => escape_quotes(unwrap_string(property_hash[:value]['password'])) + 'password' => escape_quotes(unwrap_string(property_hash[:value]['password'])), } when 'DateTime' # These have to be handled specifically because they rely on the *Puppet* DateTime, @@ -1232,11 +1230,11 @@ def invoke_params(resource) # rubocop:disable Metrics/MethodLength # CIM instances do not do a consistent job of casting an empty array properly. empty_array_parameters = resource[:parameters].select { |_k, v| v[:value].is_a?(Array) && v[:value].empty? } empty_array_parameters.each do |name, properties| - param_block_name = name.to_s.gsub(/^dsc_/, '') + param_block_name = name.to_s.gsub(%r{^dsc_}, '') params_block = params_block.gsub("#{param_block_name} = @()", "#{param_block_name} = [#{properties[:mof_type]}]@()") end # HACK: make CIM instances work: - resource[:parameters].select { |_key, hash| hash[:mof_is_embedded] && hash[:mof_type].include?('[]') }.each do |_property_name, property_hash| + resource[:parameters].select { |_key, hash| hash[:mof_is_embedded] && hash[:mof_type].include?('[]') }.each_value do |property_hash| formatted_property_hash = interpolate_variables(format(property_hash[:value])) params_block = params_block.gsub(formatted_property_hash, "[CimInstance[]]#{formatted_property_hash}") end @@ -1298,11 +1296,9 @@ def unwrap(value) end unwrapped when Array - unwrapped = [] - value.each do |v| - unwrapped << unwrap(v) + unwrapped = value.map do |v| + unwrap(v) end - unwrapped else value end @@ -1323,11 +1319,9 @@ def unwrap_string(value) end unwrapped when Array - unwrapped = [] - value.each do |v| - unwrapped << unwrap_string(v) + unwrapped = value.map do |v| + unwrap_string(v) end - unwrapped else value end @@ -1349,7 +1343,7 @@ def escape_quotes(text) # With multiple methods which need to discover secrets it is necessary to keep a single regex # which can discover them. This will lazily match everything in a single-quoted string which # ends with the secret postfix id and mark the actual contents of the string as the secret. - SECRET_DATA_REGEX = /'(?[^']+)+?#{Regexp.quote(SECRET_POSTFIX)}'/ + SECRET_DATA_REGEX = %r{'(?[^']+)+?#{Regexp.quote(SECRET_POSTFIX)}'} # Strings containing sensitive data have a secrets postfix. These strings cannot be passed # directly either to debug streams or to PowerShell and must be handled; this method contains @@ -1362,12 +1356,12 @@ def escape_quotes(text) def handle_secrets(text, replacement, error_message) # Every secret unwrapped in this module will unwrap as "'secret#{SECRET_POSTFIX}'" # Currently, no known resources specify a SecureString instead of a PSCredential object. - return text unless /#{Regexp.quote(SECRET_POSTFIX)}/.match?(text) + return text unless %r{#{Regexp.quote(SECRET_POSTFIX)}}.match?(text) # In order to reduce time-to-parse, look at each line individually and *only* attempt # to substitute if a naive match for the secret postfix is found on the line. modified_text = text.split("\n").map do |line| - if /#{Regexp.quote(SECRET_POSTFIX)}/.match?(line) + if %r{#{Regexp.quote(SECRET_POSTFIX)}}.match?(line) line.gsub(SECRET_DATA_REGEX, replacement) else line @@ -1377,7 +1371,7 @@ def handle_secrets(text, replacement, error_message) modified_text = modified_text.join("\n") # Something has gone wrong, error loudly - raise error_message if /#{Regexp.quote(SECRET_POSTFIX)}/.match?(modified_text) + raise error_message if %r{#{Regexp.quote(SECRET_POSTFIX)}}.match?(modified_text) modified_text end diff --git a/lib/pwsh.rb b/lib/pwsh.rb index 67a63f3c..e19484be 100644 --- a/lib/pwsh.rb +++ b/lib/pwsh.rb @@ -34,7 +34,7 @@ def self.instances def self.default_options { debug: false, - pipe_timeout: 30 + pipe_timeout: 30, } end @@ -280,7 +280,7 @@ def make_ps_code(powershell_code, timeout_ms = nil, working_dir = nil, environme if (envlist = environment_variables) envlist = [envlist] unless envlist.is_a? Array envlist.each do |setting| - if setting =~ /^(\w+)=((.|\n)+)$/ + if setting =~ %r{^(\w+)=((.|\n)+)$} env_name = Regexp.last_match(1) value = Regexp.last_match(2) if environment.include?(env_name) || environment.include?(env_name.to_sym) @@ -489,7 +489,7 @@ def pipe_command(command) def length_prefixed_string(data) msg = data.encode(Encoding::UTF_8) # https://ruby-doc.org/core-1.9.3/Array.html#method-i-pack - [msg.bytes.length].pack('V') + msg.force_encoding(Encoding::BINARY) + [msg.bytesize].pack('V') + msg.force_encoding(Encoding::BINARY) end # Writes binary-encoded data to the PowerShell manager process via the pipe. @@ -511,7 +511,7 @@ def write_pipe(input) # @param timeout [Float] The number of seconds to wait for the pipe to be readable # @yield [String] a binary encoded string chunk # @return nil - def read_from_pipe(pipe, timeout = 0.1, &_block) + def read_from_pipe(pipe, timeout = 0.1, &) if self.class.readable?(pipe, timeout) l = pipe.readpartial(4096) # TODO: Log a debug for "#{Time.now} PIPE> #{l}" @@ -538,7 +538,7 @@ def drain_pipe_until_signaled(pipe, signal) read_from_pipe(pipe, 0) { |s| output << s } while self.class.readable?(pipe) # String has been binary up to this point, so force UTF-8 now - output == [] ? [] : [output.join.force_encoding(Encoding::UTF_8)] + (output == []) ? [] : [output.join.force_encoding(Encoding::UTF_8)] end # Open threads and pipes to read stdout and stderr from the PowerShell manager, @@ -589,8 +589,8 @@ def read_streams [ output, - stdout == [] ? nil : stdout.join, # native stdout - stderr_reader.value # native stderr + (stdout == []) ? nil : stdout.join, # native stdout + stderr_reader.value, # native stderr ] ensure # Failsafe if the prior unlock was never reached / Mutex wasn't unlocked diff --git a/lib/pwsh/util.rb b/lib/pwsh/util.rb index 21b01710..21b5868a 100644 --- a/lib/pwsh/util.rb +++ b/lib/pwsh/util.rb @@ -1,141 +1,139 @@ # frozen_string_literal: true # Manage PowerShell and Windows PowerShell via ruby -module Pwsh - # Various helper methods - module Util - module_function - - # Verifies whether or not the current context is running on a Windows node. - # Implementation copied from `facets`: https://github.com/rubyworks/facets/blob/main/lib/standard/facets/rbconfig.rb - # - # @return [Bool] true if on windows - def on_windows? - host_os = RbConfig::CONFIG['host_os'] - !!(host_os =~ /mswin|mingw/) - end +# Various helper methods +module Pwsh::Util + module_function + + # Verifies whether or not the current context is running on a Windows node. + # Implementation copied from `facets`: https://github.com/rubyworks/facets/blob/main/lib/standard/facets/rbconfig.rb + # + # @return [Bool] true if on windows + def on_windows? + host_os = RbConfig::CONFIG['host_os'] + !!(host_os =~ %r{mswin|mingw}) + end - # Verify paths specified are valid directories. - # Skips paths which do not exist. - # @return [Bool] true if any paths specified are not valid directories - def invalid_directories?(path_collection) - return false if path_collection.nil? || path_collection.empty? + # Verify paths specified are valid directories. + # Skips paths which do not exist. + # @return [Bool] true if any paths specified are not valid directories + def invalid_directories?(path_collection) + return false if path_collection.nil? || path_collection.empty? - delimiter = on_windows? ? ';' : ':' - paths = path_collection.split(delimiter) + delimiter = on_windows? ? ';' : ':' + paths = path_collection.split(delimiter) - paths.any? { |path| !path.empty? && File.exist?(path) && !File.directory?(path) } - end + paths.any? { |path| !path.empty? && File.exist?(path) && !File.directory?(path) } + end - # Return a string or symbol converted to snake_case - # - # @return [String] snake_cased string - def snake_case(object) - # Implementation copied from: https://github.com/rubyworks/facets/blob/master/lib/core/facets/string/snakecase.rb - # gsub(/::/, '/'). - should_symbolize = object.is_a?(Symbol) - raise "snake_case method only handles strings and symbols, passed a #{object.class}: #{object}" unless should_symbolize || object.is_a?(String) - - text = object.to_s - .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') - .gsub(/([a-z\d])([A-Z])/, '\1_\2') - .tr('-', '_') - .gsub(/\s/, '_') - .gsub(/__+/, '_') - .downcase - should_symbolize ? text.to_sym : text - end + # Return a string or symbol converted to snake_case + # + # @return [String] snake_cased string + def snake_case(object) + # Implementation copied from: https://github.com/rubyworks/facets/blob/master/lib/core/facets/string/snakecase.rb + # gsub(/::/, '/'). + should_symbolize = object.is_a?(Symbol) + raise "snake_case method only handles strings and symbols, passed a #{object.class}: #{object}" unless should_symbolize || object.is_a?(String) + + text = object.to_s + .gsub(%r{([A-Z]+)([A-Z][a-z])}, '\1_\2') + .gsub(%r{([a-z\d])([A-Z])}, '\1_\2') + .tr('-', '_') + .gsub(%r{\s}, '_') + .gsub(%r{__+}, '_') + .downcase + should_symbolize ? text.to_sym : text + end - # Iterate through a hashes keys, snake_casing them - # - # @return [Hash] Hash with all keys snake_cased - def snake_case_hash_keys(object) - snake_case_proc = proc { |key| snake_case(key) } - apply_key_mutator(object, snake_case_proc) - end + # Iterate through a hashes keys, snake_casing them + # + # @return [Hash] Hash with all keys snake_cased + def snake_case_hash_keys(object) + snake_case_proc = proc { |key| snake_case(key) } + apply_key_mutator(object, snake_case_proc) + end - # Return a string or symbol converted to PascalCase - # - # @return [String] PascalCased string - def pascal_case(object) - should_symbolize = object.is_a?(Symbol) - raise "snake_case method only handles strings and symbols, passed a #{object.class}: #{object}" unless should_symbolize || object.is_a?(String) + # Return a string or symbol converted to PascalCase + # + # @return [String] PascalCased string + def pascal_case(object) + should_symbolize = object.is_a?(Symbol) + raise "snake_case method only handles strings and symbols, passed a #{object.class}: #{object}" unless should_symbolize || object.is_a?(String) - # Break word boundaries to snake case first - text = snake_case(object.to_s).split('_').collect(&:capitalize).join - should_symbolize ? text.to_sym : text - end + # Break word boundaries to snake case first + text = snake_case(object.to_s).split('_').collect(&:capitalize).join + should_symbolize ? text.to_sym : text + end - # Iterate through a hashes keys, PascalCasing them - # - # @return [Hash] Hash with all keys PascalCased - def pascal_case_hash_keys(object) - pascal_case_proc = proc { |key| pascal_case(key) } - apply_key_mutator(object, pascal_case_proc) - end + # Iterate through a hashes keys, PascalCasing them + # + # @return [Hash] Hash with all keys PascalCased + def pascal_case_hash_keys(object) + pascal_case_proc = proc { |key| pascal_case(key) } + apply_key_mutator(object, pascal_case_proc) + end - # Ensure that quotes inside a passed string will continue to be passed - # - # @return [String] the string with quotes escaped - def escape_quotes(text) - text.gsub("'", "''") - end + # Ensure that quotes inside a passed string will continue to be passed + # + # @return [String] the string with quotes escaped + def escape_quotes(text) + text.gsub("'", "''") + end - # Ensure that all keys in a hash are symbols, not strings. - # - # @return [Hash] a hash whose keys have been converted to symbols. - def symbolize_hash_keys(object) - symbolize_proc = proc(&:to_sym) - apply_key_mutator(object, symbolize_proc) - end + # Ensure that all keys in a hash are symbols, not strings. + # + # @return [Hash] a hash whose keys have been converted to symbols. + def symbolize_hash_keys(object) + symbolize_proc = proc(&:to_sym) + apply_key_mutator(object, symbolize_proc) + end - def apply_key_mutator(object, proc) - return object.map { |item| apply_key_mutator(item, proc) } if object.is_a?(Array) - return object unless object.is_a?(Hash) + def apply_key_mutator(object, proc) + return object.map { |item| apply_key_mutator(item, proc) } if object.is_a?(Array) + return object unless object.is_a?(Hash) - modified_hash = {} - object.each do |key, value| - modified_hash[proc.call(key)] = apply_key_mutator(value, proc) - end - modified_hash + modified_hash = {} + object.each do |key, value| + modified_hash[proc.call(key)] = apply_key_mutator(value, proc) end + modified_hash + end - private_class_method :apply_key_mutator - - # Convert a ruby value into a string to be passed along to PowerShell for interpolation in a command - # Handles: - # - Strings - # - Numbers - # - Booleans - # - Symbols - # - Arrays - # - Hashes - # - # @return [String] representation of the value for interpolation - def format_powershell_value(object) - if %i[true false].include?(object) || %w[trueclass falseclass].include?(object.class.name.downcase) - "$#{object}" - elsif object.instance_of?(Symbol) || object.class.ancestors.include?(Numeric) - object.to_s - elsif object.instance_of?(String) - "'#{escape_quotes(object)}'" - elsif object.instance_of?(Array) - "@(#{object.collect { |item| format_powershell_value(item) }.join(', ')})" - elsif object.instance_of?(Hash) - "@{#{object.collect { |k, v| "#{format_powershell_value(k)} = #{format_powershell_value(v)}" }.join('; ')}}" - else - raise "unsupported type #{object.class} of value '#{object}'" - end + private_class_method :apply_key_mutator + + # Convert a ruby value into a string to be passed along to PowerShell for interpolation in a command + # Handles: + # - Strings + # - Numbers + # - Booleans + # - Symbols + # - Arrays + # - Hashes + # + # @return [String] representation of the value for interpolation + def format_powershell_value(object) + if [true, false].include?(object) || ['trueclass', 'falseclass'].include?(object.class.name.downcase) + "$#{object}" + elsif object.instance_of?(Symbol) || object.class.ancestors.include?(Numeric) + object.to_s + elsif object.instance_of?(String) + "'#{escape_quotes(object)}'" + elsif object.instance_of?(Array) + "@(#{object.collect { |item| format_powershell_value(item) }.join(', ')})" + elsif object.instance_of?(Hash) + "@{#{object.collect { |k, v| "#{format_powershell_value(k)} = #{format_powershell_value(v)}" }.join('; ')}}" + else + raise "unsupported type #{object.class} of value '#{object}'" end + end - # Return the representative string of a PowerShell hash for a custom object property to be used in selecting or filtering. - # The script block for the expression must be passed as the string you want interpolated into the hash; this method does - # not do any of the additional work of interpolation for you as the type sits inside a code block inside a hash. - # - # @return [String] representation of a PowerShell hash with the keys 'Name' and 'Expression' - def custom_powershell_property(name, expression) - "@{Name = '#{name}'; Expression = {#{expression}}}" - end + # Return the representative string of a PowerShell hash for a custom object property to be used in selecting or filtering. + # The script block for the expression must be passed as the string you want interpolated into the hash; this method does + # not do any of the additional work of interpolation for you as the type sits inside a code block inside a hash. + # + # @return [String] representation of a PowerShell hash with the keys 'Name' and 'Expression' + def custom_powershell_property(name, expression) + "@{Name = '#{name}'; Expression = {#{expression}}}" end end diff --git a/lib/pwsh/windows_powershell.rb b/lib/pwsh/windows_powershell.rb index e39b92bc..d21b255e 100644 --- a/lib/pwsh/windows_powershell.rb +++ b/lib/pwsh/windows_powershell.rb @@ -2,107 +2,103 @@ require File.join(File.dirname(__FILE__), 'util') -module Pwsh - # Returns information about the available versions of Windows PowerShell on the node, if any. - class WindowsPowerShell - # Return whether or not the latest version of PowerShell available on the machine - # is compatible with the implementation of the Manager. - def self.compatible_version? - # If this method isn't defined, we're not on Windows! - return false if defined?(Pwsh::WindowsPowerShell.version).nil? +# Returns information about the available versions of Windows PowerShell on the node, if any. +class Pwsh::WindowsPowerShell + # Return whether or not the latest version of PowerShell available on the machine + # is compatible with the implementation of the Manager. + def self.compatible_version? + # If this method isn't defined, we're not on Windows! + return false if defined?(Pwsh::WindowsPowerShell.version).nil? - powershell_version = defined?(Pwsh::WindowsPowerShell.version) ? Pwsh::WindowsPowerShell.version : nil + powershell_version = defined?(Pwsh::WindowsPowerShell.version) ? Pwsh::WindowsPowerShell.version : nil - # If we get nil, something's gone wrong and we're not compatible. - return false if powershell_version.nil? + # If we get nil, something's gone wrong and we're not compatible. + return false if powershell_version.nil? - # PowerShell v1 - definitely not good to go. Really the whole library - # may not even work but I digress - return false if Gem::Version.new(powershell_version) < Gem::Version.new(2) + # PowerShell v1 - definitely not good to go. Really the whole library + # may not even work but I digress + return false if Gem::Version.new(powershell_version) < Gem::Version.new(2) - # PowerShell v3+, we are good to go b/c .NET 4+ - # https://msdn.microsoft.com/en-us/powershell/scripting/setup/windows-powershell-system-requirements - # Look at Microsoft .NET Framwork Requirements section. - return true if Gem::Version.new(powershell_version) >= Gem::Version.new(3) + # PowerShell v3+, we are good to go b/c .NET 4+ + # https://msdn.microsoft.com/en-us/powershell/scripting/setup/windows-powershell-system-requirements + # Look at Microsoft .NET Framwork Requirements section. + return true if Gem::Version.new(powershell_version) >= Gem::Version.new(3) - # If we are using PowerShell v2, we need to see what the latest - # version of .NET is that we have - # https://msdn.microsoft.com/en-us/library/hh925568.aspx - value = false - if Pwsh::Util.on_windows? - require 'win32/registry' - begin - # At this point in the check, PowerShell is using .NET Framework - # 2.x family, so we only need to verify v3.5 key exists. - # If we were verifying all compatible types we would look for - # any of these keys: v3.5, v4.0, v4 - Win32::Registry::HKEY_LOCAL_MACHINE.open('SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5', Win32::Registry::KEY_READ | 0x100) do - value = true - end - rescue Win32::Registry::Error - value = false + # If we are using PowerShell v2, we need to see what the latest + # version of .NET is that we have + # https://msdn.microsoft.com/en-us/library/hh925568.aspx + value = false + if Pwsh::Util.on_windows? + require 'win32/registry' + begin + # At this point in the check, PowerShell is using .NET Framework + # 2.x family, so we only need to verify v3.5 key exists. + # If we were verifying all compatible types we would look for + # any of these keys: v3.5, v4.0, v4 + Win32::Registry::HKEY_LOCAL_MACHINE.open('SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5', Win32::Registry::KEY_READ | 0x100) do + value = true end + rescue Win32::Registry::Error + value = false end - - value end + + value end end if Pwsh::Util.on_windows? require 'win32/registry' - module Pwsh - # Returns information about the available versions of Windows PowerShell on the node, if any. - class WindowsPowerShell - # Shorthand constant to reference the registry key access type - ACCESS_TYPE = Win32::Registry::KEY_READ | 0x100 - # Shorthand constant to reference the local machine hive - HKLM = Win32::Registry::HKEY_LOCAL_MACHINE - # The path to the original version of the Windows PowerShell Engine's data in registry - PS_ONE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine' - # The path to the newer version of the Windows PowerShell Engine's data in registry - PS_THREE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine' - # The name of the registry key for looking up the latest version of Windows PowerShell for a given engine. - REG_KEY = 'PowerShellVersion' + # Returns information about the available versions of Windows PowerShell on the node, if any. + class Pwsh::WindowsPowerShell + # Shorthand constant to reference the registry key access type + ACCESS_TYPE = Win32::Registry::KEY_READ | 0x100 + # Shorthand constant to reference the local machine hive + HKLM = Win32::Registry::HKEY_LOCAL_MACHINE + # The path to the original version of the Windows PowerShell Engine's data in registry + PS_ONE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine' + # The path to the newer version of the Windows PowerShell Engine's data in registry + PS_THREE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine' + # The name of the registry key for looking up the latest version of Windows PowerShell for a given engine. + REG_KEY = 'PowerShellVersion' - # Returns the latest available version of Windows PowerShell on the machine - # - # @return [String] a version string representing the latest version of Windows PowerShell available - def self.version - powershell_three_version || powershell_one_version - end + # Returns the latest available version of Windows PowerShell on the machine + # + # @return [String] a version string representing the latest version of Windows PowerShell available + def self.version + powershell_three_version || powershell_one_version + end - # Returns the latest available version of Windows PowerShell using the older - # engine as determined by checking the registry. - # - # @return [String] a version string representing the latest version of Windows PowerShell using the original engine - def self.powershell_one_version - version = nil - begin - HKLM.open(PS_ONE_REG_PATH, ACCESS_TYPE) do |reg| - version = reg[REG_KEY] - end - rescue StandardError - version = nil + # Returns the latest available version of Windows PowerShell using the older + # engine as determined by checking the registry. + # + # @return [String] a version string representing the latest version of Windows PowerShell using the original engine + def self.powershell_one_version + version = nil + begin + HKLM.open(PS_ONE_REG_PATH, ACCESS_TYPE) do |reg| + version = reg[REG_KEY] end - version + rescue StandardError + version = nil end + version + end - # Returns the latest available version of Windows PowerShell as determined by - # checking the registry. - # - # @return [String] a version string representing the latest version of Windows PowerShell using the newer engine - def self.powershell_three_version - version = nil - begin - HKLM.open(PS_THREE_REG_PATH, ACCESS_TYPE) do |reg| - version = reg[REG_KEY] - end - rescue StandardError - version = nil + # Returns the latest available version of Windows PowerShell as determined by + # checking the registry. + # + # @return [String] a version string representing the latest version of Windows PowerShell using the newer engine + def self.powershell_three_version + version = nil + begin + HKLM.open(PS_THREE_REG_PATH, ACCESS_TYPE) do |reg| + version = reg[REG_KEY] end - version + rescue StandardError + version = nil end + version end end end diff --git a/spec/acceptance/dsc/basic.rb b/spec/acceptance/dsc/basic.rb index 47669175..b1281af1 100644 --- a/spec/acceptance/dsc/basic.rb +++ b/spec/acceptance/dsc/basic.rb @@ -31,7 +31,7 @@ def execute_reset_command(reset_command) "dsc_name => 'PSGallery',", "dsc_ensure => 'Present',", "dsc_installationpolicy => 'Trusted'", - '}' + '}', ].join(' ') end @@ -57,9 +57,9 @@ def execute_reset_command(reset_command) it 'applies idempotently' do first_run_result = powershell.execute(command) expect(first_run_result[:exitcode]).to be(2) - expect(first_run_result[:native_stdout]).to match(/dsc_installationpolicy changed 'Untrusted' to 'Trusted'/) - expect(first_run_result[:native_stdout]).to match(/Updating: Finished/) - expect(first_run_result[:native_stdout]).to match(/Applied catalog/) + expect(first_run_result[:native_stdout]).to match(%r{dsc_installationpolicy changed 'Untrusted' to 'Trusted'}) + expect(first_run_result[:native_stdout]).to match(%r{Updating: Finished}) + expect(first_run_result[:native_stdout]).to match(%r{Applied catalog}) second_run_result = powershell.execute(command) expect(second_run_result[:exitcode]).to be(0) end @@ -71,7 +71,7 @@ def execute_reset_command(reset_command) "dsc_psmodule { 'Install BurntToast':", "dsc_name => 'BurntToast',", "dsc_ensure => 'Present',", - '}' + '}', ].join(' ') end @@ -88,9 +88,9 @@ def execute_reset_command(reset_command) it 'applies idempotently' do first_run_result = powershell.execute(command) expect(first_run_result[:exitcode]).to be(2) - expect(first_run_result[:native_stdout]).to match(/dsc_ensure changed 'Absent' to 'Present'/) - expect(first_run_result[:native_stdout]).to match(/Creating: Finished/) - expect(first_run_result[:native_stdout]).to match(/Applied catalog/) + expect(first_run_result[:native_stdout]).to match(%r{dsc_ensure changed 'Absent' to 'Present'}) + expect(first_run_result[:native_stdout]).to match(%r{Creating: Finished}) + expect(first_run_result[:native_stdout]).to match(%r{Applied catalog}) second_run_result = powershell.execute(command) expect(second_run_result[:exitcode]).to be(0) end @@ -102,7 +102,7 @@ def execute_reset_command(reset_command) "dsc_psmodule { 'Install BurntToast':", "dsc_name => 'BurntToast',", "dsc_ensure => 'Absent',", - '}' + '}', ].join(' ') end @@ -121,9 +121,9 @@ def execute_reset_command(reset_command) it 'applies idempotently' do first_run_result = powershell.execute(command) expect(first_run_result[:exitcode]).to be(2) - expect(first_run_result[:native_stdout]).to match(/dsc_ensure changed 'Present' to 'Absent'/) - expect(first_run_result[:native_stdout]).to match(/Deleting: Finished/) - expect(first_run_result[:native_stdout]).to match(/Applied catalog/) + expect(first_run_result[:native_stdout]).to match(%r{dsc_ensure changed 'Present' to 'Absent'}) + expect(first_run_result[:native_stdout]).to match(%r{Deleting: Finished}) + expect(first_run_result[:native_stdout]).to match(%r{Applied catalog}) second_run_result = powershell.execute(command) expect(second_run_result[:exitcode]).to be(0) end @@ -165,17 +165,17 @@ def execute_reset_command(reset_command) "'user' => '#{local_user}',", "'password' => Sensitive('#{local_pw}')", '}', - '}' + '}', ].join(' ') end it 'applies idempotently without leaking secrets' do first_run_result = powershell.execute(command) expect(first_run_result[:exitcode]).to be(2) - expect(first_run_result[:native_stdout]).to match(/dsc_installationpolicy changed 'Untrusted' to 'Trusted'/) - expect(first_run_result[:native_stdout]).to match(/Updating: Finished/) - expect(first_run_result[:native_stdout]).to match(/Applied catalog/) - expect(first_run_result[:native_stdout]).to match(/'#'/) + expect(first_run_result[:native_stdout]).to match(%r{dsc_installationpolicy changed 'Untrusted' to 'Trusted'}) + expect(first_run_result[:native_stdout]).to match(%r{Updating: Finished}) + expect(first_run_result[:native_stdout]).to match(%r{Applied catalog}) + expect(first_run_result[:native_stdout]).to match(%r{'#'}) expect(first_run_result[:native_stdout]).not_to match(local_pw) second_run_result = powershell.execute(command) expect(second_run_result[:exitcode]).to be(0) @@ -193,15 +193,15 @@ def execute_reset_command(reset_command) "'user' => 'definitely_do_not_exist_here',", "'password' => Sensitive('#{local_pw}')", '}', - '}' + '}', ].join(' ') end it 'errors loudly without leaking secrets' do first_run_result = powershell.execute(command) expect(first_run_result[:exitcode]).to be(4) - expect(first_run_result[:stderr].first).to match(/dsc_psrepository: The user name or password is incorrect/) - expect(first_run_result[:native_stdout]).to match(/'#'/) + expect(first_run_result[:stderr].first).to match(%r{dsc_psrepository: The user name or password is incorrect}) + expect(first_run_result[:native_stdout]).to match(%r{'#'}) expect(first_run_result[:native_stdout]).not_to match(local_pw) end end diff --git a/spec/acceptance/dsc/cim_instances.rb b/spec/acceptance/dsc/cim_instances.rb index 99f656dc..a94c2921 100644 --- a/spec/acceptance/dsc/cim_instances.rb +++ b/spec/acceptance/dsc/cim_instances.rb @@ -25,7 +25,7 @@ def execute_reset_command(reset_command) end context 'Managing the access control list of a folder' do - before do + before(:each) do reset_command = <<~RESET_COMMAND $TestFolderPath = Join-Path -Path "#{fixtures_path}" -Childpath access_control # Delete the test folder if it exists (to clear access control modifications) @@ -66,13 +66,13 @@ def execute_reset_command(reset_command) first_run_result = powershell.execute(puppet_apply) expect(first_run_result[:exitcode]).to be(2) # Access Control Set - expect(first_run_result[:native_stdout]).to match(/dsc_accesscontrollist: dsc_accesscontrollist changed/) + expect(first_run_result[:native_stdout]).to match(%r{dsc_accesscontrollist: dsc_accesscontrollist changed}) expect(first_run_result[:native_stdout]).to match(%r{dsc_ntfsaccessentry\[\{:?name(?:=>|: )"Test", :?dsc_path(?:=>|: )".+/spec/fixtures/access_control"\}\]: Updating: Finished}) - expect(first_run_result[:stderr]).not_to match(/Error/) - expect(first_run_result[:stderr]).not_to match(/Warning: Provider returned data that does not match the Type Schema/) - expect(first_run_result[:stderr]).not_to match(/Value type mismatch/) + expect(first_run_result[:stderr]).not_to match(%r{Error}) + expect(first_run_result[:stderr]).not_to match(%r{Warning: Provider returned data that does not match the Type Schema}) + expect(first_run_result[:stderr]).not_to match(%r{Value type mismatch}) # Run finished - expect(first_run_result[:native_stdout]).to match(/Applied catalog/) + expect(first_run_result[:native_stdout]).to match(%r{Applied catalog}) # Second run is idempotent second_run_result = powershell.execute(puppet_apply) expect(second_run_result[:exitcode]).to be(0) diff --git a/spec/acceptance/dsc/class.rb b/spec/acceptance/dsc/class.rb index 187dfb88..333f5dec 100644 --- a/spec/acceptance/dsc/class.rb +++ b/spec/acceptance/dsc/class.rb @@ -28,11 +28,11 @@ def execute_reset_command(reset_command) "dsc_ensure => 'Present',", "dsc_path => '#{psrc_path}',", "dsc_description => 'Example role capability file'", - '}' + '}', ].join(' ') end - before do + before(:each) do reset_command = <<~RESET_COMMAND $PsrcPath = '#{psrc_path}' # Delete the test PSRC fixture if it exists @@ -46,10 +46,10 @@ def execute_reset_command(reset_command) it 'applies idempotently' do first_run_result = powershell.execute(command) expect(first_run_result[:exitcode]).to be(2) - expect(first_run_result[:native_stdout]).to match(//) - expect(first_run_result[:native_stdout]).to match(/dsc_description changed.*to 'Example role capability file'/) - expect(first_run_result[:native_stdout]).to match(/Creating: Finished/) - expect(first_run_result[:native_stdout]).to match(/Applied catalog/) + expect(first_run_result[:native_stdout]).to match(%r{}) + expect(first_run_result[:native_stdout]).to match(%r{dsc_description changed.*to 'Example role capability file'}) + expect(first_run_result[:native_stdout]).to match(%r{Creating: Finished}) + expect(first_run_result[:native_stdout]).to match(%r{Applied catalog}) second_run_result = powershell.execute(command) expect(second_run_result[:exitcode]).to be(0) end @@ -64,11 +64,11 @@ def execute_reset_command(reset_command) "dsc_ensure => 'Present',", "dsc_path => '#{psrc_path}',", "dsc_description => 'Updated role capability file'", - '}' + '}', ].join(' ') end - before do + before(:each) do reset_command = <<~RESET_COMMAND $PsrcPath = '#{psrc_path}' # Delete the test PSRC fixture if it exists @@ -84,9 +84,9 @@ def execute_reset_command(reset_command) it 'applies idempotently' do first_run_result = powershell.execute(command) expect(first_run_result[:exitcode]).to be(2) - expect(first_run_result[:native_stdout]).to match(/dsc_description changed 'Example role capability file' to 'Updated role capability file'/) - expect(first_run_result[:native_stdout]).to match(/Updating: Finished/) - expect(first_run_result[:native_stdout]).to match(/Applied catalog/) + expect(first_run_result[:native_stdout]).to match(%r{dsc_description changed 'Example role capability file' to 'Updated role capability file'}) + expect(first_run_result[:native_stdout]).to match(%r{Updating: Finished}) + expect(first_run_result[:native_stdout]).to match(%r{Applied catalog}) second_run_result = powershell.execute(command) expect(second_run_result[:exitcode]).to be(0) end @@ -100,11 +100,11 @@ def execute_reset_command(reset_command) "dsc_jearolecapabilities { 'ExampleRoleCapability':", "dsc_ensure => 'Absent',", "dsc_path => '#{psrc_path}'", - '}' + '}', ].join(' ') end - before do + before(:each) do reset_command = <<~RESET_COMMAND $PsrcPath = '#{psrc_path}' # Delete the test PSRC fixture if it exists @@ -119,9 +119,9 @@ def execute_reset_command(reset_command) it 'applies idempotently' do first_run_result = powershell.execute(command) expect(first_run_result[:exitcode]).to be(2) - expect(first_run_result[:native_stdout]).to match(/dsc_ensure changed 'Present' to 'Absent'/) - expect(first_run_result[:native_stdout]).to match(/Deleting: Finished/) - expect(first_run_result[:native_stdout]).to match(/Applied catalog/) + expect(first_run_result[:native_stdout]).to match(%r{dsc_ensure changed 'Present' to 'Absent'}) + expect(first_run_result[:native_stdout]).to match(%r{Deleting: Finished}) + expect(first_run_result[:native_stdout]).to match(%r{Applied catalog}) second_run_result = powershell.execute(command) expect(second_run_result[:exitcode]).to be(0) end diff --git a/spec/acceptance/dsc/complex.rb b/spec/acceptance/dsc/complex.rb index 81614556..07f4304b 100644 --- a/spec/acceptance/dsc/complex.rb +++ b/spec/acceptance/dsc/complex.rb @@ -21,7 +21,7 @@ def execute_reset_command(reset_command) end context 'Adding a new website' do - before do + before(:each) do reset_command = <<~RESET_COMMAND # Ensure IIS is not installed $Feature = Get-WindowsFeature -Name 'Web-Asp-Net45' @@ -114,10 +114,10 @@ def execute_reset_command(reset_command) expect(first_run_result[:exitcode]).to be(2) # The Default Site is stopped expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwebsite\[DefaultSite\]/dsc_state: dsc_state changed 'Started' to 'Stopped'}) - expect(first_run_result[:native_stdout]).to match(/dsc_xwebsite\[\{:?name(?:=>|: )"DefaultSite", :?dsc_name(?:=>|: )"Default Web Site"\}\]: Updating: Finished/) + expect(first_run_result[:native_stdout]).to match(%r{dsc_xwebsite\[\{:?name(?:=>|: )"DefaultSite", :?dsc_name(?:=>|: )"Default Web Site"\}\]: Updating: Finished}) # AspNet45 is installed expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwindowsfeature\[AspNet45\]/dsc_ensure: dsc_ensure changed 'Absent' to 'Present'}) - expect(first_run_result[:native_stdout]).to match(/dsc_xwindowsfeature\[\{:?name(?:=>|: )"AspNet45", :?dsc_name(?:=>|: )"Web-Asp-Net45"\}\]: Creating: Finished/) + expect(first_run_result[:native_stdout]).to match(%r{dsc_xwindowsfeature\[\{:?name(?:=>|: )"AspNet45", :?dsc_name(?:=>|: )"Web-Asp-Net45"\}\]: Creating: Finished}) # Web content folder created expect(first_run_result[:native_stdout]).to match(%r{File\[WebContentFolder\]/ensure: created}) # Web content index created @@ -128,9 +128,9 @@ def execute_reset_command(reset_command) expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwebsite\[NewWebsite\]/dsc_physicalpath: dsc_physicalpath changed.*to '.+fixtures/website'}) expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwebsite\[NewWebsite\]/dsc_state: dsc_state changed.*to 'Started'}) expect(first_run_result[:native_stdout]).to match(%r{Dsc_xwebsite\[NewWebsite\]/dsc_serverautostart: dsc_serverautostart changed.*to 'true'}) - expect(first_run_result[:native_stdout]).to match(/dsc_xwebsite\[\{:?name(?:=>|: )"NewWebsite", :?dsc_name(?:=>|: )"Puppet DSC Site"\}\]: Creating: Finished/) + expect(first_run_result[:native_stdout]).to match(%r{dsc_xwebsite\[\{:?name(?:=>|: )"NewWebsite", :?dsc_name(?:=>|: )"Puppet DSC Site"\}\]: Creating: Finished}) # Run finished - expect(first_run_result[:native_stdout]).to match(/Applied catalog/) + expect(first_run_result[:native_stdout]).to match(%r{Applied catalog}) # Second run is idempotent second_run_result = powershell.execute(puppet_apply) expect(second_run_result[:exitcode]).to be(0) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8e90ba42..baaf7d9b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,7 +7,7 @@ SimpleCov.formatters = [ SimpleCov::Formatter::HTMLFormatter, - SimpleCov::Formatter::Console + SimpleCov::Formatter::Console, ] SimpleCov.start do diff --git a/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb b/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb index f9f13d0f..f8841381 100644 --- a/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb +++ b/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb @@ -15,7 +15,7 @@ let(:execute_response) { { stdout: nil, stderr: nil, exitcode: 0 } } # Reset the caches after each run - after do + after(:each) do provider.instance_variable_set(:@cached_canonicalized_resource, []) provider.instance_variable_set(:@cached_query_results, []) provider.instance_variable_set(:@cached_test_results, []) @@ -26,7 +26,7 @@ end describe '.initialize' do - before do + before(:each) do # Need to initialize the provider to load the instance variables provider end @@ -61,9 +61,9 @@ end describe '.cached_test_results' do - let(:cache_value) { %w[foo bar] } + let(:cache_value) { ['foo', 'bar'] } - before do + before(:each) do provider.instance_variable_set(:@cached_test_results, cache_value) end @@ -95,12 +95,12 @@ subject(:canonicalized_resource) { provider.canonicalize(context, [manifest_resource]) } let(:resource_name_hash) { { name: 'foo', dsc_name: 'foo' } } - let(:namevar_keys) { %i[name dsc_name] } - let(:parameter_keys) { %i[dsc_parameter dsc_psdscrunascredential] } + let(:namevar_keys) { [:name, :dsc_name] } + let(:parameter_keys) { [:dsc_parameter, :dsc_psdscrunascredential] } let(:credential_hash) { { 'username' => 'foo', 'password' => 'bar' } } let(:base_resource) { resource_name_hash.dup } - before do + before(:each) do allow(context).to receive(:debug) allow(provider).to receive(:namevar_attributes).and_return(namevar_keys) allow(provider).to receive(:fetch_cached_hashes).and_return(cached_canonicalized_resource) @@ -129,7 +129,7 @@ context 'when a manifest resource not in the canonicalized resource cache' do let(:cached_canonicalized_resource) { [] } - before do + before(:each) do allow(provider).to receive(:invoke_get_method).and_return(actual_resource) end @@ -143,7 +143,7 @@ end context 'when invoke_get_method returns a resource' do - before do + before(:each) do allow(provider).to receive(:parameter_attributes).and_return(parameter_keys) allow(provider).to receive(:enum_attributes).and_return([]) end @@ -189,7 +189,7 @@ let(:manifest_resource) { base_resource.merge({ dsc_property: 'Dword' }) } let(:actual_resource) { base_resource.merge({ dsc_property: 'DWord' }) } - before do + before(:each) do allow(provider).to receive(:enum_attributes).and_return([:dsc_property]) end @@ -257,7 +257,7 @@ end describe '.get' do - after do + after(:each) do provider.instance_variable_set(:@cached_canonicalized_resource, []) end @@ -288,9 +288,9 @@ # Empty because we can mock everything but calling .keys on the hash let(:attributes) { { name: {}, dsc_name: {}, dsc_setting: {} } } - before do + before(:each) do allow(context).to receive(:type).and_return(type) - allow(type).to receive(:namevars).and_return(%i[name dsc_name]) + allow(type).to receive(:namevars).and_return([:name, :dsc_name]) allow(type).to receive(:attributes).and_return(attributes) end @@ -412,7 +412,7 @@ let(:should_hash) { { name: 'foo', dsc_setting: 'Foo', validation_mode: 'property' } } let(:fresh_state) { { name: 'foo', dsc_setting: 'Bar' } } - before do + before(:each) do allow(context).to receive(:debug) end @@ -444,7 +444,7 @@ end context 'when DSC Test says out of sync' do - before do + before(:each) do allow(provider).to receive(:fetch_cached_hashes).and_return([]) allow(provider).to receive(:invoke_test_method).and_return([false, 'not in desired state']) end @@ -455,7 +455,7 @@ # fresh_state has 'Bar', should_hash has 'Foo' — values differ expect(result).to be_an(Array) expect(result.first).to be false - expect(result.last).to match(/dsc_setting changed/) + expect(result.last).to match(%r{dsc_setting changed}) end it 'returns true for non-dsc_ properties (suppressed)' do @@ -568,7 +568,7 @@ end it 'returns true for arrays in different order' do - expect(provider.values_equal?(%w[a b c], %w[c b a])).to be true + expect(provider.values_equal?(['a', 'b', 'c'], ['c', 'b', 'a'])).to be true end it 'returns true for hashes with case-different keys/values' do @@ -589,7 +589,7 @@ let(:should_hash) { { name: 'foo', dsc_name: 'foo', dsc_setting: 'desired' } } let(:attributes) { { name: { type: 'String' }, dsc_name: { type: 'String', mandatory_for_get: true }, dsc_setting: { type: 'String' } } } - before do + before(:each) do allow(context).to receive(:debug) allow(context).to receive(:type).and_return(type) allow(type).to receive(:attributes).and_return(attributes) @@ -627,7 +627,7 @@ context 'when invoke_dsc_resource raises an error' do it 'returns nil and logs a debug message' do allow(provider).to receive(:invoke_dsc_resource).and_raise(StandardError, 'boom') - expect(context).to receive(:debug).with(/perform_fresh_get failed/) + expect(context).to receive(:debug).with(%r{perform_fresh_get failed}) expect(provider.perform_fresh_get(context, name, should_hash)).to be_nil end end @@ -638,7 +638,7 @@ let(:should_hash) { { name: 'foo', dsc_setting: 'desired' } } let(:fresh_result) { { name: 'foo', dsc_setting: 'current' } } - before do + before(:each) do allow(context).to receive(:debug) end @@ -666,7 +666,7 @@ { name: { type: 'String', - behaviour: :namevar + behaviour: :namevar, }, dsc_name: { type: 'String', @@ -674,7 +674,7 @@ mandatory_for_get: true, mandatory_for_set: true, mof_type: 'String', - mof_is_embedded: false + mof_is_embedded: false, }, dsc_psdscrunascredential: { type: 'Optional[Struct[{ user => String[1], password => Variant[String[1], Sensitive[String[1]]] }]]', @@ -682,21 +682,21 @@ mandatory_for_get: false, mandatory_for_set: false, mof_type: 'PSCredential', - mof_is_embedded: true + mof_is_embedded: true, }, dsc_ensure: { type: "Optional[Enum['Present', 'Absent']]", mandatory_for_get: false, mandatory_for_set: false, mof_type: 'String', - mof_is_embedded: false + mof_is_embedded: false, }, dsc_time: { type: 'Optional[Timestamp]', mandatory_for_get: false, mandatory_for_set: false, mof_type: 'DateTime', - mof_is_embedded: false + mof_is_embedded: false, }, dsc_ciminstance: { type: "Optional[Struct[{ @@ -706,7 +706,7 @@ mandatory_for_get: false, mandatory_for_set: false, mof_type: 'FooBarCimInstance', - mof_is_embedded: true + mof_is_embedded: true, }, dsc_nestedciminstance: { type: "Optional[Struct[{ @@ -720,14 +720,14 @@ mandatory_for_get: false, mandatory_for_set: false, mof_type: 'BazCimInstance', - mof_is_embedded: true + mof_is_embedded: true, }, dsc_array: { type: 'Optional[Array[String]]', mandatory_for_get: false, mandatory_for_set: false, mof_type: 'String[]', - mof_is_embedded: false + mof_is_embedded: false, }, dsc_param: { type: 'Optional[String]', @@ -735,12 +735,12 @@ mandatory_for_get: false, mandatory_for_set: false, mof_type: 'String', - mof_is_embedded: false - } + mof_is_embedded: false, + }, } end let(:name_hash) { { name: 'foo', dsc_name: 'foo', dsc_time: '2100-01-01' } } - let(:mandatory_get_attributes) { %i[dsc_name] } + let(:mandatory_get_attributes) { [:dsc_name] } let(:query_props) { { dsc_name: 'foo' } } let(:resource) { "Resource: #{query_props}" } let(:script) { "Script: #{query_props}" } @@ -752,16 +752,16 @@ 'CimInstance' => { 'Foo' => true, 'Bar' => 'Ope' }, 'NestedCimInstance' => { 'Baz' => true, - 'NestedProperty' => { 'NestedFoo' => 'yay', 'NestedBar' => 'Ope', 'cim_instance_type' => 'FooBarNestedCimINstance' } + 'NestedProperty' => { 'NestedFoo' => 'yay', 'NestedBar' => 'Ope', 'cim_instance_type' => 'FooBarNestedCimINstance' }, }, - 'Array' => %w[foo bar], + 'Array' => ['foo', 'bar'], 'EmptyArray' => nil, 'Param' => 'Value', - 'UnusedProperty' => 'foo' + 'UnusedProperty' => 'foo', } end - before do + before(:each) do allow(context).to receive(:debug) allow(provider).to receive(:mandatory_get_attributes).and_return(mandatory_get_attributes) allow(provider).to receive(:invocable_resource).with(query_props, context, 'get').and_return(resource) @@ -773,12 +773,12 @@ allow(type).to receive(:attributes).and_return(attributes) end - after do + after(:each) do provider.instance_variable_set(:@cached_query_results, []) end context 'when the invocation script returns data without errors' do - before do + before(:each) do allow(ps_manager).to receive(:execute).with(script, nil).and_return({ stdout: 'DSC Data' }) allow(JSON).to receive(:parse).with('DSC Data').and_return(parsed_invocation_data) allow(Puppet::Pops::Time::Timestamp).to receive(:parse).with('2100-01-01').and_return('TimeStamp:2100-01-01') @@ -819,27 +819,27 @@ end it 'downcases keys in cim instance properties' do - expect(result[:dsc_nestedciminstance].keys).to eq(%w[baz nestedproperty]) - expect(result[:dsc_nestedciminstance]['nestedproperty'].keys).to eq(%w[cim_instance_type nestedbar nestedfoo]) + expect(result[:dsc_nestedciminstance].keys).to eq(['baz', 'nestedproperty']) + expect(result[:dsc_nestedciminstance]['nestedproperty'].keys).to eq(['cim_instance_type', 'nestedbar', 'nestedfoo']) end it 'recursively sorts the result for order-insensitive comparisons' do - expect(result.keys).to eq(%i[dsc_array dsc_ciminstance dsc_ensure dsc_name dsc_nestedciminstance dsc_time name]) - expect(result[:dsc_array]).to eq(%w[bar foo]) - expect(result[:dsc_ciminstance].keys).to eq(%w[bar foo]) - expect(result[:dsc_nestedciminstance].keys).to eq(%w[baz nestedproperty]) - expect(result[:dsc_nestedciminstance]['nestedproperty'].keys).to eq(%w[cim_instance_type nestedbar nestedfoo]) + expect(result.keys).to eq([:dsc_array, :dsc_ciminstance, :dsc_ensure, :dsc_name, :dsc_nestedciminstance, :dsc_time, :name]) + expect(result[:dsc_array]).to eq(['bar', 'foo']) + expect(result[:dsc_ciminstance].keys).to eq(['bar', 'foo']) + expect(result[:dsc_nestedciminstance].keys).to eq(['baz', 'nestedproperty']) + expect(result[:dsc_nestedciminstance]['nestedproperty'].keys).to eq(['cim_instance_type', 'nestedbar', 'nestedfoo']) end context 'when a namevar is an array' do - let(:name_hash) { { name: 'foo', dsc_name: 'foo', dsc_array: %w[foo bar] } } - let(:query_props) { { dsc_name: 'foo', dsc_array: %w[foo bar] } } - let(:mandatory_get_attributes) { %i[dsc_name dsc_array] } + let(:name_hash) { { name: 'foo', dsc_name: 'foo', dsc_array: ['foo', 'bar'] } } + let(:query_props) { { dsc_name: 'foo', dsc_array: ['foo', 'bar'] } } + let(:mandatory_get_attributes) { [:dsc_name, :dsc_array] } let(:attributes) do { name: { type: 'String', - behaviour: :namevar + behaviour: :namevar, }, dsc_name: { type: 'String', @@ -847,23 +847,23 @@ mandatory_for_get: true, mandatory_for_set: true, mof_type: 'String', - mof_is_embedded: false + mof_is_embedded: false, }, dsc_array: { type: 'Array[String]', mandatory_for_get: true, mandatory_for_set: true, mof_type: 'String[]', - mof_is_embedded: false - } + mof_is_embedded: false, + }, } end let(:parsed_invocation_data) do - { 'Name' => 'foo', 'Ensure' => 'Present', 'Array' => %w[foo bar] } + { 'Name' => 'foo', 'Ensure' => 'Present', 'Array' => ['foo', 'bar'] } end it 'behaves like any other namevar when specified as not empty' do - expect(result[:dsc_array]).to eq(%w[bar foo]) + expect(result[:dsc_array]).to eq(['bar', 'foo']) end context 'when the namevar array is empty' do @@ -899,11 +899,11 @@ { 'Name' => 'foo', 'Ensure' => 'System.Object[]', - 'Time' => nil + 'Time' => nil, } end - before do + before(:each) do allow(ps_manager).to receive(:execute).with(script, nil).and_return({ stdout: 'DSC Data' }) allow(JSON).to receive(:parse).with('DSC Data').and_return(parsed_invocation_data) allow(provider).to receive(:fetch_cached_hashes).and_return([]) @@ -918,15 +918,15 @@ let(:parsed_invocation_data) do { 'Name' => nil, - 'Ensure' => 'Present' + 'Ensure' => 'Present', } end - before do + before(:each) do allow(ps_manager).to receive(:execute).with(script, nil).and_return({ stdout: 'DSC Data' }) allow(JSON).to receive(:parse).with('DSC Data').and_return(parsed_invocation_data) allow(provider).to receive(:fetch_cached_hashes).and_return([]) - allow(provider).to receive(:namevar_attributes).and_return(%i[name dsc_name]) + allow(provider).to receive(:namevar_attributes).and_return([:name, :dsc_name]) end it 'preserves the namevar value from the input name_hash' do @@ -945,7 +945,7 @@ end context 'when handling DateTimes' do - before do + before(:each) do allow(ps_manager).to receive(:execute).with(script, nil).and_return({ stdout: 'DSC Data' }) allow(JSON).to receive(:parse).with('DSC Data').and_return(parsed_invocation_data) allow(provider).to receive(:fetch_cached_hashes).and_return([]) @@ -970,7 +970,7 @@ end it 'writes an error and sets the value of `dsc_time` to nil' do - expect(context).to receive(:err).with(/Value returned for DateTime/) + expect(context).to receive(:err).with(%r{Value returned for DateTime}) expect(result[:dsc_time]).to be_nil end end @@ -982,7 +982,7 @@ end it 'writes an error and sets the value of `dsc_time` to nil' do - expect(context).to receive(:err).with(/Value returned for DateTime/) + expect(context).to receive(:err).with(%r{Value returned for DateTime}) expect(result[:dsc_time]).to be_nil end end @@ -996,14 +996,14 @@ let(:query_props) { { dsc_name: 'foo', dsc_psdscrunascredential: credential_hash } } context 'when the credential is invalid' do - before do + before(:each) do allow(provider).to receive(:logon_failed_already?).and_return(false) allow(ps_manager).to receive(:execute).with(script, nil).and_return({ stdout: 'DSC Data' }) allow(JSON).to receive(:parse).with('DSC Data').and_return({ 'errormessage' => dsc_logon_failure_error }) allow(context).to receive(:err).with(name_hash[:name], puppet_logon_failure_error) end - after do + after(:each) do provider.instance_variable_set(:@logon_failures, []) end @@ -1041,7 +1041,7 @@ let(:resource) { "Resource: #{apply_props}" } let(:script) { "Script: #{apply_props}" } - before do + before(:each) do allow(context).to receive(:debug) allow(provider).to receive(:invocable_resource).with(apply_props, context, 'set').and_return(resource) allow(provider).to receive(:ps_script_content).with(resource).and_return(script) @@ -1078,9 +1078,9 @@ context 'when the invocation script errors with a collision' do it 'writes a notice via context and applies successfully on retry' do expect(ps_manager).to receive(:execute).and_return({ stdout: '{"errormessage": "The Invoke-DscResource cmdlet is in progress and must return before Invoke-DscResource can be invoked"}' }) - expect(context).to receive(:notice).with(/Invoke-DscResource collision detected: Please stagger the timing of your Puppet runs as this can lead to unexpected behaviour./).once + expect(context).to receive(:notice).with(%r{Invoke-DscResource collision detected: Please stagger the timing of your Puppet runs as this can lead to unexpected behaviour.}).once expect(context).to receive(:notice).with('Sleeping for 60 seconds.').twice - expect(context).to receive(:notice).with(/Retrying: attempt [1-2] of 5/).twice + expect(context).to receive(:notice).with(%r{Retrying: attempt [1-2] of 5}).twice expect(ps_manager).to receive(:execute).and_return({ stdout: '{"errormessage": "The Invoke-DscResource cmdlet is in progress and must return before Invoke-DscResource can be invoked"}' }) expect(context).to receive(:notice).with('Attempt 1 of 5 failed.') allow(provider).to receive(:sleep) @@ -1091,25 +1091,25 @@ it 'writes a error via context and fails to apply when all retry attempts used' do expect(ps_manager).to receive(:execute).and_return({ stdout: '{"errormessage": "The Invoke-DscResource cmdlet is in progress and must return before Invoke-DscResource can be invoked"}' }) .exactly(5).times - expect(context).to receive(:notice).with(/Invoke-DscResource collision detected: Please stagger the timing of your Puppet runs as this can lead to unexpected behaviour./).once + expect(context).to receive(:notice).with(%r{Invoke-DscResource collision detected: Please stagger the timing of your Puppet runs as this can lead to unexpected behaviour.}).once expect(context).to receive(:notice).with('Sleeping for 60 seconds.').exactly(5).times - expect(context).to receive(:notice).with(/Retrying: attempt [1-5] of 5/).exactly(5).times + expect(context).to receive(:notice).with(%r{Retrying: attempt [1-5] of 5}).exactly(5).times expect(ps_manager).to receive(:execute).and_return({ stdout: '{"errormessage": "The Invoke-DscResource cmdlet is in progress and must return before Invoke-DscResource can be invoked"}' }) - expect(context).to receive(:notice).with(/Attempt [1-5] of 5 failed/).exactly(5).times - expect(context).to receive(:err).with(/The Invoke-DscResource cmdlet is in progress and must return before Invoke-DscResource can be invoked/) + expect(context).to receive(:notice).with(%r{Attempt [1-5] of 5 failed}).exactly(5).times + expect(context).to receive(:err).with(%r{The Invoke-DscResource cmdlet is in progress and must return before Invoke-DscResource can be invoked}) allow(provider).to receive(:sleep) expect(result).to be_nil end it 'writes an error via context and fails to apply when encountering an unexpected error' do expect(ps_manager).to receive(:execute).and_return({ stdout: '{"errormessage": "The Invoke-DscResource cmdlet is in progress and must return before Invoke-DscResource can be invoked"}' }) - expect(context).to receive(:notice).with(/Invoke-DscResource collision detected: Please stagger the timing of your Puppet runs as this can lead to unexpected behaviour./).once + expect(context).to receive(:notice).with(%r{Invoke-DscResource collision detected: Please stagger the timing of your Puppet runs as this can lead to unexpected behaviour.}).once expect(context).to receive(:notice).with('Sleeping for 60 seconds.').once - expect(context).to receive(:notice).with(/Retrying: attempt 1 of 5/).once + expect(context).to receive(:notice).with(%r{Retrying: attempt 1 of 5}).once allow(provider).to receive(:sleep) expect(ps_manager).to receive(:execute).and_return({ stdout: '{"errormessage": "Some unexpected error"}' }).once - expect(context).to receive(:notice).with(/Attempt 1 of 5 failed/).once - expect(context).to receive(:err).with(/Some unexpected error/) + expect(context).to receive(:notice).with(%r{Attempt 1 of 5 failed}).once + expect(context).to receive(:err).with(%r{Some unexpected error}) expect(result).to be_nil end end @@ -1120,7 +1120,7 @@ let(:resource_with_timeout) { "Resource: #{apply_props_with_timeout}" } let(:script_with_timeout) { "Script: #{apply_props_with_timeout}" } - before do + before(:each) do allow(provider).to receive(:invocable_resource).with(apply_props_with_timeout, context, 'set').and_return(resource_with_timeout) allow(provider).to receive(:ps_script_content).with(resource_with_timeout).and_return(script_with_timeout) allow(provider).to receive(:remove_secret_identifiers).with(script_with_timeout).and_return(script_with_timeout) @@ -1144,9 +1144,9 @@ end describe '.invoke_get_method_for_canonicalize' do - before do + before(:each) do allow(context).to receive(:debug) - allow(provider).to receive(:namevar_attributes).and_return(%i[name dsc_name]) + allow(provider).to receive(:namevar_attributes).and_return([:name, :dsc_name]) end it 'calls invoke_get_method and caches to @cached_canonicalize_results' do @@ -1206,12 +1206,12 @@ dscmeta_module_name: 'PuppetDsc', dscmeta_module_version: '1.2.3.4', docs: 'The DSC Foo resource type. Automatically generated from version 1.2.3.4', - features: %w[simple_get_filter canonicalize], + features: ['simple_get_filter', 'canonicalize'], attributes: { name: { type: 'String', desc: 'Description of the purpose for this resource declaration.', - behaviour: :namevar + behaviour: :namevar, }, dsc_name: { type: 'String', @@ -1220,7 +1220,7 @@ mandatory_for_get: true, mandatory_for_set: true, mof_type: 'String', - mof_is_embedded: false + mof_is_embedded: false, }, dsc_psdscrunascredential: { type: 'Optional[Struct[{ user => String[1], password => Variant[String[1], Sensitive[String[1]]] }]]', @@ -1229,7 +1229,7 @@ mandatory_for_get: false, mandatory_for_set: false, mof_type: 'PSCredential', - mof_is_embedded: true + mof_is_embedded: true, }, dsc_ensure: { type: "Optional[Enum['Present', 'Absent']]", @@ -1237,15 +1237,15 @@ mandatory_for_get: false, mandatory_for_set: false, mof_type: 'String', - mof_is_embedded: false - } - } + mof_is_embedded: false, + }, + }, } end let(:should_hash) { { dsc_name: 'foo' } } let(:vendored_modules_path) { 'C:/code/puppetlabs/gems/ruby-pwsh/lib/puppet_x/puppetdsc/dsc_resources' } - before do + before(:each) do allow(context).to receive(:debug) allow(context).to receive(:type).and_return(type) allow(type).to receive(:definition).and_return(definition) @@ -1298,13 +1298,13 @@ let(:load_path) { [] } let(:new_path_nil_root_module) { 'C:/code/puppetlabs/gems/ruby-pwsh/lib/puppet_x/puppetdsc/dsc_resources' } - before do + before(:each) do allow(provider).to receive(:load_path).and_return(load_path) allow(File).to receive(:exist?).and_call_original end it 'raises an error when the vendored resources cannot be found' do - expect { provider.vendored_modules_path('NeverGonnaFindMe') }.to raise_error(/Unable to find expected vendored DSC Resource/) + expect { provider.vendored_modules_path('NeverGonnaFindMe') }.to raise_error(%r{Unable to find expected vendored DSC Resource}) end context 'when the vendored resources are in puppet_x//dsc_resources' do @@ -1367,16 +1367,16 @@ let(:name) { { name: 'foo', dsc_name: 'bar' } } let(:should_hash) { name.merge(dsc_ensure: 'present') } - let(:test_properties) { should_hash.reject { |k, _v| k == :name } } + let(:test_properties) { should_hash.except(:name) } let(:invoke_dsc_resource_data) { nil } - before do + before(:each) do allow(context).to receive(:notice) allow(context).to receive(:debug) allow(provider).to receive(:invoke_dsc_resource).with(context, name, test_properties, 'test').and_return(invoke_dsc_resource_data) end - after do + after(:each) do provider.instance_variable_set(:@cached_test_results, []) end @@ -1406,7 +1406,7 @@ # Resource is not in the desired state expect(result.first).to be(false) # Custom out-of-sync message passed - expect(result.last).to match(/not in the desired state/) + expect(result.last).to match(%r{not in the desired state}) expect(provider.cached_test_results).to eq([name.merge(in_desired_state: false)]) end end @@ -1418,13 +1418,13 @@ end it 'includes underscores instead of hyphens' do - expect(provider.random_variable_name).to match(/_/) - expect(provider.random_variable_name).not_to match(/-/) + expect(provider.random_variable_name).to match(%r{_}) + expect(provider.random_variable_name).not_to match(%r{-}) end end describe '.instantiated_variables' do - after do + after(:each) do provider.instance_variable_set(:@instantiated_variables, []) end @@ -1439,7 +1439,7 @@ end describe '.clear_instantiated_variables!' do - after do + after(:each) do provider.instance_variable_set(:@instantiated_variables, []) end @@ -1463,12 +1463,12 @@ end context 'when the logon_failures cache has entries' do - before do + before(:each) do allow(good_password).to receive(:unwrap).and_return('foo') allow(bad_password).to receive(:unwrap).and_return('bar') end - after do + after(:each) do provider.instance_variable_set(:@logon_failures, []) end @@ -1490,7 +1490,7 @@ 'SomeKey' => 'value', 'SomeArray' => [ { 'ArrayKeyOne' => 1, 'ArrayKeyTwo' => 2 }, - { 'ArrayKeyOne' => '1', 'ArrayKeyTwo' => '2' } + { 'ArrayKeyOne' => '1', 'ArrayKeyTwo' => '2' }, ], 'SomeHash' => { 'NestedKey' => 'foo', @@ -1499,24 +1499,24 @@ 'DeeplyNestedKey' => 'foo', 'DeeplyNestedArray' => [{ 'DeeplyNestedArrayKeyOne' => 1, 'DeeplyNestedArrayKeyTwo' => 2 }], 'DeeplyNestedHash' => { - 'VeryDeeplyNestedKey' => 'foo' - } - } - } + 'VeryDeeplyNestedKey' => 'foo', + }, + }, + }, } end it 'converts all the keys in a hash into downcase, even if nested in another hash or array' do downcased_hash = test_hash.dup expect { provider.downcase_hash_keys!(downcased_hash) }.not_to raise_error - expect(downcased_hash.keys).to eq(%w[somekey somearray somehash]) - expect(downcased_hash['somearray'][0].keys).to eq(%w[arraykeyone arraykeytwo]) - expect(downcased_hash['somearray'][1].keys).to eq(%w[arraykeyone arraykeytwo]) - expect(downcased_hash['somehash'].keys).to eq(%w[nestedkey nestedarray nestedhash]) - expect(downcased_hash['somehash']['nestedarray'].first.keys).to eq(%w[nestedarraykeyone nestedarraykeytwo]) - expect(downcased_hash['somehash']['nestedhash'].keys).to eq(%w[deeplynestedkey deeplynestedarray deeplynestedhash]) - expect(downcased_hash['somehash']['nestedhash']['deeplynestedarray'].first.keys).to eq(%w[deeplynestedarraykeyone deeplynestedarraykeytwo]) - expect(downcased_hash['somehash']['nestedhash']['deeplynestedhash'].keys).to eq(%w[verydeeplynestedkey]) + expect(downcased_hash.keys).to eq(['somekey', 'somearray', 'somehash']) + expect(downcased_hash['somearray'][0].keys).to eq(['arraykeyone', 'arraykeytwo']) + expect(downcased_hash['somearray'][1].keys).to eq(['arraykeyone', 'arraykeytwo']) + expect(downcased_hash['somehash'].keys).to eq(['nestedkey', 'nestedarray', 'nestedhash']) + expect(downcased_hash['somehash']['nestedarray'].first.keys).to eq(['nestedarraykeyone', 'nestedarraykeytwo']) + expect(downcased_hash['somehash']['nestedhash'].keys).to eq(['deeplynestedkey', 'deeplynestedarray', 'deeplynestedhash']) + expect(downcased_hash['somehash']['nestedhash']['deeplynestedarray'].first.keys).to eq(['deeplynestedarraykeyone', 'deeplynestedarraykeytwo']) + expect(downcased_hash['somehash']['nestedhash']['deeplynestedhash'].keys).to eq(['verydeeplynestedkey']) end end @@ -1532,7 +1532,7 @@ 'cim_instance_type' => 'MSFT_xWebBindingInformation', 'Port' => 80, 'IPAddress' => '*', - 'Protocol' => 'http' + 'Protocol' => 'http', } end let(:nested_cim_instance) do @@ -1543,15 +1543,15 @@ 'Inheritance' => 'This folder and files', 'Ensure' => 'Present', 'cim_instance_type' => 'NTFSAccessControlEntry', - 'FileSystemRights' => ['FullControl'] - } + 'FileSystemRights' => ['FullControl'], + }, ], 'ForcePrincipal' => true, - 'Principal' => 'Everyone' + 'Principal' => 'Everyone', } end - before { provider.munge_cim_instances!(value) } + before(:each) { provider.munge_cim_instances!(value) } context 'when called against a non-nested cim instance' do let(:value) { cim_instance.dup } @@ -1586,7 +1586,7 @@ end context 'when called against a value which is not a cim_instance' do - let(:original) { %w[foo bar baz] } + let(:original) { ['foo', 'bar', 'baz'] } let(:value) { original.dup } it 'does not change the value' do @@ -1601,7 +1601,7 @@ SomeKey: 'Value', SomeArray: [ { ArrayKeyOne: 1, ArrayKeyTwo: 2 }, - { ArrayKeyOne: 'ONE', ArrayKeyTwo: 2 } + { ArrayKeyOne: 'ONE', ArrayKeyTwo: 2 }, ], SomeHash: { NestedKey: 'Foo', @@ -1610,10 +1610,10 @@ DeeplyNestedKey: 'Foo', DeeplyNestedArray: [{ DeeplyNestedArrayKeyOne: 'One', DeeplyNestedArrayKeyTwo: 2 }], DeeplyNestedHash: { - VeryDeeplyNestedKey: 'Foo' - } - } - } + VeryDeeplyNestedKey: 'Foo', + }, + }, + }, } end let(:downcased_array) { [{ arraykeyone: 1, arraykeytwo: 2 }, { arraykeyone: 'one', arraykeytwo: 2 }] } @@ -1625,9 +1625,9 @@ deeplynestedkey: 'foo', deeplynestedarray: [{ deeplynestedarraykeyone: 'one', deeplynestedarraykeytwo: 2 }], deeplynestedhash: { - verydeeplynestedkey: 'foo' - } - } + verydeeplynestedkey: 'foo', + }, + }, } end @@ -1653,20 +1653,20 @@ DeeplyNestedArray: [2, 3, 'a', 1, 'c', 'b'], DeeplyNestedHash: { VeryDeeplyNestedKey2: 'Foo', - VeryDeeplyNestedKey1: 'Bar' - } - } - } + VeryDeeplyNestedKey1: 'Bar', + }, + }, + }, } end - let(:sorted_keys) { %i[SomeArray SomeComplexArray SomeHash SomeKey] } + let(:sorted_keys) { [:SomeArray, :SomeComplexArray, :SomeHash, :SomeKey] } let(:sorted_some_array) { [1, 2, 3] } let(:sorted_complex_array) { [2, 3, [1, 2]] } - let(:sorted_some_hash_keys) { %i[NestedArray NestedHash NestedKey] } + let(:sorted_some_hash_keys) { [:NestedArray, :NestedHash, :NestedKey] } let(:sorted_nested_array) { [1, 2, { NestedArrayKeyOne: 'ONE', NestedArrayKeyTwo: 2 }] } - let(:sorted_nested_hash_keys) { %i[DeeplyNestedArray DeeplyNestedHash DeeplyNestedKey] } + let(:sorted_nested_hash_keys) { [:DeeplyNestedArray, :DeeplyNestedHash, :DeeplyNestedKey] } let(:sorted_deeply_nested_array) { [1, 2, 3, 'a', 'b', 'c'] } - let(:sorted_deeply_nested_hash_keys) { %i[VeryDeeplyNestedKey1 VeryDeeplyNestedKey2] } + let(:sorted_deeply_nested_hash_keys) { [:VeryDeeplyNestedKey1, :VeryDeeplyNestedKey2] } it 'downcases any string passed, whether alone or in a hash or array or nested deeply' do result = provider.recursively_sort(test_hash) @@ -1710,14 +1710,14 @@ name: { type: 'String' }, dsc_ensure: { mandatory_for_get: true }, dsc_enum: { mandatory_for_get: false }, - dsc_string: { mandatory_for_get: true } + dsc_string: { mandatory_for_get: true }, } end it 'returns the list of attributes from the type where the mandatory_for_get meta property is true' do expect(context).to receive(:type).and_return(type) expect(type).to receive(:attributes).and_return(attributes) - expect(provider.mandatory_get_attributes(context)).to eq(%i[dsc_ensure dsc_string]) + expect(provider.mandatory_get_attributes(context)).to eq([:dsc_ensure, :dsc_string]) end end @@ -1727,14 +1727,14 @@ name: { type: 'String' }, dsc_ensure: { mandatory_for_set: true }, dsc_enum: { mandatory_for_set: false }, - dsc_string: { mandatory_for_set: true } + dsc_string: { mandatory_for_set: true }, } end it 'returns the list of attributes from the type where the mandatory_for_set meta property is true' do expect(context).to receive(:type).and_return(type) expect(type).to receive(:attributes).and_return(attributes) - expect(provider.mandatory_set_attributes(context)).to eq(%i[dsc_ensure dsc_string]) + expect(provider.mandatory_set_attributes(context)).to eq([:dsc_ensure, :dsc_string]) end end @@ -1745,14 +1745,14 @@ dsc_name: { type: 'String', behaviour: :namevar }, dsc_ensure: { type: "[Enum['Present', 'Absent']]" }, dsc_enum: { type: "Optional[Enum['Trusted', 'Untrusted']]" }, - dsc_string: { type: 'Optional[String]' } + dsc_string: { type: 'Optional[String]' }, } end it 'returns the list of attributes from the type where the attribute has the namevar behavior' do expect(context).to receive(:type).and_return(type) expect(type).to receive(:attributes).and_return(attributes) - expect(provider.namevar_attributes(context)).to eq(%i[name dsc_name]) + expect(provider.namevar_attributes(context)).to eq([:name, :dsc_name]) end end @@ -1763,14 +1763,14 @@ dsc_name: { type: 'String', behaviour: :namevar }, dsc_ensure: { type: "[Enum['Present', 'Absent']]" }, dsc_enum: { type: "Optional[Enum['Trusted', 'Untrusted']]", behaviour: :parameter }, - dsc_string: { type: 'Optional[String]', behaviour: :parameter } + dsc_string: { type: 'Optional[String]', behaviour: :parameter }, } end it 'returns the list of attributes from the type where the attribute has the parameter behavior' do expect(context).to receive(:type).and_return(type) expect(type).to receive(:attributes).and_return(attributes) - expect(provider.parameter_attributes(context)).to eq(%i[dsc_enum dsc_string]) + expect(provider.parameter_attributes(context)).to eq([:dsc_enum, :dsc_string]) end end @@ -1780,14 +1780,14 @@ name: { type: 'String' }, dsc_ensure: { type: "[Enum['Present', 'Absent']]" }, dsc_enum: { type: "Optional[Enum['Trusted', 'Untrusted']]" }, - dsc_string: { type: 'Optional[String]' } + dsc_string: { type: 'Optional[String]' }, } end it 'returns the list of attributes from the type where the attribute data type is an enum' do expect(context).to receive(:type).and_return(type) expect(type).to receive(:attributes).and_return(enum_test_attributes) - expect(provider.enum_attributes(context)).to eq(%i[dsc_ensure dsc_enum]) + expect(provider.enum_attributes(context)).to eq([:dsc_ensure, :dsc_enum]) end end @@ -1796,11 +1796,11 @@ { some_variable_name: 'FooBar', another_variable_name: 'Get-Foo', - third_variable_name: 'Get-Foo "bar"' + third_variable_name: 'Get-Foo "bar"', } end - before do + before(:each) do allow(provider).to receive(:instantiated_variables).and_return(instantiated_variables) end @@ -1821,7 +1821,7 @@ it 'sets $UnmungedPSModulePath to the current PSModulePath' do # since https://github.com/puppetlabs/ruby-pwsh/pull/261 we load vendored path for MOF resources as well - expect(result).to match(/\$UnmungedPSModulePath = .+GetEnvironmentVariable.+PSModulePath.+machine/) + expect(result).to match(%r{\$UnmungedPSModulePath = .+GetEnvironmentVariable.+PSModulePath.+machine}) end end @@ -1830,7 +1830,7 @@ # since https://github.com/puppetlabs/ruby-pwsh/pull/261 we load vendored path for MOF resources as well it 'sets $UnmungedPSModulePath to the current PSModulePath' do - expect(result).to match(/\$UnmungedPSModulePath = .+GetEnvironmentVariable.+PSModulePath.+machine/) + expect(result).to match(%r{\$UnmungedPSModulePath = .+GetEnvironmentVariable.+PSModulePath.+machine}) end end @@ -1838,19 +1838,19 @@ let(:test_resource) { { dscmeta_resource_implementation: 'Class', vendored_modules_path: 'C:/foo/bar' } } it 'sets $UnmungedPSModulePath to the current PSModulePath' do - expect(result).to match(/\$UnmungedPSModulePath = .+GetEnvironmentVariable.+PSModulePath.+machine/) + expect(result).to match(%r{\$UnmungedPSModulePath = .+GetEnvironmentVariable.+PSModulePath.+machine}) end it 'sets $MungedPSModulePath the vendor path with backslash separators' do - expect(result).to match(/\$MungedPSModulePath = .+;C:\\foo\\bar/) + expect(result).to match(%r{\$MungedPSModulePath = .+;C:\\foo\\bar}) end it 'updates the system PSModulePath to $MungedPSModulePath' do - expect(result).to match(/-Name 'PSModulePath' -Value \$MungedPSModulePath/) + expect(result).to match(%r{-Name 'PSModulePath' -Value \$MungedPSModulePath}) end it 'sets the process level PSModulePath to the modified system PSModulePath' do - expect(result).to match(/\$env:PSModulePath = .+GetEnvironmentVariable.+PSModulePath.+machine/) + expect(result).to match(%r{\$env:PSModulePath = .+GetEnvironmentVariable.+PSModulePath.+machine}) end end end @@ -1861,8 +1861,8 @@ let(:base_resource) do { parameters: { - dsc_name: { value: 'foo', mof_type: 'String', mof_is_embedded: false } - } + dsc_name: { value: 'foo', mof_type: 'String', mof_is_embedded: false }, + }, } end @@ -1882,20 +1882,20 @@ parameters: { dsc_psdscrunascredential: { value: nil, mof_type: 'PSCredential' }, dsc_somecredential: { value: { 'user' => 'foo', 'password' => foo_password }, mof_type: 'PSCredential' }, - dsc_othercredential: { value: { 'user' => 'bar', 'password' => bar_password }, mof_type: 'PSCredential' } - } + dsc_othercredential: { value: { 'user' => 'bar', 'password' => bar_password }, mof_type: 'PSCredential' }, + }, } end let(:test_resource) { base_resource.merge(additional_parameters) } - before do + before(:each) do allow(Puppet::Pops::Types::PSensitiveType::Sensitive).to receive(:===).with(foo_password).and_return(true) allow(Puppet::Pops::Types::PSensitiveType::Sensitive).to receive(:===).with(bar_password).and_return(true) allow(foo_password).to receive(:unwrap).and_return('foo') allow(bar_password).to receive(:unwrap).and_return('bar') end - after do + after(:each) do provider.instance_variable_set(:@instantiated_variables, []) end @@ -1910,8 +1910,8 @@ end it 'returns an array of strings each containing the instantiation of a PowerShell variable representing the credential hash' do - expect(result[0]).to match(/^\$\w+ = New-PSCredential -User foo -Password 'foo#PuppetSensitive'/) - expect(result[1]).to match(/^\$\w+ = New-PSCredential -User bar -Password 'bar#PuppetSensitive'/) + expect(result[0]).to match(%r{^\$\w+ = New-PSCredential -User foo -Password 'foo#PuppetSensitive'}) + expect(result[1]).to match(%r{^\$\w+ = New-PSCredential -User bar -Password 'bar#PuppetSensitive'}) end end end @@ -1928,7 +1928,7 @@ describe '.prepare_cim_instances' do subject(:result) { provider.prepare_cim_instances(test_resource) } - after do + after(:each) do provider.instance_variable_set(:@instantiated_variables, []) end @@ -1939,14 +1939,14 @@ dsc_someciminstance: { value: { 'foo' => 1, 'bar' => 'two' }, mof_type: 'SomeCimType', - mof_is_embedded: true + mof_is_embedded: true, }, - dsc_name: { value: 'foo', mof_type: 'String', mof_is_embedded: false } - } + dsc_name: { value: 'foo', mof_type: 'String', mof_is_embedded: false }, + }, } end - before do + before(:each) do allow(provider).to receive(:nested_cim_instances).with(test_resource[:parameters][:dsc_someciminstance][:value]).and_return([nil, nil]) allow(provider).to receive(:random_variable_name).and_return('cim_foo') end @@ -1970,34 +1970,34 @@ 'accesscontrolentry' => [{ 'accesscontroltype' => 'Allow', 'ensure' => 'Present', - 'cim_instance_type' => 'NTFSAccessControlEntry' + 'cim_instance_type' => 'NTFSAccessControlEntry', }], - 'principal' => 'veryRealUserName' + 'principal' => 'veryRealUserName', }], mof_type: 'NTFSAccessControlList[]', - mof_is_embedded: true - } - } + mof_is_embedded: true, + }, + }, } end let(:nested_cim_instances) do [[[{ 'accesscontroltype' => 'Allow', 'ensure' => 'Present', 'cim_instance_type' => 'NTFSAccessControlEntry' }], nil]] end - before do + before(:each) do allow(provider).to receive(:nested_cim_instances).with(test_resource[:parameters][:dsc_accesscontrollist][:value]).and_return(nested_cim_instances) allow(provider).to receive(:random_variable_name).and_return('cim_foo', 'cim_bar') end it 'processes nested cim instances first' do cim_instance_declarations = result.split("\n") - expect(cim_instance_declarations.first).to match(/\$cim_foo =/) - expect(cim_instance_declarations.first).to match(/ClassName 'NTFSAccessControlEntry'/) + expect(cim_instance_declarations.first).to match(%r{\$cim_foo =}) + expect(cim_instance_declarations.first).to match(%r{ClassName 'NTFSAccessControlEntry'}) end it 'references nested cim instances as variables in the parent cim instance' do cim_instance_declarations = result.split("\n") - expect(cim_instance_declarations[1]).to match(/\$cim_bar =.+Property @{'accesscontrolentry' = \[CimInstance\[\]\]@\(\$cim_foo\); 'principal' = 'veryRealUserName'}/) + expect(cim_instance_declarations[1]).to match(%r{\$cim_bar =.+Property @{'accesscontrolentry' = \[CimInstance\[\]\]@\(\$cim_foo\); 'principal' = 'veryRealUserName'}}) end end @@ -2005,8 +2005,8 @@ let(:test_resource) do { parameters: { - dsc_name: { value: 'foo', mof_type: 'String', mof_is_embedded: false } - } + dsc_name: { value: 'foo', mof_type: 'String', mof_is_embedded: false }, + }, } end @@ -2024,9 +2024,9 @@ 'accesscontrolentry' => [{ 'accesscontroltype' => 'Allow', 'ensure' => 'Present', - 'cim_instance_type' => 'NTFSAccessControlEntry' + 'cim_instance_type' => 'NTFSAccessControlEntry', }], - 'principal' => 'veryRealUserName' + 'principal' => 'veryRealUserName', }] end @@ -2037,7 +2037,7 @@ end describe '.format_ciminstance' do - after do + after(:each) do provider.instance_variable_set(:@instantiated_variables, []) end @@ -2049,14 +2049,14 @@ it 'handles arrays of cim instances' do property_hash = [{ 'foo' => 1, 'bar' => 'two' }, { 'foo' => 3, 'bar' => 'four' }] - expected_cim_instance_array_regex = /Property \[CimInstance\[\]\]@\(@\{'foo' = 1; 'bar' = 'two'}, @\{'foo' = 3; 'bar' = 'four'\}\)/ + expected_cim_instance_array_regex = %r{Property \[CimInstance\[\]\]@\(@\{'foo' = 1; 'bar' = 'two'}, @\{'foo' = 3; 'bar' = 'four'\}\)} expect(provider.format_ciminstance('foo', 'SomeClass', property_hash)).to match(expected_cim_instance_array_regex) end it 'interpolates variables in the case of a cim instance containing a nested instance' do provider.instance_variable_set(:@instantiated_variables, { 'SomeVariable' => { 'bar' => 'ope' } }) property_hash = { 'foo' => { 'bar' => 'ope' } } - expect(provider.format_ciminstance('foo', 'SomeClass', property_hash)).to match(/@\{'foo' = \$SomeVariable\}/) + expect(provider.format_ciminstance('foo', 'SomeClass', property_hash)).to match(%r{@\{'foo' = \$SomeVariable\}}) end end @@ -2073,26 +2073,26 @@ dscmeta_module_name: 'PuppetDsc', dsc_invoke_method: 'Get', vendored_modules_path: 'C:/code/puppetlabs/gems/ruby-pwsh/lib/puppet_x/puppetdsc/dsc_resources', - attributes: nil + attributes: nil, } end it 'includes the DSC Resource name in the output hash' do - expect(result).to match(/Name = 'Foo'/) + expect(result).to match(%r{Name = 'Foo'}) end it 'includes the specified method in the output hash' do - expect(result).to match(/Method = 'Get'/) + expect(result).to match(%r{Method = 'Get'}) end it 'includes the properties as a hashtable to pass the DSC Resource in the output hash' do - expect(result).to match(/Property = @\{name = 'foo'\}/) + expect(result).to match(%r{Property = @\{name = 'foo'\}}) end context 'when handling module versioning' do context 'when the dscmeta_module_version is not specified' do it 'includes the ModuleName in the output hash as a string' do - expect(result).to match(/ModuleName = 'PuppetDsc'/) + expect(result).to match(%r{ModuleName = 'PuppetDsc'}) end end @@ -2104,7 +2104,7 @@ dscmeta_module_name: 'PuppetDsc', dscmeta_module_version: '1.2.3.4', dsc_invoke_method: 'Get', - vendored_modules_path: 'C:/path/to/ruby-pwsh/lib/puppet_x/puppetdsc/dsc_resources' + vendored_modules_path: 'C:/path/to/ruby-pwsh/lib/puppet_x/puppetdsc/dsc_resources', } end let(:expected_module_name) do @@ -2112,7 +2112,7 @@ end it 'includes the ModuleName in the output hash as a hashtable of name and version' do - expect(result).to match(/#{expected_module_name}/) + expect(result).to match(%r{#{expected_module_name}}) end end end @@ -2130,7 +2130,7 @@ "$InvokeParams = @{Name = 'Foo'; Method = 'Get'; Property = @{credential = $SomeCredential}; ModuleName = 'PuppetDsc'}" end - before do + before(:each) do allow(Puppet::Pops::Types::PSensitiveType::Sensitive).to receive(:===).with(password).and_return(true) allow(password).to receive(:unwrap).and_return('bar') end @@ -2150,7 +2150,7 @@ it 'casts the formatted timestamp string to DateTime in the property hash' do expect(date_time).to receive(:format).and_return('2100-01-01') - expect(result).to match(/datetime = \[DateTime\]'2100-01-01'/) + expect(result).to match(%r{datetime = \[DateTime\]'2100-01-01'}) end end @@ -2160,7 +2160,7 @@ end it 'casts the empty aray to the mof type in the property hash' do - expect(result).to match(/emptyarray = \[String\[\]\]@\(\)/) + expect(result).to match(%r{emptyarray = \[String\[\]\]@\(\)}) end end @@ -2170,7 +2170,7 @@ end it 'casts the Cim Instance value as [CimInstance[]]in the property hash' do - expect(result).to match(/ciminstance = \[CimInstance\[\]\]@\{'something' = 1; 'another' = 'two'\}/) + expect(result).to match(%r{ciminstance = \[CimInstance\[\]\]@\{'something' = 1; 'another' = 'two'\}}) end end end @@ -2186,7 +2186,7 @@ "Functions Block\nPreamble Block\n\n\n\nParameters Block\nPostscript Block" end - before do + before(:each) do allow(File).to receive(:new).with("#{template_path}/invoke_dsc_resource_functions.ps1").and_return(functions_file_handle) allow(functions_file_handle).to receive(:read).and_return('Functions Block') allow(File).to receive(:new).with("#{template_path}/invoke_dsc_resource_preamble.ps1").and_return(preamble_file_handle) @@ -2282,9 +2282,9 @@ nested_value: sensitive_string, nested_array: [sensitive_string, 'bar'], nested_hash: { - deeply_nested_value: sensitive_string - } - } + deeply_nested_value: sensitive_string, + }, + }, } result = provider.unwrap(sensitive_structure) @@ -2455,7 +2455,7 @@ describe '.ps_manager' do describe '.ps_manager on non-Windows' do - before do + before(:each) do allow(Pwsh::Util).to receive(:on_windows?).and_return(false) allow(Pwsh::Manager).to receive(:pwsh_path).and_return('pwsh') allow(Pwsh::Manager).to receive(:pwsh_args).and_return('args') @@ -2475,7 +2475,7 @@ end describe '.ps_manager on Windows' do - before do + before(:each) do allow(Pwsh::Util).to receive(:on_windows?).and_return(true) allow(Pwsh::Manager).to receive(:powershell_path).and_return('pwsh') allow(Pwsh::Manager).to receive(:powershell_args).and_return('args') diff --git a/spec/unit/pwsh/util_spec.rb b/spec/unit/pwsh/util_spec.rb index fae8feeb..df4b4641 100644 --- a/spec/unit/pwsh/util_spec.rb +++ b/spec/unit/pwsh/util_spec.rb @@ -11,7 +11,7 @@ [ 'just a string', { - some_key: 'a value' + some_key: 'a value', }, 1, { @@ -20,27 +20,27 @@ nested_array: [ 1, 'another string', - { super_nested_key: 'value' } - ] - } - } + { super_nested_key: 'value' }, + ], + }, + }, ] end let(:snake_case_hash) do { a: 1, - apple_butter: %w[a b c], + apple_butter: ['a', 'b', 'c'], some_key_value: { nested_key: 1, - another_nested_key: 2 - } + another_nested_key: 2, + }, } end let(:pascal_case_hash_in_an_array) do [ 'just a string', { - SomeKey: 'a value' + SomeKey: 'a value', }, 1, { @@ -49,40 +49,40 @@ NestedArray: [ 1, 'another string', - { SuperNestedKey: 'value' } - ] - } - } + { SuperNestedKey: 'value' }, + ], + }, + }, ] end let(:pascal_case_hash) do { A: 1, - AppleButter: %w[a b c], + AppleButter: ['a', 'b', 'c'], SomeKeyValue: { NestedKey: 1, - AnotherNestedKey: 2 - } + AnotherNestedKey: 2, + }, } end let(:kebab_case_hash) do { a: 1, - 'apple-butter': %w[a b c], + 'apple-butter': ['a', 'b', 'c'], 'some-key-value': { 'nested-key': 1, - 'another-nested-key': 2 - } + 'another-nested-key': 2, + }, } end let(:camel_case_hash) do { a: 1, - appleButter: %w[a b c], + appleButter: ['a', 'b', 'c'], someKeyValue: { nestedKey: 1, - anotherNestedKey: 2 - } + anotherNestedKey: 2, + }, } end @@ -213,7 +213,7 @@ [ 'just a string', { - 'some_key' => 'a value' + 'some_key' => 'a value', }, 1, { @@ -222,17 +222,17 @@ 'nested_array' => [ 1, 'another string', - { 'super_nested_key' => 'value' } - ] - } - } + { 'super_nested_key' => 'value' }, + ], + }, + }, ] end let(:array_with_symbol_keys_in_hashes) do [ 'just a string', { - some_key: 'a value' + some_key: 'a value', }, 1, { @@ -241,10 +241,10 @@ nested_array: [ 1, 'another string', - { super_nested_key: 'value' } - ] - } - } + { super_nested_key: 'value' }, + ], + }, + }, ] end @@ -255,19 +255,19 @@ describe '.escape_quotes' do it 'handles single quotes' do - expect(described_class.escape_quotes("The 'Cats' go 'meow'!")).to match(/The ''Cats'' go ''meow''!/) + expect(described_class.escape_quotes("The 'Cats' go 'meow'!")).to match(%r{The ''Cats'' go ''meow''!}) end it 'handles double single quotes' do - expect(described_class.escape_quotes("The ''Cats'' go 'meow'!")).to match(/The ''''Cats'''' go ''meow''!/) + expect(described_class.escape_quotes("The ''Cats'' go 'meow'!")).to match(%r{The ''''Cats'''' go ''meow''!}) end it 'handles double quotes' do - expect(described_class.escape_quotes("The 'Cats' go \"meow\"!")).to match(/The ''Cats'' go "meow"!/) + expect(described_class.escape_quotes("The 'Cats' go \"meow\"!")).to match(%r{The ''Cats'' go "meow"!}) end it 'handles dollar signs' do - expect(described_class.escape_quotes('This should show $foo variable')).to match(/This should show \$foo variable/) + expect(described_class.escape_quotes('This should show $foo variable')).to match(%r{This should show \$foo variable}) end end @@ -283,8 +283,8 @@ nested_hash: { another_string: 'foo', another_number: 2, - array: [1, 2, 3] - } + array: [1, 2, 3], + }, } end let(:powershell_hash) { "@{string = 'string'; number = 1; symbol = some_symbol; boolean = $true; nested_hash = @{another_string = 'foo'; another_number = 2; array = @(1, 2, 3)}}" } @@ -316,7 +316,7 @@ end it 'raises an error if an unknown type is passed' do - expect { described_class.format_powershell_value(described_class) }.to raise_error(/unsupported type Module/) + expect { described_class.format_powershell_value(described_class) }.to raise_error(%r{unsupported type Module}) end end diff --git a/spec/unit/pwsh_spec.rb b/spec/unit/pwsh_spec.rb index c949b57d..cca1b8b2 100644 --- a/spec/unit/pwsh_spec.rb +++ b/spec/unit/pwsh_spec.rb @@ -71,7 +71,7 @@ def is_osx? it 'fails if the manger is created with a short timeout' do expect { described_class.new(ps_command, ps_args, debug: false, pipe_timeout: 0.01) }.to raise_error do |e| expect(e).to be_a(RuntimeError) - expected_error = /Failure waiting for PowerShell process (\d+) to start pipe server/ + expected_error = %r{Failure waiting for PowerShell process (\d+) to start pipe server} expect(e.message).to match expected_error pid = expected_error.match(e.message)[1].to_i @@ -229,7 +229,7 @@ def close_stream(stream, style = :inprocess) # fails with vanilla EPIPE or various EBADF depening on timing / Ruby version msgs = [ "^#{Regexp.escape(Errno::EPIPE.new.inspect)}", - bad_file_descriptor_regex + bad_file_descriptor_regex, ] expect_dead_manager(manager, msgs, :regex) @@ -258,7 +258,7 @@ def close_stream(stream, style = :inprocess) # fails with vanilla EPIPE or various EBADF depening on timing / Ruby version msgs = [ "^#{Regexp.escape(Errno::EPIPE.new.inspect)}", - bad_file_descriptor_regex + bad_file_descriptor_regex, ] expect_dead_manager(manager, msgs, :regex) @@ -333,8 +333,8 @@ def close_stream(stream, style = :inprocess) if Pwsh::Util.on_windows? expect(result[:stderr]).to eq(["'foo.exe' is not recognized as an internal or external command,\r\noperable program or batch file.\r\n"]) else - expect(result[:stderr][0]).to match(/foo\.exe:(?:.*)not found/) - expect(result[:stderr][0]).to match(/bar/) + expect(result[:stderr][0]).to match(%r{foo\.exe:(?:.*)not found}) + expect(result[:stderr][0]).to match(%r{bar}) end expect(result[:exitcode]).not_to eq(0) end @@ -436,7 +436,7 @@ def close_stream(stream, style = :inprocess) end it 'executes multi-line' do - result = manager.execute(<<-CODE + result = manager.execute(<<-CODE, $foo = ls $count = $foo.count $count @@ -448,7 +448,7 @@ def close_stream(stream, style = :inprocess) end it 'executes code with a try/catch, receiving the output of Write-Error' do - result = manager.execute(<<-CODE + result = manager.execute(<<-CODE, try{ $foo = ls $count = $foo.count @@ -464,7 +464,7 @@ def close_stream(stream, style = :inprocess) end it 'is able to execute the code in a try block when using try/catch' do - result = manager.execute(<<-CODE + result = manager.execute(<<-CODE, try { $foo = @(1, 2, 3).count exit 400 @@ -480,7 +480,7 @@ def close_stream(stream, style = :inprocess) end it 'is able to execute the code in a catch block when using try/catch' do - result = manager.execute(<<-CODE + result = manager.execute(<<-CODE, try { throw "Error!" exit 0 @@ -561,14 +561,14 @@ def output_cmdlet(ps_command, ps_args) # behavior of Write-Output introduces newlines after every width number # of characters as specified in the BufferSize of the custom console UI # Write-Host should usually be avoided, but works for this test in old PS2 - current_powershell_major_version(ps_command, ps_args) >= 3 ? 'Write-Output' : 'Write-Host' + (current_powershell_major_version(ps_command, ps_args) >= 3) ? 'Write-Output' : 'Write-Host' end it 'is be able to write more than the 64k default buffer size to the managers pipe without deadlocking the Ruby parent process or breaking the pipe' do # this was tested successfully up to 5MB of text # we add some additional bytes so it's not always on a 1KB boundary and forces pipe reading in different lengths, not always 1K chunks buffer_string_96k = 'a' * ((1024 * 96) + 11) - result = manager.execute(<<-CODE + result = manager.execute(<<-CODE, '#{buffer_string_96k}' | #{output_cmdlet(ps_command, ps_args)} CODE ) @@ -581,7 +581,7 @@ def output_cmdlet(ps_command, ps_args) it 'is be able to write more than the 64k default buffer size to child process stdout without deadlocking the Ruby parent process' do # we add some additional bytes so it's not always on a 1KB boundary and forces pipe reading in different lengths, not always 1K chunks - result = manager.execute(<<-CODE + result = manager.execute(<<-CODE, $bytes_in_k = (1024 * 64) + 11 [Text.Encoding]::UTF8.GetString((New-Object Byte[] ($bytes_in_k))) | #{output_cmdlet(ps_command, ps_args)} CODE @@ -597,7 +597,7 @@ def output_cmdlet(ps_command, ps_args) it 'returns a response with a timeout error if the execution timeout is exceeded' do timeout_ms = 100 result = manager.execute('sleep 1', timeout_ms) - msg = /Catastrophic failure: PowerShell module timeout \(#{timeout_ms} ms\) exceeded while executing/ + msg = %r{Catastrophic failure: PowerShell module timeout \(#{timeout_ms} ms\) exceeded while executing} expect(result[:errormessage]).to match(msg) end @@ -607,19 +607,19 @@ def output_cmdlet(ps_command, ps_args) result = manager.execute(command, timeout_ms) expect(result[:exitcode]).to eq(1) # starts with Write-Output and Write-Debug messages - expect(result[:stdout]).to match(/200 OK Glenn/) - expect(result[:stdout]).to match(/DEBUG: 304 Not Modified James/) + expect(result[:stdout]).to match(%r{200 OK Glenn}) + expect(result[:stdout]).to match(%r{DEBUG: 304 Not Modified James}) # then command may have \r\n injected, so remove those for comparison - expect(result[:stdout].gsub(/\r\n/, '')).to include(command) + expect(result[:stdout].gsub("\r\n", '')).to include(command) # and it should end with the Write-Error content - expect(result[:stdout]).to match(/404 Craig Not Found/) + expect(result[:stdout]).to match(%r{404 Craig Not Found}) end it 'uses a default timeout of 300 seconds if the user specified a timeout of 0' do timeout_ms = 0 command = 'return $true' code = manager.make_ps_code(command, timeout_ms) - expect(code).to match(/TimeoutMilliseconds\s+=\s+300000/) + expect(code).to match(%r{TimeoutMilliseconds\s+=\s+300000}) end it 'uses the correct correct timeout if a small value is specified' do @@ -631,11 +631,11 @@ def output_cmdlet(ps_command, ps_args) timeout_ms = 20 command = 'return $true' code = manager.make_ps_code(command, timeout_ms) - expect(code).to match(/TimeoutMilliseconds\s+=\s+50/) + expect(code).to match(%r{TimeoutMilliseconds\s+=\s+50}) end it 'does not deadlock and returns a valid response given invalid unparseable PowerShell code' do - result = manager.execute(<<-CODE + result = manager.execute(<<-CODE, { CODE @@ -650,7 +650,7 @@ def output_cmdlet(ps_command, ps_args) result = manager.execute('(Get-Location).Path', nil, work_dir) expect(result[:exitcode]).not_to eq(0) - expect(result[:errormessage]).to match(/Working directory .+ does not exist/) + expect(result[:errormessage]).to match(%r{Working directory .+ does not exist}) end it 'allows forward slashes in working directory', if: Pwsh::Util.on_windows? do @@ -660,7 +660,7 @@ def output_cmdlet(ps_command, ps_args) result = manager.execute('(Get-Location).Path', nil, forward_work_dir)[:stdout] - expect(result).to match(/#{Regexp.escape(work_dir)}/i) + expect(result).to match(%r{#{Regexp.escape(work_dir)}}i) end it 'uses a specific working directory if set' do @@ -668,7 +668,7 @@ def output_cmdlet(ps_command, ps_args) result = manager.execute('(Get-Location).Path', nil, work_dir)[:stdout] - expect(result).to match(/#{Regexp.escape(work_dir)}/i) + expect(result).to match(%r{#{Regexp.escape(work_dir)}}i) end it 'does not reuse the same working directory between runs' do @@ -688,15 +688,15 @@ def output_cmdlet(ps_command, ps_args) result = manager.execute(powershell_runtime_error) expect(result[:exitcode]).to eq(1) - expect(result[:errormessage]).not_to match(/EndInvoke/) - expect(result[:errormessage]).not_to match(/throw/) + expect(result[:errormessage]).not_to match(%r{EndInvoke}) + expect(result[:errormessage]).not_to match(%r{throw}) end it 'displays line and char information for a runtime error' do result = manager.execute(powershell_runtime_error) expect(result[:exitcode]).to eq(1) - expect(result[:errormessage]).to match(/At line:\d+ char:\d+/) + expect(result[:errormessage]).to match(%r{At line:\d+ char:\d+}) end end @@ -705,15 +705,15 @@ def output_cmdlet(ps_command, ps_args) result = manager.execute(powershell_parseexception_error) expect(result[:exitcode]).to eq(1) - expect(result[:errormessage]).not_to match(/EndInvoke/) - expect(result[:errormessage]).not_to match(/throw/) + expect(result[:errormessage]).not_to match(%r{EndInvoke}) + expect(result[:errormessage]).not_to match(%r{throw}) end it 'displays line and char information for a ParseException error' do result = manager.execute(powershell_parseexception_error) expect(result[:exitcode]).to eq(1) - expect(result[:errormessage]).to match(/At line:\d+ char:\d+/) + expect(result[:errormessage]).to match(%r{At line:\d+ char:\d+}) end end @@ -722,15 +722,15 @@ def output_cmdlet(ps_command, ps_args) result = manager.execute(powershell_incompleteparseexception_error) expect(result[:exitcode]).to eq(1) - expect(result[:errormessage]).not_to match(/EndInvoke/) - expect(result[:errormessage]).not_to match(/throw/) + expect(result[:errormessage]).not_to match(%r{EndInvoke}) + expect(result[:errormessage]).not_to match(%r{throw}) end it 'does not display line and char information for an IncompleteParseException error' do result = manager.execute(powershell_incompleteparseexception_error) expect(result[:exitcode]).to eq(1) - expect(result[:errormessage]).not_to match(/At line:\d+ char:\d+/) + expect(result[:errormessage]).not_to match(%r{At line:\d+ char:\d+}) end end end @@ -740,7 +740,7 @@ def output_cmdlet(ps_command, ps_args) msg = SecureRandom.uuid.to_s.delete('-') result = manager.execute("$VerbosePreference = 'Continue';Write-Verbose '#{msg}'") - expect(result[:stdout]).to match(/^VERBOSE: #{msg}/) + expect(result[:stdout]).to match(%r{^VERBOSE: #{msg}}) expect(result[:exitcode]).to eq(0) end @@ -748,7 +748,7 @@ def output_cmdlet(ps_command, ps_args) msg = SecureRandom.uuid.to_s.delete('-') result = manager.execute("$debugPreference = 'Continue';Write-debug '#{msg}'") - expect(result[:stdout]).to match(/^DEBUG: #{msg}/) + expect(result[:stdout]).to match(%r{^DEBUG: #{msg}}) expect(result[:exitcode]).to eq(0) end @@ -756,7 +756,7 @@ def output_cmdlet(ps_command, ps_args) msg = SecureRandom.uuid.to_s.delete('-') result = manager.execute("Write-Warning '#{msg}'") - expect(result[:stdout]).to match(/^WARNING: #{msg}/) + expect(result[:stdout]).to match(%r{^WARNING: #{msg}}) expect(result[:exitcode]).to eq(0) end @@ -764,7 +764,7 @@ def output_cmdlet(ps_command, ps_args) msg = SecureRandom.uuid.to_s.delete('-') result = manager.execute("$ErrorView = 'NormalView' ; Write-Error '#{msg}'") - expect(result[:stdout]).to match(/Write-Error '#{msg}' : #{msg}/) + expect(result[:stdout]).to match(%r{Write-Error '#{msg}' : #{msg}}) expect(result[:exitcode]).to eq(0) end From 6dea21555d0485d4bfa53c4d42fbd93ee9406b2e Mon Sep 17 00:00:00 2001 From: Gavin Didrichsen Date: Mon, 26 Oct 2026 12:34:41 +0000 Subject: [PATCH 03/10] (MAINT) Fix the regex broken by the rubocop autocorrect Style/RegexpLiteral rewrote the expected_cim_instance_array_regex literal from /.../ to %r{...} but left the `}` after 'two' unescaped. That closes the %r{} early and makes the spec file a syntax error (Lint/Syntax). Escape the brace so the pattern matches the same text. Co-Authored-By: Claude Sonnet 5.5 --- .../puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb b/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb index f8841381..328bba3d 100644 --- a/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb +++ b/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb @@ -2049,7 +2049,7 @@ it 'handles arrays of cim instances' do property_hash = [{ 'foo' => 1, 'bar' => 'two' }, { 'foo' => 3, 'bar' => 'four' }] - expected_cim_instance_array_regex = %r{Property \[CimInstance\[\]\]@\(@\{'foo' = 1; 'bar' = 'two'}, @\{'foo' = 3; 'bar' = 'four'\}\)} + expected_cim_instance_array_regex = %r{Property \[CimInstance\[\]\]@\(@\{'foo' = 1; 'bar' = 'two'\}, @\{'foo' = 3; 'bar' = 'four'\}\)} expect(provider.format_ciminstance('foo', 'SomeClass', property_hash)).to match(expected_cim_instance_array_regex) end From fbcacaccfcca528541d24e904627ae8d8ce67135 Mon Sep 17 00:00:00 2001 From: Gavin Didrichsen Date: Mon, 26 Oct 2026 12:37:00 +0000 Subject: [PATCH 04/10] (MAINT) Inherit the rubocop todo file through .sync.yml The pdk update replaced .rubocop.yml with the template's version, which no longer inherited .rubocop_todo.yml, so the module's known offences were reported again. Set include_todos in .sync.yml so the template renders `inherit_from: .rubocop_todo.yml` (with merged Excludes) instead of hand-editing a managed file that the next pdk update would overwrite. Co-Authored-By: Claude Sonnet 5.5 --- .rubocop.yml | 4 ++++ .sync.yml | 3 +++ 2 files changed, 7 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index 86228843..ed02062f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -23,6 +23,10 @@ AllCops: - "**/Puppetfile" - "**/Vagrantfile" - "**/Guardfile" +inherit_from: ".rubocop_todo.yml" +inherit_mode: + merge: + - Exclude Layout/LineLength: Description: People have wide screens, use them. Max: 200 diff --git a/.sync.yml b/.sync.yml index 115bd263..b2830046 100644 --- a/.sync.yml +++ b/.sync.yml @@ -17,3 +17,6 @@ spec/spec_helper.rb: delete: true Rakefile: unmanaged: true +.rubocop.yml: + selected_profile: 'on' + include_todos: true From 64fac178dc64bf46a75e005f19c558d1e5871832 Mon Sep 17 00:00:00 2001 From: Gavin Didrichsen Date: Mon, 26 Oct 2026 12:49:30 +0000 Subject: [PATCH 05/10] (MAINT) Revert the lib/ changes made by the rubocop autocorrect The autocorrect in 0db45e4 also rewrote production code. Style/ClassAndModuleChildren turned `module Pwsh` / `module Util` into `module Pwsh::Util` and `class Pwsh::WindowsPowerShell`, which raises `NameError: uninitialized constant Pwsh` at load time. It also changed behaviour: `return nil` became `return false` in a predicate, and several `select` blocks became `slice`. This PR is about the Puppet 8 CI floor, so keep lib/ exactly as on main and let the rubocop todo file carry any offences there. With lib/ restored, the unit specs that can run without a pwsh binary pass (231 examples, 0 failures). Co-Authored-By: Claude Sonnet 5.5 --- .../dsc_base_provider/dsc_base_provider.rb | 86 ++++--- lib/pwsh.rb | 14 +- lib/pwsh/util.rb | 236 +++++++++--------- lib/pwsh/windows_powershell.rb | 158 ++++++------ 4 files changed, 253 insertions(+), 241 deletions(-) diff --git a/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb b/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb index 171de739..f33dea86 100644 --- a/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb +++ b/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb @@ -32,10 +32,10 @@ def initialize # @param hashes [Array] the list of hashes to search the cache for # @return [Array] an array containing the matching hashes for the search condition, if any def fetch_cached_hashes(cache, hashes) - cache.reject { |item| + cache.reject do |item| matching_hash = hashes.select { |hash| (item.to_a - hash.to_a).empty? || (hash.to_a - item.to_a).empty? } matching_hash.empty? - }.flatten + end.flatten end # Implements the canonicalize feature of the Resource API; this method is called first against any resources @@ -54,7 +54,7 @@ def canonicalize(context, resources) resources.collect do |r| # During RSAPI refresh runs mandatory parameters are stripped and not available; # Instead of checking again and failing, search the cache for a namevar match. - namevarized_r = r.slice(*namevar_attributes(context)) + namevarized_r = r.select { |k, _v| namevar_attributes(context).include?(k) } cached_result = fetch_cached_hashes(@cached_canonicalized_resource, [namevarized_r]).first if cached_result.nil? # If the resource is meant to be absent, skip canonicalization and rely on the manifest @@ -76,7 +76,7 @@ def canonicalize(context, resources) canonicalized = r.dup @cached_canonicalized_resource << r.dup else - parameters = r.slice(*parameter_attributes(context)) + parameters = r.select { |name, _properties| parameter_attributes(context).include?(name) } canonicalized.merge!(parameters) canonicalized[:name] = r[:name] if r[:dsc_psdscrunascredential].nil? @@ -162,7 +162,9 @@ def get(context, names = nil) mandatory_properties = {} else canonicalized_resource = @cached_canonicalized_resource[0].dup - mandatory_properties = canonicalized_resource.slice(*(mandatory_get_attributes(context) - namevar_attributes(context))) + mandatory_properties = canonicalized_resource.select do |attribute, _value| + (mandatory_get_attributes(context) - namevar_attributes(context)).include?(attribute) + end # If dsc_psdscrunascredential was specified, re-add it here. mandatory_properties[:dsc_psdscrunascredential] = canonicalized_resource[:dsc_psdscrunascredential] if canonicalized_resource.key?(:dsc_psdscrunascredential) end @@ -304,7 +306,7 @@ def perform_fresh_get(context, name, should) next unless valid_attributes.include?(type_key.to_s) # Sanitize .NET serialization artifacts (e.g., "System.Object[]") - if value.is_a?(String) && value =~ %r{^System\.\w+\[\]$} + if value.is_a?(String) && value =~ /^System\.\w+\[\]$/ result[type_key] = nil else # Normalize CIM instances the same way invoke_get_method does, @@ -401,7 +403,7 @@ def invoke_dsc_resource(context, name_hash, props, method) if output[:stdout].nil? message = 'Nothing returned.' - message += " #{output[:errormessage]}" if output[:errormessage]&.match?(%r{PowerShell module timeout \(\d+ ms\) exceeded while executing}) + message += " #{output[:errormessage]}" if output[:errormessage]&.match?(/PowerShell module timeout \(\d+ ms\) exceeded while executing/) context.err(message) return nil end @@ -413,7 +415,7 @@ def invoke_dsc_resource(context, name_hash, props, method) return nil end context.debug("raw data received: #{data.inspect}") - collision_error_matcher = %r{The Invoke-DscResource cmdlet is in progress and must return before Invoke-DscResource can be invoked} + collision_error_matcher = /The Invoke-DscResource cmdlet is in progress and must return before Invoke-DscResource can be invoked/ error = data['errormessage'] @@ -528,7 +530,7 @@ def insync?(context, name, property_name, _is_hash, should_hash) # insync? again for the same property. Return nil so default comparison handles it. cache_key = name.is_a?(Hash) ? name[:name] : name property_key = "#{cache_key}_#{property_name}" - return false if @insync_property_cache.delete(property_key) + return nil if @insync_property_cache.delete(property_key) if should_hash[:validation_mode] == 'resource' insync_resource_mode(context, name, property_name, should_hash) @@ -652,7 +654,7 @@ def invoke_get_method(context, name_hash) # @return [Hash] returns a hash representing the DSC resource munged to the representation the Puppet Type expects def invoke_get_method_for_canonicalize(context, name_hash) # Check the canonicalize-specific cache first - cached = fetch_cached_hashes(@cached_canonicalize_results, [name_hash.slice(*namevar_attributes(context))]) + cached = fetch_cached_hashes(@cached_canonicalize_results, [name_hash.select { |k, _v| namevar_attributes(context).include?(k) }]) return cached.first unless cached.empty? # Call invoke_get_method which will do the actual DSC Get call. @@ -663,7 +665,7 @@ def invoke_get_method_for_canonicalize(context, name_hash) # Remove the entry that invoke_get_method just added to @cached_query_results # so that get() will do its own fresh DSC Get call and produce independent "is" data. @cached_query_results.select! do |item| - matching = fetch_cached_hashes([item], [name_hash.slice(*namevar_attributes(context))]) + matching = fetch_cached_hashes([item], [name_hash.select { |k, _v| namevar_attributes(context).include?(k) }]) matching.empty? end @@ -682,7 +684,7 @@ def canonicalize_get_value!(context, data, type_key, query_props) end # Convert DateTime back to appropriate type - if context.type.attributes[type_key][:mof_type] =~ %r{DateTime}i && !data[type_key].nil? + if context.type.attributes[type_key][:mof_type] =~ /DateTime/i && !data[type_key].nil? data[type_key] = begin Puppet::Pops::Time::Timestamp.parse(data[type_key]) rescue ArgumentError, TypeError => e @@ -698,7 +700,7 @@ def canonicalize_get_value!(context, data, type_key, query_props) # Sanitize .NET serialization artifacts (e.g., "System.Object[]") to nil. def sanitize_dotnet_artifacts!(context, data) data.each do |key, value| - next unless value.is_a?(String) && value =~ %r{^System\.\w+\[\]$} + next unless value.is_a?(String) && value =~ /^System\.\w+\[\]$/ context.debug("Sanitizing .NET serialization artifact for #{key}: #{value} -> nil") data[key] = nil @@ -725,7 +727,7 @@ def preserve_namevar_values!(context, data, name_hash) def invoke_set_method(context, name, should) context.debug("Invoking Set Method for '#{name}' with #{should.inspect}") - apply_props = should.select { |k, _v| k.to_s =~ %r{^dsc_} } + apply_props = should.select { |k, _v| k.to_s =~ /^dsc_/ } invoke_dsc_resource(context, should, apply_props, 'set') # TODO: Implement this functionality for notifying a DSC reboot? @@ -743,7 +745,7 @@ def invoke_test_method(context, name, should) context.debug("Relying on DSC Test method for validating if '#{name}' is in the desired state") context.debug("Invoking Test Method for '#{name}' with #{should.inspect}") - test_props = should.select { |k, _v| k.to_s =~ %r{^dsc_} } + test_props = should.select { |k, _v| k.to_s =~ /^dsc_/ } data = invoke_dsc_resource(context, name, test_props, 'test') # Something went wrong with Invoke-DscResource; fall back on property state comparisons return nil if data.nil? @@ -768,7 +770,7 @@ def invoke_test_method(context, name, should) def invocable_resource(should, context, dsc_invoke_method) resource = {} resource[:parameters] = {} - [:name, :dscmeta_resource_friendly_name, :dscmeta_resource_name, :dscmeta_resource_implementation, :dscmeta_module_name, :dscmeta_module_version].each do |k| + %i[name dscmeta_resource_friendly_name dscmeta_resource_name dscmeta_resource_implementation dscmeta_module_name dscmeta_module_version].each do |k| resource[k] = context.type.definition[k] end should.each do |k, v| @@ -778,7 +780,7 @@ def invocable_resource(should, context, dsc_invoke_method) resource[:parameters][k] = {} resource[:parameters][k][:value] = v - [:mof_type, :mof_is_embedded].each do |ky| + %i[mof_type mof_is_embedded].each do |ky| resource[:parameters][k][ky] = context.type.definition[:attributes][k][ky] end end @@ -839,9 +841,9 @@ def puppetize_name(name) # Puppet module names must be lower case name = name.downcase # Puppet module names must only include lowercase letters, digits and underscores - name = name.gsub(%r{[^a-z0-9_]}, '_') + name = name.gsub(/[^a-z0-9_]/, '_') # Puppet module names must not start with a number so if it does, append the letter 'a' to the name. Eg: '7zip' becomes 'a7zip' - name = "a#{name}" if name.match?(%r{^\d}) # rubocop:disable Lint/UselessAssignment + name = name.match?(/^\d/) ? "a#{name}" : name # rubocop:disable Lint/UselessAssignment end # Return a UUID with the dashes turned into underscores to enable the specifying of guaranteed-unique @@ -1019,7 +1021,7 @@ def enum_values(context, attribute) return [] unless type_string&.include?('Enum[') # Extract the enum values from the type string - enum_content = type_string.match(%r{Enum\[(.*?)\]})&.[](1) + enum_content = type_string.match(/Enum\[(.*?)\]/)&.[](1) # Return an empty array if we couldn't find the enum values return [] if enum_content.nil? @@ -1071,20 +1073,20 @@ def munge_psmodulepath(resource) # @return [String] An array of lines of PowerShell to instantiate PSCredentialObjects and store them in variables def prepare_credentials(resource) credentials_block = [] - resource[:parameters].each_value do |property_hash| + resource[:parameters].each do |_property_name, property_hash| next unless property_hash[:mof_type] == 'PSCredential' next if property_hash[:value].nil? variable_name = random_variable_name credential_hash = { 'user' => property_hash[:value]['user'], - 'password' => escape_quotes(unwrap_string(property_hash[:value]['password'])), + 'password' => escape_quotes(unwrap_string(property_hash[:value]['password'])) } credentials_block << format_pscredential(variable_name, credential_hash) instantiated_variables.merge!(variable_name => credential_hash) end credentials_block.join("\n") - (credentials_block == []) ? '' : credentials_block + credentials_block == [] ? '' : credentials_block end # Write a line of PowerShell which creates a PSCredential object and assigns it to a variable @@ -1106,7 +1108,7 @@ def format_pscredential(variable_name, credential_hash) # @return [String] An array of lines of PowerShell to instantiate CIM Instances and store them in variables def prepare_cim_instances(resource) cim_instances_block = [] - resource[:parameters].each_value do |property_hash| + resource[:parameters].each do |_property_name, property_hash| next unless property_hash[:mof_is_embedded] next if property_hash[:mof_type] == 'PSCredential' # Credentials are handled separately @@ -1120,13 +1122,13 @@ def prepare_cim_instances(resource) cim_instance_hashes.each do |instance| variable_name = random_variable_name class_name = instance['cim_instance_type'] - properties = instance.except('cim_instance_type') + properties = instance.reject { |k, _v| k == 'cim_instance_type' } cim_instances_block << format_ciminstance(variable_name, class_name, properties) instantiated_variables.merge!(variable_name => instance) end end # We have to handle arrays of CIM instances slightly differently - if %r{\[\]$}.match?(property_hash[:mof_type]) + if /\[\]$/.match?(property_hash[:mof_type]) class_name = property_hash[:mof_type].gsub('[]', '') property_hash[:value].each do |hash| variable_name = random_variable_name @@ -1140,7 +1142,7 @@ def prepare_cim_instances(resource) instantiated_variables.merge!(variable_name => property_hash[:value]) end end - (cim_instances_block == []) ? '' : cim_instances_block.join("\n") + cim_instances_block == [] ? '' : cim_instances_block.join("\n") end # Recursively search for and return CIM instances nested in an enumerable @@ -1187,7 +1189,7 @@ def invoke_params(resource) # rubocop:disable Metrics/MethodLength params = { Name: resource[:dscmeta_resource_friendly_name], Method: resource[:dsc_invoke_method], - Property: {}, + Property: {} } if resource.key?(:dscmeta_module_version) params[:ModuleName] = {} @@ -1206,14 +1208,14 @@ def invoke_params(resource) # rubocop:disable Metrics/MethodLength next if property_name == :dsc_timeout # strip dsc_ from the beginning of the property name declaration - name = property_name.to_s.gsub(%r{^dsc_}, '').to_sym + name = property_name.to_s.gsub(/^dsc_/, '').to_sym params[:Property][name] = case property_hash[:mof_type] when 'PSCredential' # format can't unwrap Sensitive strings nested in arbitrary hashes/etc, so make # the Credential hash interpolable as it will be replaced by a variable reference. { 'user' => property_hash[:value]['user'], - 'password' => escape_quotes(unwrap_string(property_hash[:value]['password'])), + 'password' => escape_quotes(unwrap_string(property_hash[:value]['password'])) } when 'DateTime' # These have to be handled specifically because they rely on the *Puppet* DateTime, @@ -1230,11 +1232,11 @@ def invoke_params(resource) # rubocop:disable Metrics/MethodLength # CIM instances do not do a consistent job of casting an empty array properly. empty_array_parameters = resource[:parameters].select { |_k, v| v[:value].is_a?(Array) && v[:value].empty? } empty_array_parameters.each do |name, properties| - param_block_name = name.to_s.gsub(%r{^dsc_}, '') + param_block_name = name.to_s.gsub(/^dsc_/, '') params_block = params_block.gsub("#{param_block_name} = @()", "#{param_block_name} = [#{properties[:mof_type]}]@()") end # HACK: make CIM instances work: - resource[:parameters].select { |_key, hash| hash[:mof_is_embedded] && hash[:mof_type].include?('[]') }.each_value do |property_hash| + resource[:parameters].select { |_key, hash| hash[:mof_is_embedded] && hash[:mof_type].include?('[]') }.each do |_property_name, property_hash| formatted_property_hash = interpolate_variables(format(property_hash[:value])) params_block = params_block.gsub(formatted_property_hash, "[CimInstance[]]#{formatted_property_hash}") end @@ -1296,9 +1298,11 @@ def unwrap(value) end unwrapped when Array - unwrapped = value.map do |v| - unwrap(v) + unwrapped = [] + value.each do |v| + unwrapped << unwrap(v) end + unwrapped else value end @@ -1319,9 +1323,11 @@ def unwrap_string(value) end unwrapped when Array - unwrapped = value.map do |v| - unwrap_string(v) + unwrapped = [] + value.each do |v| + unwrapped << unwrap_string(v) end + unwrapped else value end @@ -1343,7 +1349,7 @@ def escape_quotes(text) # With multiple methods which need to discover secrets it is necessary to keep a single regex # which can discover them. This will lazily match everything in a single-quoted string which # ends with the secret postfix id and mark the actual contents of the string as the secret. - SECRET_DATA_REGEX = %r{'(?[^']+)+?#{Regexp.quote(SECRET_POSTFIX)}'} + SECRET_DATA_REGEX = /'(?[^']+)+?#{Regexp.quote(SECRET_POSTFIX)}'/ # Strings containing sensitive data have a secrets postfix. These strings cannot be passed # directly either to debug streams or to PowerShell and must be handled; this method contains @@ -1356,12 +1362,12 @@ def escape_quotes(text) def handle_secrets(text, replacement, error_message) # Every secret unwrapped in this module will unwrap as "'secret#{SECRET_POSTFIX}'" # Currently, no known resources specify a SecureString instead of a PSCredential object. - return text unless %r{#{Regexp.quote(SECRET_POSTFIX)}}.match?(text) + return text unless /#{Regexp.quote(SECRET_POSTFIX)}/.match?(text) # In order to reduce time-to-parse, look at each line individually and *only* attempt # to substitute if a naive match for the secret postfix is found on the line. modified_text = text.split("\n").map do |line| - if %r{#{Regexp.quote(SECRET_POSTFIX)}}.match?(line) + if /#{Regexp.quote(SECRET_POSTFIX)}/.match?(line) line.gsub(SECRET_DATA_REGEX, replacement) else line @@ -1371,7 +1377,7 @@ def handle_secrets(text, replacement, error_message) modified_text = modified_text.join("\n") # Something has gone wrong, error loudly - raise error_message if %r{#{Regexp.quote(SECRET_POSTFIX)}}.match?(modified_text) + raise error_message if /#{Regexp.quote(SECRET_POSTFIX)}/.match?(modified_text) modified_text end diff --git a/lib/pwsh.rb b/lib/pwsh.rb index e19484be..67a63f3c 100644 --- a/lib/pwsh.rb +++ b/lib/pwsh.rb @@ -34,7 +34,7 @@ def self.instances def self.default_options { debug: false, - pipe_timeout: 30, + pipe_timeout: 30 } end @@ -280,7 +280,7 @@ def make_ps_code(powershell_code, timeout_ms = nil, working_dir = nil, environme if (envlist = environment_variables) envlist = [envlist] unless envlist.is_a? Array envlist.each do |setting| - if setting =~ %r{^(\w+)=((.|\n)+)$} + if setting =~ /^(\w+)=((.|\n)+)$/ env_name = Regexp.last_match(1) value = Regexp.last_match(2) if environment.include?(env_name) || environment.include?(env_name.to_sym) @@ -489,7 +489,7 @@ def pipe_command(command) def length_prefixed_string(data) msg = data.encode(Encoding::UTF_8) # https://ruby-doc.org/core-1.9.3/Array.html#method-i-pack - [msg.bytesize].pack('V') + msg.force_encoding(Encoding::BINARY) + [msg.bytes.length].pack('V') + msg.force_encoding(Encoding::BINARY) end # Writes binary-encoded data to the PowerShell manager process via the pipe. @@ -511,7 +511,7 @@ def write_pipe(input) # @param timeout [Float] The number of seconds to wait for the pipe to be readable # @yield [String] a binary encoded string chunk # @return nil - def read_from_pipe(pipe, timeout = 0.1, &) + def read_from_pipe(pipe, timeout = 0.1, &_block) if self.class.readable?(pipe, timeout) l = pipe.readpartial(4096) # TODO: Log a debug for "#{Time.now} PIPE> #{l}" @@ -538,7 +538,7 @@ def drain_pipe_until_signaled(pipe, signal) read_from_pipe(pipe, 0) { |s| output << s } while self.class.readable?(pipe) # String has been binary up to this point, so force UTF-8 now - (output == []) ? [] : [output.join.force_encoding(Encoding::UTF_8)] + output == [] ? [] : [output.join.force_encoding(Encoding::UTF_8)] end # Open threads and pipes to read stdout and stderr from the PowerShell manager, @@ -589,8 +589,8 @@ def read_streams [ output, - (stdout == []) ? nil : stdout.join, # native stdout - stderr_reader.value, # native stderr + stdout == [] ? nil : stdout.join, # native stdout + stderr_reader.value # native stderr ] ensure # Failsafe if the prior unlock was never reached / Mutex wasn't unlocked diff --git a/lib/pwsh/util.rb b/lib/pwsh/util.rb index 21b5868a..21b01710 100644 --- a/lib/pwsh/util.rb +++ b/lib/pwsh/util.rb @@ -1,139 +1,141 @@ # frozen_string_literal: true # Manage PowerShell and Windows PowerShell via ruby -# Various helper methods -module Pwsh::Util - module_function - - # Verifies whether or not the current context is running on a Windows node. - # Implementation copied from `facets`: https://github.com/rubyworks/facets/blob/main/lib/standard/facets/rbconfig.rb - # - # @return [Bool] true if on windows - def on_windows? - host_os = RbConfig::CONFIG['host_os'] - !!(host_os =~ %r{mswin|mingw}) - end +module Pwsh + # Various helper methods + module Util + module_function + + # Verifies whether or not the current context is running on a Windows node. + # Implementation copied from `facets`: https://github.com/rubyworks/facets/blob/main/lib/standard/facets/rbconfig.rb + # + # @return [Bool] true if on windows + def on_windows? + host_os = RbConfig::CONFIG['host_os'] + !!(host_os =~ /mswin|mingw/) + end - # Verify paths specified are valid directories. - # Skips paths which do not exist. - # @return [Bool] true if any paths specified are not valid directories - def invalid_directories?(path_collection) - return false if path_collection.nil? || path_collection.empty? + # Verify paths specified are valid directories. + # Skips paths which do not exist. + # @return [Bool] true if any paths specified are not valid directories + def invalid_directories?(path_collection) + return false if path_collection.nil? || path_collection.empty? - delimiter = on_windows? ? ';' : ':' - paths = path_collection.split(delimiter) + delimiter = on_windows? ? ';' : ':' + paths = path_collection.split(delimiter) - paths.any? { |path| !path.empty? && File.exist?(path) && !File.directory?(path) } - end + paths.any? { |path| !path.empty? && File.exist?(path) && !File.directory?(path) } + end - # Return a string or symbol converted to snake_case - # - # @return [String] snake_cased string - def snake_case(object) - # Implementation copied from: https://github.com/rubyworks/facets/blob/master/lib/core/facets/string/snakecase.rb - # gsub(/::/, '/'). - should_symbolize = object.is_a?(Symbol) - raise "snake_case method only handles strings and symbols, passed a #{object.class}: #{object}" unless should_symbolize || object.is_a?(String) - - text = object.to_s - .gsub(%r{([A-Z]+)([A-Z][a-z])}, '\1_\2') - .gsub(%r{([a-z\d])([A-Z])}, '\1_\2') - .tr('-', '_') - .gsub(%r{\s}, '_') - .gsub(%r{__+}, '_') - .downcase - should_symbolize ? text.to_sym : text - end + # Return a string or symbol converted to snake_case + # + # @return [String] snake_cased string + def snake_case(object) + # Implementation copied from: https://github.com/rubyworks/facets/blob/master/lib/core/facets/string/snakecase.rb + # gsub(/::/, '/'). + should_symbolize = object.is_a?(Symbol) + raise "snake_case method only handles strings and symbols, passed a #{object.class}: #{object}" unless should_symbolize || object.is_a?(String) + + text = object.to_s + .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') + .gsub(/([a-z\d])([A-Z])/, '\1_\2') + .tr('-', '_') + .gsub(/\s/, '_') + .gsub(/__+/, '_') + .downcase + should_symbolize ? text.to_sym : text + end - # Iterate through a hashes keys, snake_casing them - # - # @return [Hash] Hash with all keys snake_cased - def snake_case_hash_keys(object) - snake_case_proc = proc { |key| snake_case(key) } - apply_key_mutator(object, snake_case_proc) - end + # Iterate through a hashes keys, snake_casing them + # + # @return [Hash] Hash with all keys snake_cased + def snake_case_hash_keys(object) + snake_case_proc = proc { |key| snake_case(key) } + apply_key_mutator(object, snake_case_proc) + end - # Return a string or symbol converted to PascalCase - # - # @return [String] PascalCased string - def pascal_case(object) - should_symbolize = object.is_a?(Symbol) - raise "snake_case method only handles strings and symbols, passed a #{object.class}: #{object}" unless should_symbolize || object.is_a?(String) + # Return a string or symbol converted to PascalCase + # + # @return [String] PascalCased string + def pascal_case(object) + should_symbolize = object.is_a?(Symbol) + raise "snake_case method only handles strings and symbols, passed a #{object.class}: #{object}" unless should_symbolize || object.is_a?(String) - # Break word boundaries to snake case first - text = snake_case(object.to_s).split('_').collect(&:capitalize).join - should_symbolize ? text.to_sym : text - end + # Break word boundaries to snake case first + text = snake_case(object.to_s).split('_').collect(&:capitalize).join + should_symbolize ? text.to_sym : text + end - # Iterate through a hashes keys, PascalCasing them - # - # @return [Hash] Hash with all keys PascalCased - def pascal_case_hash_keys(object) - pascal_case_proc = proc { |key| pascal_case(key) } - apply_key_mutator(object, pascal_case_proc) - end + # Iterate through a hashes keys, PascalCasing them + # + # @return [Hash] Hash with all keys PascalCased + def pascal_case_hash_keys(object) + pascal_case_proc = proc { |key| pascal_case(key) } + apply_key_mutator(object, pascal_case_proc) + end - # Ensure that quotes inside a passed string will continue to be passed - # - # @return [String] the string with quotes escaped - def escape_quotes(text) - text.gsub("'", "''") - end + # Ensure that quotes inside a passed string will continue to be passed + # + # @return [String] the string with quotes escaped + def escape_quotes(text) + text.gsub("'", "''") + end - # Ensure that all keys in a hash are symbols, not strings. - # - # @return [Hash] a hash whose keys have been converted to symbols. - def symbolize_hash_keys(object) - symbolize_proc = proc(&:to_sym) - apply_key_mutator(object, symbolize_proc) - end + # Ensure that all keys in a hash are symbols, not strings. + # + # @return [Hash] a hash whose keys have been converted to symbols. + def symbolize_hash_keys(object) + symbolize_proc = proc(&:to_sym) + apply_key_mutator(object, symbolize_proc) + end - def apply_key_mutator(object, proc) - return object.map { |item| apply_key_mutator(item, proc) } if object.is_a?(Array) - return object unless object.is_a?(Hash) + def apply_key_mutator(object, proc) + return object.map { |item| apply_key_mutator(item, proc) } if object.is_a?(Array) + return object unless object.is_a?(Hash) - modified_hash = {} - object.each do |key, value| - modified_hash[proc.call(key)] = apply_key_mutator(value, proc) + modified_hash = {} + object.each do |key, value| + modified_hash[proc.call(key)] = apply_key_mutator(value, proc) + end + modified_hash end - modified_hash - end - private_class_method :apply_key_mutator - - # Convert a ruby value into a string to be passed along to PowerShell for interpolation in a command - # Handles: - # - Strings - # - Numbers - # - Booleans - # - Symbols - # - Arrays - # - Hashes - # - # @return [String] representation of the value for interpolation - def format_powershell_value(object) - if [true, false].include?(object) || ['trueclass', 'falseclass'].include?(object.class.name.downcase) - "$#{object}" - elsif object.instance_of?(Symbol) || object.class.ancestors.include?(Numeric) - object.to_s - elsif object.instance_of?(String) - "'#{escape_quotes(object)}'" - elsif object.instance_of?(Array) - "@(#{object.collect { |item| format_powershell_value(item) }.join(', ')})" - elsif object.instance_of?(Hash) - "@{#{object.collect { |k, v| "#{format_powershell_value(k)} = #{format_powershell_value(v)}" }.join('; ')}}" - else - raise "unsupported type #{object.class} of value '#{object}'" + private_class_method :apply_key_mutator + + # Convert a ruby value into a string to be passed along to PowerShell for interpolation in a command + # Handles: + # - Strings + # - Numbers + # - Booleans + # - Symbols + # - Arrays + # - Hashes + # + # @return [String] representation of the value for interpolation + def format_powershell_value(object) + if %i[true false].include?(object) || %w[trueclass falseclass].include?(object.class.name.downcase) + "$#{object}" + elsif object.instance_of?(Symbol) || object.class.ancestors.include?(Numeric) + object.to_s + elsif object.instance_of?(String) + "'#{escape_quotes(object)}'" + elsif object.instance_of?(Array) + "@(#{object.collect { |item| format_powershell_value(item) }.join(', ')})" + elsif object.instance_of?(Hash) + "@{#{object.collect { |k, v| "#{format_powershell_value(k)} = #{format_powershell_value(v)}" }.join('; ')}}" + else + raise "unsupported type #{object.class} of value '#{object}'" + end end - end - # Return the representative string of a PowerShell hash for a custom object property to be used in selecting or filtering. - # The script block for the expression must be passed as the string you want interpolated into the hash; this method does - # not do any of the additional work of interpolation for you as the type sits inside a code block inside a hash. - # - # @return [String] representation of a PowerShell hash with the keys 'Name' and 'Expression' - def custom_powershell_property(name, expression) - "@{Name = '#{name}'; Expression = {#{expression}}}" + # Return the representative string of a PowerShell hash for a custom object property to be used in selecting or filtering. + # The script block for the expression must be passed as the string you want interpolated into the hash; this method does + # not do any of the additional work of interpolation for you as the type sits inside a code block inside a hash. + # + # @return [String] representation of a PowerShell hash with the keys 'Name' and 'Expression' + def custom_powershell_property(name, expression) + "@{Name = '#{name}'; Expression = {#{expression}}}" + end end end diff --git a/lib/pwsh/windows_powershell.rb b/lib/pwsh/windows_powershell.rb index d21b255e..e39b92bc 100644 --- a/lib/pwsh/windows_powershell.rb +++ b/lib/pwsh/windows_powershell.rb @@ -2,103 +2,107 @@ require File.join(File.dirname(__FILE__), 'util') -# Returns information about the available versions of Windows PowerShell on the node, if any. -class Pwsh::WindowsPowerShell - # Return whether or not the latest version of PowerShell available on the machine - # is compatible with the implementation of the Manager. - def self.compatible_version? - # If this method isn't defined, we're not on Windows! - return false if defined?(Pwsh::WindowsPowerShell.version).nil? +module Pwsh + # Returns information about the available versions of Windows PowerShell on the node, if any. + class WindowsPowerShell + # Return whether or not the latest version of PowerShell available on the machine + # is compatible with the implementation of the Manager. + def self.compatible_version? + # If this method isn't defined, we're not on Windows! + return false if defined?(Pwsh::WindowsPowerShell.version).nil? - powershell_version = defined?(Pwsh::WindowsPowerShell.version) ? Pwsh::WindowsPowerShell.version : nil + powershell_version = defined?(Pwsh::WindowsPowerShell.version) ? Pwsh::WindowsPowerShell.version : nil - # If we get nil, something's gone wrong and we're not compatible. - return false if powershell_version.nil? + # If we get nil, something's gone wrong and we're not compatible. + return false if powershell_version.nil? - # PowerShell v1 - definitely not good to go. Really the whole library - # may not even work but I digress - return false if Gem::Version.new(powershell_version) < Gem::Version.new(2) + # PowerShell v1 - definitely not good to go. Really the whole library + # may not even work but I digress + return false if Gem::Version.new(powershell_version) < Gem::Version.new(2) - # PowerShell v3+, we are good to go b/c .NET 4+ - # https://msdn.microsoft.com/en-us/powershell/scripting/setup/windows-powershell-system-requirements - # Look at Microsoft .NET Framwork Requirements section. - return true if Gem::Version.new(powershell_version) >= Gem::Version.new(3) + # PowerShell v3+, we are good to go b/c .NET 4+ + # https://msdn.microsoft.com/en-us/powershell/scripting/setup/windows-powershell-system-requirements + # Look at Microsoft .NET Framwork Requirements section. + return true if Gem::Version.new(powershell_version) >= Gem::Version.new(3) - # If we are using PowerShell v2, we need to see what the latest - # version of .NET is that we have - # https://msdn.microsoft.com/en-us/library/hh925568.aspx - value = false - if Pwsh::Util.on_windows? - require 'win32/registry' - begin - # At this point in the check, PowerShell is using .NET Framework - # 2.x family, so we only need to verify v3.5 key exists. - # If we were verifying all compatible types we would look for - # any of these keys: v3.5, v4.0, v4 - Win32::Registry::HKEY_LOCAL_MACHINE.open('SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5', Win32::Registry::KEY_READ | 0x100) do - value = true + # If we are using PowerShell v2, we need to see what the latest + # version of .NET is that we have + # https://msdn.microsoft.com/en-us/library/hh925568.aspx + value = false + if Pwsh::Util.on_windows? + require 'win32/registry' + begin + # At this point in the check, PowerShell is using .NET Framework + # 2.x family, so we only need to verify v3.5 key exists. + # If we were verifying all compatible types we would look for + # any of these keys: v3.5, v4.0, v4 + Win32::Registry::HKEY_LOCAL_MACHINE.open('SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5', Win32::Registry::KEY_READ | 0x100) do + value = true + end + rescue Win32::Registry::Error + value = false end - rescue Win32::Registry::Error - value = false end - end - value + value + end end end if Pwsh::Util.on_windows? require 'win32/registry' - # Returns information about the available versions of Windows PowerShell on the node, if any. - class Pwsh::WindowsPowerShell - # Shorthand constant to reference the registry key access type - ACCESS_TYPE = Win32::Registry::KEY_READ | 0x100 - # Shorthand constant to reference the local machine hive - HKLM = Win32::Registry::HKEY_LOCAL_MACHINE - # The path to the original version of the Windows PowerShell Engine's data in registry - PS_ONE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine' - # The path to the newer version of the Windows PowerShell Engine's data in registry - PS_THREE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine' - # The name of the registry key for looking up the latest version of Windows PowerShell for a given engine. - REG_KEY = 'PowerShellVersion' + module Pwsh + # Returns information about the available versions of Windows PowerShell on the node, if any. + class WindowsPowerShell + # Shorthand constant to reference the registry key access type + ACCESS_TYPE = Win32::Registry::KEY_READ | 0x100 + # Shorthand constant to reference the local machine hive + HKLM = Win32::Registry::HKEY_LOCAL_MACHINE + # The path to the original version of the Windows PowerShell Engine's data in registry + PS_ONE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine' + # The path to the newer version of the Windows PowerShell Engine's data in registry + PS_THREE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine' + # The name of the registry key for looking up the latest version of Windows PowerShell for a given engine. + REG_KEY = 'PowerShellVersion' - # Returns the latest available version of Windows PowerShell on the machine - # - # @return [String] a version string representing the latest version of Windows PowerShell available - def self.version - powershell_three_version || powershell_one_version - end + # Returns the latest available version of Windows PowerShell on the machine + # + # @return [String] a version string representing the latest version of Windows PowerShell available + def self.version + powershell_three_version || powershell_one_version + end - # Returns the latest available version of Windows PowerShell using the older - # engine as determined by checking the registry. - # - # @return [String] a version string representing the latest version of Windows PowerShell using the original engine - def self.powershell_one_version - version = nil - begin - HKLM.open(PS_ONE_REG_PATH, ACCESS_TYPE) do |reg| - version = reg[REG_KEY] - end - rescue StandardError + # Returns the latest available version of Windows PowerShell using the older + # engine as determined by checking the registry. + # + # @return [String] a version string representing the latest version of Windows PowerShell using the original engine + def self.powershell_one_version version = nil + begin + HKLM.open(PS_ONE_REG_PATH, ACCESS_TYPE) do |reg| + version = reg[REG_KEY] + end + rescue StandardError + version = nil + end + version end - version - end - # Returns the latest available version of Windows PowerShell as determined by - # checking the registry. - # - # @return [String] a version string representing the latest version of Windows PowerShell using the newer engine - def self.powershell_three_version - version = nil - begin - HKLM.open(PS_THREE_REG_PATH, ACCESS_TYPE) do |reg| - version = reg[REG_KEY] - end - rescue StandardError + # Returns the latest available version of Windows PowerShell as determined by + # checking the registry. + # + # @return [String] a version string representing the latest version of Windows PowerShell using the newer engine + def self.powershell_three_version version = nil + begin + HKLM.open(PS_THREE_REG_PATH, ACCESS_TYPE) do |reg| + version = reg[REG_KEY] + end + rescue StandardError + version = nil + end + version end - version end end end From d1b8c2735923c6d6692c091ac3e17389bcb775d8 Mon Sep 17 00:00:00 2001 From: Gavin Didrichsen Date: Mon, 26 Oct 2026 12:52:07 +0000 Subject: [PATCH 06/10] (MAINT) Keep Style/ClassAndModuleChildren disabled through .sync.yml main disabled Style/ClassAndModuleChildren in its hand-edited .rubocop.yml. The template profile enforces compact style, which is what broke the module loading in 0db45e4. Disable it with a cop_overrides entry in .sync.yml so the setting survives the next pdk update. Co-Authored-By: Claude Sonnet 5.5 --- .rubocop.yml | 1 + .sync.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index ed02062f..516a29e7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -51,6 +51,7 @@ Style/BlockDelimiters: Style/ClassAndModuleChildren: Description: Compact style reduces the required amount of indentation. EnforcedStyle: compact + Enabled: false Style/EmptyElse: Description: Enforce against empty else clauses, but allow `nil` for clarity. EnforcedStyle: empty diff --git a/.sync.yml b/.sync.yml index b2830046..a280100b 100644 --- a/.sync.yml +++ b/.sync.yml @@ -20,3 +20,6 @@ Rakefile: .rubocop.yml: selected_profile: 'on' include_todos: true + cop_overrides: + Style/ClassAndModuleChildren: + Enabled: false From 942c66874a2271fdc37318af6178d3e125804759 Mon Sep 17 00:00:00 2001 From: Gavin Didrichsen Date: Mon, 26 Oct 2026 13:01:13 +0000 Subject: [PATCH 07/10] (MAINT) Apply the safe rubocop autocorrects to lib/ Apply only the autocorrects rubocop marks safe, in one pass: Style/RegexpLiteral, Style/TernaryParentheses, Style/TrailingCommaIn*Literal, Style/SymbolArray, Style/WordArray and Style/BlockDelimiters (37 offences in lib/pwsh.rb, lib/pwsh/util.rb and the dsc_base_provider). No module or class structure changes. Every touched file passes `ruby -c`, and the four spec files that run without a pwsh binary pass (231 examples, 0 failures, 1 pending). Co-Authored-By: Claude Sonnet 5.5 --- .../dsc_base_provider/dsc_base_provider.rb | 52 +++++++++---------- lib/pwsh.rb | 10 ++-- lib/pwsh/util.rb | 12 ++--- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb b/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb index f33dea86..d9e44849 100644 --- a/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb +++ b/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb @@ -32,10 +32,10 @@ def initialize # @param hashes [Array] the list of hashes to search the cache for # @return [Array] an array containing the matching hashes for the search condition, if any def fetch_cached_hashes(cache, hashes) - cache.reject do |item| + cache.reject { |item| matching_hash = hashes.select { |hash| (item.to_a - hash.to_a).empty? || (hash.to_a - item.to_a).empty? } matching_hash.empty? - end.flatten + }.flatten end # Implements the canonicalize feature of the Resource API; this method is called first against any resources @@ -306,7 +306,7 @@ def perform_fresh_get(context, name, should) next unless valid_attributes.include?(type_key.to_s) # Sanitize .NET serialization artifacts (e.g., "System.Object[]") - if value.is_a?(String) && value =~ /^System\.\w+\[\]$/ + if value.is_a?(String) && value =~ %r{^System\.\w+\[\]$} result[type_key] = nil else # Normalize CIM instances the same way invoke_get_method does, @@ -403,7 +403,7 @@ def invoke_dsc_resource(context, name_hash, props, method) if output[:stdout].nil? message = 'Nothing returned.' - message += " #{output[:errormessage]}" if output[:errormessage]&.match?(/PowerShell module timeout \(\d+ ms\) exceeded while executing/) + message += " #{output[:errormessage]}" if output[:errormessage]&.match?(%r{PowerShell module timeout \(\d+ ms\) exceeded while executing}) context.err(message) return nil end @@ -415,7 +415,7 @@ def invoke_dsc_resource(context, name_hash, props, method) return nil end context.debug("raw data received: #{data.inspect}") - collision_error_matcher = /The Invoke-DscResource cmdlet is in progress and must return before Invoke-DscResource can be invoked/ + collision_error_matcher = %r{The Invoke-DscResource cmdlet is in progress and must return before Invoke-DscResource can be invoked} error = data['errormessage'] @@ -684,7 +684,7 @@ def canonicalize_get_value!(context, data, type_key, query_props) end # Convert DateTime back to appropriate type - if context.type.attributes[type_key][:mof_type] =~ /DateTime/i && !data[type_key].nil? + if context.type.attributes[type_key][:mof_type] =~ %r{DateTime}i && !data[type_key].nil? data[type_key] = begin Puppet::Pops::Time::Timestamp.parse(data[type_key]) rescue ArgumentError, TypeError => e @@ -700,7 +700,7 @@ def canonicalize_get_value!(context, data, type_key, query_props) # Sanitize .NET serialization artifacts (e.g., "System.Object[]") to nil. def sanitize_dotnet_artifacts!(context, data) data.each do |key, value| - next unless value.is_a?(String) && value =~ /^System\.\w+\[\]$/ + next unless value.is_a?(String) && value =~ %r{^System\.\w+\[\]$} context.debug("Sanitizing .NET serialization artifact for #{key}: #{value} -> nil") data[key] = nil @@ -727,7 +727,7 @@ def preserve_namevar_values!(context, data, name_hash) def invoke_set_method(context, name, should) context.debug("Invoking Set Method for '#{name}' with #{should.inspect}") - apply_props = should.select { |k, _v| k.to_s =~ /^dsc_/ } + apply_props = should.select { |k, _v| k.to_s =~ %r{^dsc_} } invoke_dsc_resource(context, should, apply_props, 'set') # TODO: Implement this functionality for notifying a DSC reboot? @@ -745,7 +745,7 @@ def invoke_test_method(context, name, should) context.debug("Relying on DSC Test method for validating if '#{name}' is in the desired state") context.debug("Invoking Test Method for '#{name}' with #{should.inspect}") - test_props = should.select { |k, _v| k.to_s =~ /^dsc_/ } + test_props = should.select { |k, _v| k.to_s =~ %r{^dsc_} } data = invoke_dsc_resource(context, name, test_props, 'test') # Something went wrong with Invoke-DscResource; fall back on property state comparisons return nil if data.nil? @@ -770,7 +770,7 @@ def invoke_test_method(context, name, should) def invocable_resource(should, context, dsc_invoke_method) resource = {} resource[:parameters] = {} - %i[name dscmeta_resource_friendly_name dscmeta_resource_name dscmeta_resource_implementation dscmeta_module_name dscmeta_module_version].each do |k| + [:name, :dscmeta_resource_friendly_name, :dscmeta_resource_name, :dscmeta_resource_implementation, :dscmeta_module_name, :dscmeta_module_version].each do |k| resource[k] = context.type.definition[k] end should.each do |k, v| @@ -780,7 +780,7 @@ def invocable_resource(should, context, dsc_invoke_method) resource[:parameters][k] = {} resource[:parameters][k][:value] = v - %i[mof_type mof_is_embedded].each do |ky| + [:mof_type, :mof_is_embedded].each do |ky| resource[:parameters][k][ky] = context.type.definition[:attributes][k][ky] end end @@ -841,9 +841,9 @@ def puppetize_name(name) # Puppet module names must be lower case name = name.downcase # Puppet module names must only include lowercase letters, digits and underscores - name = name.gsub(/[^a-z0-9_]/, '_') + name = name.gsub(%r{[^a-z0-9_]}, '_') # Puppet module names must not start with a number so if it does, append the letter 'a' to the name. Eg: '7zip' becomes 'a7zip' - name = name.match?(/^\d/) ? "a#{name}" : name # rubocop:disable Lint/UselessAssignment + name = name.match?(%r{^\d}) ? "a#{name}" : name # rubocop:disable Lint/UselessAssignment end # Return a UUID with the dashes turned into underscores to enable the specifying of guaranteed-unique @@ -1021,7 +1021,7 @@ def enum_values(context, attribute) return [] unless type_string&.include?('Enum[') # Extract the enum values from the type string - enum_content = type_string.match(/Enum\[(.*?)\]/)&.[](1) + enum_content = type_string.match(%r{Enum\[(.*?)\]})&.[](1) # Return an empty array if we couldn't find the enum values return [] if enum_content.nil? @@ -1080,13 +1080,13 @@ def prepare_credentials(resource) variable_name = random_variable_name credential_hash = { 'user' => property_hash[:value]['user'], - 'password' => escape_quotes(unwrap_string(property_hash[:value]['password'])) + 'password' => escape_quotes(unwrap_string(property_hash[:value]['password'])), } credentials_block << format_pscredential(variable_name, credential_hash) instantiated_variables.merge!(variable_name => credential_hash) end credentials_block.join("\n") - credentials_block == [] ? '' : credentials_block + (credentials_block == []) ? '' : credentials_block end # Write a line of PowerShell which creates a PSCredential object and assigns it to a variable @@ -1128,7 +1128,7 @@ def prepare_cim_instances(resource) end end # We have to handle arrays of CIM instances slightly differently - if /\[\]$/.match?(property_hash[:mof_type]) + if %r{\[\]$}.match?(property_hash[:mof_type]) class_name = property_hash[:mof_type].gsub('[]', '') property_hash[:value].each do |hash| variable_name = random_variable_name @@ -1142,7 +1142,7 @@ def prepare_cim_instances(resource) instantiated_variables.merge!(variable_name => property_hash[:value]) end end - cim_instances_block == [] ? '' : cim_instances_block.join("\n") + (cim_instances_block == []) ? '' : cim_instances_block.join("\n") end # Recursively search for and return CIM instances nested in an enumerable @@ -1189,7 +1189,7 @@ def invoke_params(resource) # rubocop:disable Metrics/MethodLength params = { Name: resource[:dscmeta_resource_friendly_name], Method: resource[:dsc_invoke_method], - Property: {} + Property: {}, } if resource.key?(:dscmeta_module_version) params[:ModuleName] = {} @@ -1208,14 +1208,14 @@ def invoke_params(resource) # rubocop:disable Metrics/MethodLength next if property_name == :dsc_timeout # strip dsc_ from the beginning of the property name declaration - name = property_name.to_s.gsub(/^dsc_/, '').to_sym + name = property_name.to_s.gsub(%r{^dsc_}, '').to_sym params[:Property][name] = case property_hash[:mof_type] when 'PSCredential' # format can't unwrap Sensitive strings nested in arbitrary hashes/etc, so make # the Credential hash interpolable as it will be replaced by a variable reference. { 'user' => property_hash[:value]['user'], - 'password' => escape_quotes(unwrap_string(property_hash[:value]['password'])) + 'password' => escape_quotes(unwrap_string(property_hash[:value]['password'])), } when 'DateTime' # These have to be handled specifically because they rely on the *Puppet* DateTime, @@ -1232,7 +1232,7 @@ def invoke_params(resource) # rubocop:disable Metrics/MethodLength # CIM instances do not do a consistent job of casting an empty array properly. empty_array_parameters = resource[:parameters].select { |_k, v| v[:value].is_a?(Array) && v[:value].empty? } empty_array_parameters.each do |name, properties| - param_block_name = name.to_s.gsub(/^dsc_/, '') + param_block_name = name.to_s.gsub(%r{^dsc_}, '') params_block = params_block.gsub("#{param_block_name} = @()", "#{param_block_name} = [#{properties[:mof_type]}]@()") end # HACK: make CIM instances work: @@ -1349,7 +1349,7 @@ def escape_quotes(text) # With multiple methods which need to discover secrets it is necessary to keep a single regex # which can discover them. This will lazily match everything in a single-quoted string which # ends with the secret postfix id and mark the actual contents of the string as the secret. - SECRET_DATA_REGEX = /'(?[^']+)+?#{Regexp.quote(SECRET_POSTFIX)}'/ + SECRET_DATA_REGEX = %r{'(?[^']+)+?#{Regexp.quote(SECRET_POSTFIX)}'} # Strings containing sensitive data have a secrets postfix. These strings cannot be passed # directly either to debug streams or to PowerShell and must be handled; this method contains @@ -1362,12 +1362,12 @@ def escape_quotes(text) def handle_secrets(text, replacement, error_message) # Every secret unwrapped in this module will unwrap as "'secret#{SECRET_POSTFIX}'" # Currently, no known resources specify a SecureString instead of a PSCredential object. - return text unless /#{Regexp.quote(SECRET_POSTFIX)}/.match?(text) + return text unless %r{#{Regexp.quote(SECRET_POSTFIX)}}.match?(text) # In order to reduce time-to-parse, look at each line individually and *only* attempt # to substitute if a naive match for the secret postfix is found on the line. modified_text = text.split("\n").map do |line| - if /#{Regexp.quote(SECRET_POSTFIX)}/.match?(line) + if %r{#{Regexp.quote(SECRET_POSTFIX)}}.match?(line) line.gsub(SECRET_DATA_REGEX, replacement) else line @@ -1377,7 +1377,7 @@ def handle_secrets(text, replacement, error_message) modified_text = modified_text.join("\n") # Something has gone wrong, error loudly - raise error_message if /#{Regexp.quote(SECRET_POSTFIX)}/.match?(modified_text) + raise error_message if %r{#{Regexp.quote(SECRET_POSTFIX)}}.match?(modified_text) modified_text end diff --git a/lib/pwsh.rb b/lib/pwsh.rb index 67a63f3c..8769d127 100644 --- a/lib/pwsh.rb +++ b/lib/pwsh.rb @@ -34,7 +34,7 @@ def self.instances def self.default_options { debug: false, - pipe_timeout: 30 + pipe_timeout: 30, } end @@ -280,7 +280,7 @@ def make_ps_code(powershell_code, timeout_ms = nil, working_dir = nil, environme if (envlist = environment_variables) envlist = [envlist] unless envlist.is_a? Array envlist.each do |setting| - if setting =~ /^(\w+)=((.|\n)+)$/ + if setting =~ %r{^(\w+)=((.|\n)+)$} env_name = Regexp.last_match(1) value = Regexp.last_match(2) if environment.include?(env_name) || environment.include?(env_name.to_sym) @@ -538,7 +538,7 @@ def drain_pipe_until_signaled(pipe, signal) read_from_pipe(pipe, 0) { |s| output << s } while self.class.readable?(pipe) # String has been binary up to this point, so force UTF-8 now - output == [] ? [] : [output.join.force_encoding(Encoding::UTF_8)] + (output == []) ? [] : [output.join.force_encoding(Encoding::UTF_8)] end # Open threads and pipes to read stdout and stderr from the PowerShell manager, @@ -589,8 +589,8 @@ def read_streams [ output, - stdout == [] ? nil : stdout.join, # native stdout - stderr_reader.value # native stderr + (stdout == []) ? nil : stdout.join, # native stdout + stderr_reader.value, # native stderr ] ensure # Failsafe if the prior unlock was never reached / Mutex wasn't unlocked diff --git a/lib/pwsh/util.rb b/lib/pwsh/util.rb index 21b01710..fe74db71 100644 --- a/lib/pwsh/util.rb +++ b/lib/pwsh/util.rb @@ -12,7 +12,7 @@ module Util # @return [Bool] true if on windows def on_windows? host_os = RbConfig::CONFIG['host_os'] - !!(host_os =~ /mswin|mingw/) + !!(host_os =~ %r{mswin|mingw}) end # Verify paths specified are valid directories. @@ -37,11 +37,11 @@ def snake_case(object) raise "snake_case method only handles strings and symbols, passed a #{object.class}: #{object}" unless should_symbolize || object.is_a?(String) text = object.to_s - .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') - .gsub(/([a-z\d])([A-Z])/, '\1_\2') + .gsub(%r{([A-Z]+)([A-Z][a-z])}, '\1_\2') + .gsub(%r{([a-z\d])([A-Z])}, '\1_\2') .tr('-', '_') - .gsub(/\s/, '_') - .gsub(/__+/, '_') + .gsub(%r{\s}, '_') + .gsub(%r{__+}, '_') .downcase should_symbolize ? text.to_sym : text end @@ -113,7 +113,7 @@ def apply_key_mutator(object, proc) # # @return [String] representation of the value for interpolation def format_powershell_value(object) - if %i[true false].include?(object) || %w[trueclass falseclass].include?(object.class.name.downcase) + if [:true, :false].include?(object) || ['trueclass', 'falseclass'].include?(object.class.name.downcase) "$#{object}" elsif object.instance_of?(Symbol) || object.class.ancestors.include?(Numeric) object.to_s From 5d3a1cbfe4bad39f28e5ccc4653f7c6fb7a10f41 Mon Sep 17 00:00:00 2001 From: Gavin Didrichsen Date: Mon, 26 Oct 2026 13:01:59 +0000 Subject: [PATCH 08/10] (MAINT) Fix the remaining correctable offences in lib/ - Performance/StringBytesize: `msg.bytes.length` becomes `msg.bytesize`, the same value without building an array. - Lint/BooleanSymbol: the `:true`/`:false` symbols in format_powershell_value are intentional (Puppet can pass them), so keep them and say so with an inline disable. Style/HashSlice is not applied: slice calls namevar_attributes once instead of once per entry, which changes the call count a unit test asserts (3 calls expected, 1 made). Co-Authored-By: Claude Sonnet 5.5 --- lib/pwsh.rb | 2 +- lib/pwsh/util.rb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pwsh.rb b/lib/pwsh.rb index 8769d127..8f2f85d9 100644 --- a/lib/pwsh.rb +++ b/lib/pwsh.rb @@ -489,7 +489,7 @@ def pipe_command(command) def length_prefixed_string(data) msg = data.encode(Encoding::UTF_8) # https://ruby-doc.org/core-1.9.3/Array.html#method-i-pack - [msg.bytes.length].pack('V') + msg.force_encoding(Encoding::BINARY) + [msg.bytesize].pack('V') + msg.force_encoding(Encoding::BINARY) end # Writes binary-encoded data to the PowerShell manager process via the pipe. diff --git a/lib/pwsh/util.rb b/lib/pwsh/util.rb index fe74db71..0a57c417 100644 --- a/lib/pwsh/util.rb +++ b/lib/pwsh/util.rb @@ -113,7 +113,9 @@ def apply_key_mutator(object, proc) # # @return [String] representation of the value for interpolation def format_powershell_value(object) - if [:true, :false].include?(object) || ['trueclass', 'falseclass'].include?(object.class.name.downcase) + # `:true`/`:false` are the boolean symbols Puppet can pass, so the symbol names are intended + if [:true, :false].include?(object) || # rubocop:disable Lint/BooleanSymbol + ['trueclass', 'falseclass'].include?(object.class.name.downcase) "$#{object}" elsif object.instance_of?(Symbol) || object.class.ancestors.include?(Numeric) object.to_s From 93a4a058f841ef2183a374b7e04447eed79c0d5d Mon Sep 17 00:00:00 2001 From: Gavin Didrichsen Date: Mon, 26 Oct 2026 13:02:43 +0000 Subject: [PATCH 09/10] (MAINT) Apply RSpec/ReceiveMessages in the dsc_base_provider spec Merge repeated `allow(...).to receive` stubs into receive_messages (27 offences). The unsafe autocorrect put the general stubs after the specific `with` stubs, so they overrode them and three ps_script_content examples failed. Keep the general stubs first so the specific ones win. 231 examples, 0 failures, 1 pending. Co-Authored-By: Claude Sonnet 5.5 --- .../dsc_base_provider_spec.rb | 40 ++++++------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb b/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb index 328bba3d..f3fbf10d 100644 --- a/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb +++ b/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb @@ -102,8 +102,7 @@ before(:each) do allow(context).to receive(:debug) - allow(provider).to receive(:namevar_attributes).and_return(namevar_keys) - allow(provider).to receive(:fetch_cached_hashes).and_return(cached_canonicalized_resource) + allow(provider).to receive_messages(namevar_attributes: namevar_keys, fetch_cached_hashes: cached_canonicalized_resource) end context 'when a manifest resource has meta parameters' do @@ -144,8 +143,7 @@ context 'when invoke_get_method returns a resource' do before(:each) do - allow(provider).to receive(:parameter_attributes).and_return(parameter_keys) - allow(provider).to receive(:enum_attributes).and_return([]) + allow(provider).to receive_messages(parameter_attributes: parameter_keys, enum_attributes: []) end context 'when canonicalizing property values' do @@ -290,8 +288,7 @@ before(:each) do allow(context).to receive(:type).and_return(type) - allow(type).to receive(:namevars).and_return([:name, :dsc_name]) - allow(type).to receive(:attributes).and_return(attributes) + allow(type).to receive_messages(namevars: [:name, :dsc_name], attributes: attributes) end context 'when the resource is not ensurable' do @@ -430,8 +427,7 @@ context 'when DSC Test says in sync' do it 'returns true for any property' do - allow(provider).to receive(:fetch_cached_hashes).and_return([]) - allow(provider).to receive(:invoke_test_method).and_return(true) + allow(provider).to receive_messages(fetch_cached_hashes: [], invoke_test_method: true) expect(provider.send(:insync?, context, name, property_name, is_hash, should_hash)).to be true end @@ -445,8 +441,7 @@ context 'when DSC Test says out of sync' do before(:each) do - allow(provider).to receive(:fetch_cached_hashes).and_return([]) - allow(provider).to receive(:invoke_test_method).and_return([false, 'not in desired state']) + allow(provider).to receive_messages(fetch_cached_hashes: [], invoke_test_method: [false, 'not in desired state']) end it 'returns fresh Get comparison result for dsc_ properties' do @@ -512,9 +507,7 @@ let(:should_hash_resource) { { name: 'foo', dsc_setting: 'Foo', validation_mode: 'resource' } } it 'returns [false, msg] and sets insync_property_cache' do - allow(provider).to receive(:fetch_cached_hashes).and_return([]) - allow(provider).to receive(:invoke_test_method).and_return([false, 'not in desired state']) - allow(provider).to receive(:get_cached_fresh_state).and_return(nil) + allow(provider).to receive_messages(fetch_cached_hashes: [], invoke_test_method: [false, 'not in desired state'], get_cached_fresh_state: nil) result = provider.send(:insync?, context, name, :dsc_setting, is_hash, should_hash_resource) expect(result).to be_an(Array) expect(result.first).to be false @@ -763,12 +756,11 @@ before(:each) do allow(context).to receive(:debug) - allow(provider).to receive(:mandatory_get_attributes).and_return(mandatory_get_attributes) allow(provider).to receive(:invocable_resource).with(query_props, context, 'get').and_return(resource) allow(provider).to receive(:ps_script_content).with(resource).and_return(script) allow(provider).to receive(:redact_secrets).with(script) allow(provider).to receive(:remove_secret_identifiers).with(script).and_return(script) - allow(provider).to receive(:ps_manager).and_return(ps_manager) + allow(provider).to receive_messages(mandatory_get_attributes: mandatory_get_attributes, ps_manager: ps_manager) allow(context).to receive(:type).and_return(type) allow(type).to receive(:attributes).and_return(attributes) end @@ -925,8 +917,7 @@ before(:each) do allow(ps_manager).to receive(:execute).with(script, nil).and_return({ stdout: 'DSC Data' }) allow(JSON).to receive(:parse).with('DSC Data').and_return(parsed_invocation_data) - allow(provider).to receive(:fetch_cached_hashes).and_return([]) - allow(provider).to receive(:namevar_attributes).and_return([:name, :dsc_name]) + allow(provider).to receive_messages(fetch_cached_hashes: [], namevar_attributes: [:name, :dsc_name]) end it 'preserves the namevar value from the input name_hash' do @@ -1151,8 +1142,7 @@ it 'calls invoke_get_method and caches to @cached_canonicalize_results' do result = { name: 'foo', dsc_name: 'foo', dsc_setting: 'bar' } - allow(provider).to receive(:invoke_get_method).and_return(result) - allow(provider).to receive(:fetch_cached_hashes).and_return([]) + allow(provider).to receive_messages(invoke_get_method: result, fetch_cached_hashes: []) provider.invoke_get_method_for_canonicalize(context, { name: 'foo', dsc_name: 'foo' }) expect(provider.instance_variable_get(:@cached_canonicalize_results)).to include(result) @@ -2193,13 +2183,11 @@ allow(preamble_file_handle).to receive(:read).and_return('Preamble Block') allow(File).to receive(:new).with("#{template_path}/invoke_dsc_resource_postscript.ps1").and_return(postscript_file_handle) allow(postscript_file_handle).to receive(:read).and_return('Postscript Block') - allow(provider).to receive(:munge_psmodulepath).and_return('') + # general stubs first: the specific `with` stubs below must win + allow(provider).to receive_messages(munge_psmodulepath: '', prepare_credentials: '', prepare_cim_instances: '', invoke_params: 'Parameters Block') allow(provider).to receive(:munge_psmodulepath).with('ClassBasedResource').and_return('PSModulePath Block') - allow(provider).to receive(:prepare_credentials).and_return('') allow(provider).to receive(:prepare_credentials).with('ResourceWithCredentials').and_return('Credential Block') - allow(provider).to receive(:prepare_cim_instances).and_return('') allow(provider).to receive(:prepare_cim_instances).with('ResourceWithCimInstances').and_return('Cim Instance Block') - allow(provider).to receive(:invoke_params).and_return('Parameters Block') end it 'returns a powershell script with the helper functions' do @@ -2457,8 +2445,7 @@ describe '.ps_manager on non-Windows' do before(:each) do allow(Pwsh::Util).to receive(:on_windows?).and_return(false) - allow(Pwsh::Manager).to receive(:pwsh_path).and_return('pwsh') - allow(Pwsh::Manager).to receive(:pwsh_args).and_return('args') + allow(Pwsh::Manager).to receive_messages(pwsh_path: 'pwsh', pwsh_args: 'args') end it 'Initializes an instance of the Pwsh::Manager' do @@ -2477,8 +2464,7 @@ describe '.ps_manager on Windows' do before(:each) do allow(Pwsh::Util).to receive(:on_windows?).and_return(true) - allow(Pwsh::Manager).to receive(:powershell_path).and_return('pwsh') - allow(Pwsh::Manager).to receive(:powershell_args).and_return('args') + allow(Pwsh::Manager).to receive_messages(powershell_path: 'pwsh', powershell_args: 'args') end it 'Initializes an instance of the Pwsh::Manager' do From 46227a2b7b6f77fb30b14162fb7b1249e7a9992b Mon Sep 17 00:00:00 2001 From: Gavin Didrichsen Date: Mon, 26 Oct 2026 13:03:07 +0000 Subject: [PATCH 10/10] (MAINT) Record the remaining rubocop offences in the todo file Add Style/HashSlice, Lint/UselessConstantScoping, RSpec/SpecFilePathSuffix and RSpec/SpecFilePathFormat (14 offences) to .rubocop_todo.yml, each with the reason it is not changed in this PR. rubocop now reports no offences. Co-Authored-By: Claude Sonnet 5.5 --- .rubocop_todo.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 26f37926..08bbd33a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -220,3 +220,31 @@ Style/RedundantSelfAssignmentBranch: Style/ReturnNilInPredicateMethodDefinition: Exclude: - 'lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb' + +# Added with the pdk update that moved the module to the template's stricter rubocop profile. +# Offense count: 5 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Not applied: slice calls namevar_attributes once instead of once per entry, which breaks a unit test's call count. +Style/HashSlice: + Exclude: + - 'lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb' + +# Offense count: 2 +# `private` has no effect on these constants; using private_constant would change their visibility. +Lint/UselessConstantScoping: + Exclude: + - 'lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb' + +# Offense count: 4 +# The acceptance specs are run by the rake task's file pattern, not by a _spec.rb suffix. +RSpec/SpecFilePathSuffix: + Exclude: + - 'spec/acceptance/dsc/*.rb' + +# Offense count: 3 +# Spec files are named after the module path, not the described class. +RSpec/SpecFilePathFormat: + Exclude: + - 'spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb' + - 'spec/unit/pwsh/version_spec.rb' + - 'spec/unit/pwsh/windows_powershell_spec.rb'