Class: Kitchen::Verifier::Pester

Inherits:
Base
  • Object
show all
Defined in:
lib/kitchen/verifier/pester.rb

Overview

A Test Kitchen verifier that runs Pester tests on the system under test.

The verifier does almost all of its work by generating PowerShell source locally and handing it to the transport to execute remotely. Each command hook -- #install_command, #init_command, #prepare_command and #run_command -- returns a script string rather than performing the work itself.

Test files, helper files and any folders named in copy_folders are staged into a sandbox by #create_sandbox, shipped to the instance, and discovered there through $Env:PSModulePath.

Examples:

configuring the verifier in kitchen.yml


verifier:
  name: pester
  test_folder: tests
  install_modules:
    - PSScriptAnalyzer
  downloads:
    ./PesterTestResults.xml: ./testresults/

See Also:

Instance Method Summary collapse

Constructor Details

#initialize(config = {}) ⇒ Pester

Creates a new Verifier object using the provided configuration data which will be merged with any default configuration.

Parameters:

  • config (Hash) (defaults to: {})

    provided verifier configuration



104
105
106
107
# File 'lib/kitchen/verifier/pester.rb', line 104

def initialize(config = {})
  init_config(config)
  raise ClientError.new "Environment Variables must be specified as a hash, not a #{config[:environment].class}" unless config[:environment].is_a?(Hash)
end

Instance Method Details

#absolute_test_folderString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Resolves test_folder to an absolute path, descending into an integration subfolder when one exists.

Returns:

  • (String)

    absolute path to the folder holding the suites



874
875
876
877
878
879
880
881
882
883
# File 'lib/kitchen/verifier/pester.rb', line 874

def absolute_test_folder
  path = (Pathname.new config[:test_folder]).realpath
  integration_path = File.join(path, "integration")
  Dir.exist?(integration_path) ? integration_path : path.to_s
rescue Errno::ENOENT
  raise UserError, "The verifier's 'test_folder' is set to " \
                   "'#{config[:test_folder]}', which does not exist. It is resolved " \
                   "relative to the directory kitchen runs in, so give it a path that " \
                   "exists there or an absolute one."
end

#call(state) ⇒ void

This method returns an undefined value.

Runs the verifier on the instance, retrieving the test results even when the run fails.

Parameters:

  • state (Hash)

    mutable instance state

Raises:

  • (Kitchen::ActionFailed)

    if the verification failed



262
263
264
265
266
267
268
269
# File 'lib/kitchen/verifier/pester.rb', line 262

def call(state)
  super
rescue
  info("Rescue to download test files.")
  download_test_files(state) unless config[:downloads].nil?
  # Rethrow the original exception; the failure still has to register.
  raise
end

#config_list(key, value) ⇒ Array

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the entries of a config option that is documented as a list.

YAML makes it easy to write a single mapping where a list of mappings was meant -- leaving off the leading - is enough. Array() turns such a mapping into a list of [key, value] pairs, which then renders as nonsense PowerShell instead of failing, so reject it here where we can still say which option is at fault.

Parameters:

  • key (String)

    the option's name, for the error message

  • value (Object)

    the configured value

Returns:

  • (Array)

    the entries to iterate over

Raises:

  • (Kitchen::UserError)

    when a single mapping was given



898
899
900
901
902
903
904
905
# File 'lib/kitchen/verifier/pester.rb', line 898

def config_list(key, value)
  if value.is_a?(Hash)
    raise UserError, "The verifier's '#{key}' must be a list, but a single mapping " \
                     "was given. Put a '- ' in front of each entry in kitchen.yml."
  end

  Array(value)
end

#copy_if_src_exists(src_to_validate, destination) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Copies a folder recursively, preserving its layers. Mostly used to copy into the sandbox. Does nothing when the source does not exist.

Parameters:

  • src_to_validate (String)

    folder to copy

  • destination (String)

    folder to copy into, created if missing



845
846
847
848
849
850
851
852
853
854
855
856
857
858
# File 'lib/kitchen/verifier/pester.rb', line 845

def copy_if_src_exists(src_to_validate, destination)
  unless Dir.exist?(src_to_validate)
    info("The path #{src_to_validate} was not found. Not copying to #{destination}.")
    return
  end

  info("Moving #{src_to_validate} to #{destination}")
  unless Dir.exist?(destination)
    FileUtils.mkdir_p(destination)
    debug("Folder '#{destination}' created.")
  end
  FileUtils.mkdir_p(File.join(destination, "__bugfix"))
  FileUtils.cp_r(src_to_validate, destination, preserve: true)
end

#create_sandboxvoid

This method returns an undefined value.

Creates a temporary directory on the local workstation into which verifier related files and directories can be copied or created. The contents of this directory will be copied over to the instance before invoking the verifier's run command. After this method completes, it is expected that the contents of the sandbox is complete and ready for copy to the remote instance.

Note: any subclasses would be well advised to call super first when overriding this method, for example:

Examples:

overriding #create_sandbox


class MyVerifier < Kitchen::Verifier::Base
  def create_sandbox
    super
    # any further file copies, preparations, etc.
  end
end


129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/kitchen/verifier/pester.rb', line 129

def create_sandbox
  super
  prepare_supporting_psmodules
  prepare_copy_folders
  prepare_pester_tests
  prepare_helpers

  debug("\n\n")
  debug("Sandbox content:\n")
  list_files(sandbox_path).each do |f|
    debug("    #{f}")
  end
end

#download_test_files(state) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Retrieves the configured result files from the instance.

Parameters:

  • state (Hash)

    mutable instance state, used to open the transport connection



647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
# File 'lib/kitchen/verifier/pester.rb', line 647

def download_test_files(state)
  if config[:downloads].nil?
    info("Skipped downloading test result file from #{instance.to_str}; 'downloads' hash is empty.")
    return
  end

  info("Downloading test result files from #{instance.to_str}")
  instance.transport.connection(state) do |conn|
    config[:downloads].each do |remotes, local|
      debug("downloading #{Array(remotes).join(", ")} to #{local}")
      conn.download(remotes, local)
    end
  end

  debug("Finished downloading test result files from #{instance.to_str}")
end

#get_powershell_modules_from_nugetapiArray<String>?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the commands that install the bootstrap modules straight from a NuGet feed.

This runs before PowerShellGet is available, so it uses Install-ModuleFromNuget from PesterUtil.psm1 rather than Install-Module. Each entry of bootstrap.modules may be a plain module name or a hash of parameters.

Returns:

  • (Array<String>, nil)

    one PowerShell fragment per module, or nil when no bootstrap modules are configured



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/kitchen/verifier/pester.rb', line 373

def get_powershell_modules_from_nugetapi
  # don't return anything is the modules subkey or bootstrap is null
  return if config.dig(:bootstrap, :modules).nil?

  bootstrap = config[:bootstrap]
  # if the repository url is set, use that as parameter to Install-ModuleFromNuget. Default is the PSGallery url
  gallery_url_param = bootstrap[:repository_url] ? "-GalleryUrl '#{bootstrap[:repository_url]}'" : ""

  info("Bootstrapping environment without PowerShellGet Provider...")
  config_list("bootstrap.modules", bootstrap[:modules]).map do |powershell_module|
    if powershell_module.is_a? Hash
      module_name = module_name!("bootstrap.modules", powershell_module)
      <<-PS1
        ${#{module_name}} = #{ps_hash(powershell_module)}

        Install-ModuleFromNuget -Module ${#{module_name}} #{gallery_url_param}
      PS1
    else
      <<-PS1
        Install-ModuleFromNuget -Module @{Name = '#{powershell_module}'} #{gallery_url_param}
      PS1
    end
  end
end

#helper_filesArray<String>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns an Array of common helper filenames currently residing on the local workstation.

Returns:

  • (Array<String>)

    array of helper files



696
697
698
699
# File 'lib/kitchen/verifier/pester.rb', line 696

def helper_files
  glob = Dir.glob(File.join(test_folder, "helpers", "*/**/*"))
  glob.reject { |f| File.directory?(f) }
end

#init_commandString

Generates a command string which will perform any data initialization or configuration required after the verifier software is installed but before the sandbox has been transferred to the instance. If no work is required, then nil will be returned.

Returns:

  • (String)

    a command string



189
190
191
# File 'lib/kitchen/verifier/pester.rb', line 189

def init_command
  restart_winrm_service if config[:restart_winrm]
end

#install_commandString

Generates a command string which will install and configure the verifier software on an instance. If no work is required, then nil will be returned. PowerShellGet & Pester Bootstrap are done in prepare_command (after sandbox is transferred) so that we can use the PesterUtil.psm1

Returns:

  • (String)

    a command string



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/kitchen/verifier/pester.rb', line 150

def install_command
  # the sandbox has not yet been copied to the SUT.
  install_command_string = <<-PS1
    Write-Verbose 'Running Install Command...'
    $modulesToRemove = @(
        if ($#{config[:remove_builtin_powershellget]}) {
            Get-module -ListAvailable -FullyQualifiedName @{ModuleName = 'PackageManagement'; RequiredVersion = '1.0.0.1'}
            Get-module -ListAvailable -FullyQualifiedName @{ModuleName = 'PowerShellGet'; RequiredVersion = '1.0.0.1'}
        }

        if ($#{config[:remove_builtin_pester]}) {
            Get-module -ListAvailable -FullyQualifiedName @{ModuleName = 'Pester'; RequiredVersion = '3.4.0'}
        }
    )

    if ($modulesToRemove.ModuleBase.Count -eq 0) {
      # for PS7 on linux
      return
    }

    $modulesToRemove.ModuleBase | Foreach-Object {
        $ModuleBaseLeaf = Split-Path -Path $_ -Leaf
        if ($ModuleBaseLeaf -as [System.version]) {
          Remove-Item -force -Recurse (Split-Path -Parent -Path $_) -ErrorAction SilentlyContinue
        }
        else {
          Remove-Item -force -Recurse $_ -ErrorAction SilentlyContinue
        }
    }
  PS1
  really_wrap_shell_code(Util.outdent!(install_command_string))
end

#install_command_scriptString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the PowerShell that prepares the SUT once the sandbox has been transferred.

Runs after the transfer so that PesterUtil.psm1 is available to import. Composes, in order: the NuGet bootstrap, any PSRepository registration, the Pester install, and any gallery modules. Each section is omitted when its config is nil.

Returns:

  • (String)

    a PowerShell script



604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
# File 'lib/kitchen/verifier/pester.rb', line 604

def install_command_script
  <<-PS1
    $PSModPathToPrepend = "#{config[:root_path]}"

    Import-Module -ErrorAction Stop PesterUtil

    #{get_powershell_modules_from_nugetapi.join("\n") unless config.dig(:bootstrap, :modules).nil?}

    #{register_psrepository_scriptblock.join("\n") unless config[:register_repository].nil?}

    #{install_pester}

    #{install_modules_from_gallery.join("\n") unless config[:install_modules].nil?}
  PS1
end

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

returns a piece of PS scriptblock for each Module to install from gallery that has been specified in install_modules config.

Returns:

  • (Array<String>)

    array of PS commands.



447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/kitchen/verifier/pester.rb', line 447

def install_modules_from_gallery
  return if config[:install_modules].nil?

  config_list("install_modules", config[:install_modules]).map do |powershell_module|
    if powershell_module.is_a? Hash
      # Sanitize variable name so that $powershell-yaml becomes $powershell_yaml
      module_name = module_name!("install_modules", powershell_module).gsub(/[\W]/, "_")
      # so we can splat that variable to install module
      <<-PS1
        $#{module_name} = #{ps_hash(powershell_module)}
        Write-Host -NoNewline 'Installing #{module_name}'
        Install-Module @#{module_name}
        Write-host '... done.'
      PS1
    else
      <<-PS1
        Write-host -NoNewline 'Installing #{powershell_module} ...'
        Install-Module -Name '#{powershell_module}'
        Write-host '... done.'
      PS1
    end
  end
end

#install_pesterString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the string command set the PSGallery as trusted, and Install Pester from gallery based on the params from Pester_install_params config

Returns:

  • (String)

    command to install Pester Module



424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'lib/kitchen/verifier/pester.rb', line 424

def install_pester
  return if config[:skip_pester_install]

  pester_install_params = config[:pester_install] || {}
  <<-PS1
    if ((Get-PSRepository -Name PSGallery).InstallationPolicy -ne 'Trusted') {
        Write-Host -Object "Trusting the PSGallery to install Pester without -Force"
        Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue
    }

    Write-Host "Installing Pester..."
    $installPesterParams = #{ps_hash(pester_install_params)}
    $installPesterParams['Name'] = 'Pester'
    Install-module @installPesterParams
    Write-Host 'Pester Installed.'
  PS1
end

#invoke_pester_scriptblockString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the PowerShell that imports Pester and invokes it.

Two dialects are emitted behind a version check evaluated on the SUT: Pester 4 and earlier take loose parameters, Pester 5 and later take a PesterConfiguration object. The script exits with Pester's failed test count so the transport registers the failure.

Returns:

  • (String)

    a PowerShell script



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/kitchen/verifier/pester.rb', line 280

def invoke_pester_scriptblock
  <<-PS1
    $PesterModule = Import-Module -Name Pester -Force -ErrorAction Stop -PassThru

    $TestPath = Join-Path "#{config[:root_path]}" -ChildPath "suites"
    $OutputFilePath = Join-Path "#{config[:root_path]}" -ChildPath 'PesterTestResults.xml'

    #{ps_environment(config[:environment])}
    if ($PesterModule.Version.Major -le 4)
    {
      Write-Host -Object "Invoke Pester with v$($PesterModule.Version) Options"
      $options = New-PesterOption -TestSuiteName "Pester - #{instance.to_str}"
      $defaultPesterParameters = @{
          Script        = $TestPath
          OutputFile    = $OutputFilePath
          OutputFormat  = 'NUnitXml'
          PassThru      = $true
          PesterOption  = $options
      }

      $pesterCmd = Get-Command -Name 'Invoke-Pester'
      $pesterConfig = #{ps_hash(config[:pester_configuration])}
      $invokePesterParams = @{}

      foreach ($paramName in $pesterCmd.Parameters.Keys)
      {
          $paramValue = $pesterConfig.($paramName)

          if ($paramValue) {
              Write-Host -Object "Using $paramName from Yaml config."
              $invokePesterParams[$paramName] = $paramValue
          }
          elseif ($defaultPesterParameters.ContainsKey($paramName))
          {
              Write-Host -Object "Using $paramName from Defaults: $($defaultPesterParameters[$paramName])."
              $invokePesterParams[$paramName] = $defaultPesterParameters[$paramName]
          }
      }

      $result = Invoke-Pester @invokePesterParams
    }
    else
    {
      Write-Host -Object "Invoke Pester with v$($PesterModule.Version) Configuration."
      $pesterConfigHash = #{ps_hash(config[:pester_configuration])}

      if (-not $pesterConfigHash.ContainsKey('run')) {
          $pesterConfigHash['run'] = @{}
      }

      if (-not $pesterConfigHash.ContainsKey('TestResult')) {
          $pesterConfigHash['TestResult'] = @{}
      }

      if (-not $pesterConfigHash.run.path) {
          $pesterConfigHash['run']['path'] = $TestPath
      }

      if (-not $pesterConfigHash.TestResult.TestSuiteName) {
          $pesterConfigHash['TestResult']['TestSuiteName'] = 'Pester - #{instance.to_str}'
      }

      if (-not $pesterConfigHash.TestResult.OutputPath) {
          $pesterConfigHash['TestResult']['OutputPath'] = $OutputFilePath
      }

      $PesterConfig = New-PesterConfiguration -Hashtable $pesterConfigHash
      $result = Invoke-Pester -Configuration $PesterConfig
    }

    $resultXmlPath = (Join-Path -Path $TestPath -ChildPath 'result.xml')
    if (Test-Path -Path $resultXmlPath) {
      $result | Export-CliXml -Path $resultXmlPath
    }

    $LASTEXITCODE = $result.FailedCount
    $host.SetShouldExit($LASTEXITCODE)

    exit $LASTEXITCODE
  PS1
end

#list_files(path) ⇒ Array<String>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Creates a flat list of the files contained in a folder.

Useful when debugging what has actually been copied to the sandbox.

Parameters:

  • path (String)

    the folder to list

Returns:

  • (Array<String>)

    paths of the entries at the top level and nested beneath it



811
812
813
814
815
# File 'lib/kitchen/verifier/pester.rb', line 811

def list_files(path)
  base_directory_content = Dir.glob(File.join(path, "*"))
  nested_directory_content = Dir.glob(File.join(path, "*/**/*"))
  [base_directory_content, nested_directory_content].flatten
end

#module_name!(key, entry) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the Name of a mapping-shaped entry in one of the module or repository lists.

The name becomes a PowerShell variable that the generated script splats, so a missing one either blows up here or emits an empty ${} that fails on the instance a long way from its cause.

Parameters:

  • key (String)

    the option's name, for the error message

  • entry (Hash)

    the entry to read the name from

Returns:

  • (String)

    the entry's Name

Raises:

  • (Kitchen::UserError)

    when the entry is not a mapping, or has no usable Name



920
921
922
923
924
925
926
927
928
929
930
931
932
933
# File 'lib/kitchen/verifier/pester.rb', line 920

def module_name!(key, entry)
  unless entry.is_a?(Hash)
    raise UserError, "Every entry under the verifier's '#{key}' must be a mapping with " \
                     "at least a 'Name'; #{entry.inspect} is a #{entry.class}."
  end

  name = entry[:Name] || entry["Name"]
  if name.to_s.empty?
    raise UserError, "Every mapping under the verifier's '#{key}' needs a 'Name'; " \
                     "#{entry.inspect} has none."
  end

  name.to_s
end

#pad(depth = 0) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a run of spaces of the given width, used to pad messages and indent generated PowerShell hashtables.

Parameters:

  • depth (Integer) (defaults to: 0)

    number of spaces

Returns:

  • (String)

    the padding



955
956
957
# File 'lib/kitchen/verifier/pester.rb', line 955

def pad(depth = 0)
  " " * depth
end

#prepare_commandString

Generates a command string which will perform any commands or configuration required just before the main verifier run command but after the sandbox has been transferred to the instance. If no work is required, then nil will be returned.

Returns:

  • (String)

    a command string



199
200
201
202
203
# File 'lib/kitchen/verifier/pester.rb', line 199

def prepare_command
  info("Preparing the SUT and Pester dependencies...")
  resolve_downloads_paths!
  really_wrap_shell_code(install_command_script)
end

#prepare_copy_foldersvoid

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Copies the folders named in copy_folders into the sandbox's modules folder, so they can be discovered through the updated $Env:PSModulePath.



791
792
793
794
795
796
797
798
799
800
801
# File 'lib/kitchen/verifier/pester.rb', line 791

def prepare_copy_folders
  return if config[:copy_folders].nil?

  info("Preparing to copy specified folders to #{sandbox_module_path}.")
  kitchen_root_path = config[:kitchen_root]
  config_list("copy_folders", config[:copy_folders]).each do |folder|
    debug("copying #{folder}")
    folder_to_copy = File.join(kitchen_root_path, folder)
    copy_if_src_exists(folder_to_copy, sandbox_module_path)
  end
end

#prepare_helpersvoid

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Copies all common testing helper files into the suites directory in the sandbox, stripping the helpers/ prefix from their paths.



706
707
708
709
710
711
712
713
714
715
# File 'lib/kitchen/verifier/pester.rb', line 706

def prepare_helpers
  base = File.join(test_folder, "helpers")

  helper_files.each do |src|
    dest = File.join(sandbox_path, src.sub("#{base}/", ""))
    debug("Copying #{src} to #{dest}")
    FileUtils.mkdir_p(File.dirname(dest))
    FileUtils.cp(src, dest, preserve: true)
  end
end

#prepare_pester_testsvoid

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Copies all test suite files into the suites directory in the sandbox.



821
822
823
824
825
# File 'lib/kitchen/verifier/pester.rb', line 821

def prepare_pester_tests
  info("Preparing to copy files from  '#{suite_test_folder}' to the SUT.")
  sandboxed_suites_path = File.join(sandbox_path, "suites")
  copy_if_src_exists(suite_test_folder, sandboxed_suites_path)
end

#prepare_supporting_psmodulesvoid

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Copies PesterUtil.psm1 into the sandbox's modules folder, where the updated $Env:PSModulePath will find it.



832
833
834
835
836
# File 'lib/kitchen/verifier/pester.rb', line 832

def prepare_supporting_psmodules
  info("Preparing to copy files from '#{support_psmodule_folder}' to the SUT.")
  sandbox_module_path = File.join(sandbox_path, "modules")
  copy_if_src_exists(support_psmodule_folder, sandbox_module_path)
end

#ps_environment(obj) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Creates environment variable assignments from a ruby map.

Parameters:

  • obj (Hash)

    variable names mapped to their values

Returns:

  • (String)

    newline-separated $env:NAME = 'value' assignments



754
755
756
757
758
759
760
# File 'lib/kitchen/verifier/pester.rb', line 754

def ps_environment(obj)
  commands = obj.map do |k, v|
    "$env:#{k} = #{ps_single_quote(v)}"
  end

  commands.join("\n")
end

#ps_hash(obj, depth = 0) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Renders a Ruby value as PowerShell source.

Hashes become hashtables, arrays become arrays, booleans become $true or $false, and everything else is quoted as a string -- PowerShell is generally able to coerce it back to the type it needs.

Parameters:

  • obj (Object)

    the value to render

  • depth (Integer) (defaults to: 0)

    current nesting depth, used for indentation

Returns:

  • (String)

    PowerShell source for the value



727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
# File 'lib/kitchen/verifier/pester.rb', line 727

def ps_hash(obj, depth = 0)
  if [true, false].include? obj
    %{$#{obj}} # Return $true or $false when value is a bool
  elsif obj.is_a?(Hash)
    obj.map do |k, v|
      # Format "Key = Value" enabling recursion
      %{#{pad(depth + 2)}#{ps_hash(k)} = #{ps_hash(v, depth + 2)}}
    end
      .join("\n") # append \n to the key/value definitions
      .insert(0, "@{\n") # prepend @{\n
      .insert(-1, "\n#{pad(depth)}}\n") # append \n}\n

  elsif obj.is_a?(Array)
    array_string = obj.map { |v| ps_hash(v, depth + 4) }.join(",")
    "#{pad(depth)}@(\n#{array_string}\n)"
  else
    # When the object is not a string nor a hash or array, it will be quoted as a string.
    # In most cases, PS is smart enough to convert back to the type it needs.
    ps_single_quote(obj)
  end
end

#ps_single_quote(value) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Renders a value as a single-quoted PowerShell string literal.

PowerShell escapes a literal quote inside a single-quoted string by doubling it. Without this an apostrophe anywhere in the config -- a module name, an environment value, a password -- closes the string early and corrupts the rest of the generated script.

Parameters:

  • value (Object)

    any value; #to_s is used

Returns:

  • (String)

    a quoted, escaped PowerShell string literal



772
773
774
# File 'lib/kitchen/verifier/pester.rb', line 772

def ps_single_quote(value)
  "'#{value.to_s.gsub("'", "''")}'"
end

#really_wrap_posix_shell_code(code) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Wraps PowerShell for a non-Windows instance.

Writes the payload to kitchen_cmd.ps1 through a quoted heredoc, so the POSIX shell does not interpolate PowerShell variables, adds a pwsh shebang and invokes it.

Parameters:

  • code (String)

    the PowerShell to run on the instance

Returns:

  • (String)

    a shell command string



549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/kitchen/verifier/pester.rb', line 549

def really_wrap_posix_shell_code(code)
  my_command = <<-BASH
    echo "Running as '$(whoami)'"
    # create the modules folder, making sure it's done as current user (not root)
    mkdir -p #{config[:root_path]}/modules
    cd #{config[:root_path]}
    # Send the bash heredoc 'EOF' to the file kitchen_cmd.ps1 using the tool cat
    cat << 'EOF' > kitchen_cmd.ps1
    #!/usr/bin/env pwsh
    #{Util.outdent!(use_local_powershell_modules(code))}
    EOF
    chmod +x kitchen_cmd.ps1
    # Invoke the created kitchen_cmd.ps1 file using pwsh
    #{shell_cmd} ./kitchen_cmd.ps1
  BASH

  debug(Util.outdent!(my_command))
  Util.outdent!(my_command)
end

#really_wrap_shell_code(code) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Wraps generated PowerShell in the platform's shell invocation.

Parameters:

  • code (String)

    the PowerShell to run on the instance

Returns:

  • (String)

    a shell command string



483
484
485
# File 'lib/kitchen/verifier/pester.rb', line 483

def really_wrap_shell_code(code)
  windows_os? ? really_wrap_windows_shell_code(code) : really_wrap_posix_shell_code(code)
end

#really_wrap_windows_shell_code(code) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Wraps PowerShell for a Windows instance.

The payload is written to kitchen_cmd.ps1 and invoked, rather than passed on the command line, so that quoting and length limits do not apply to it.

Parameters:

  • code (String)

    the PowerShell to run on the instance

Returns:

  • (String)

    a shell command string



515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
# File 'lib/kitchen/verifier/pester.rb', line 515

def really_wrap_windows_shell_code(code)
  my_command = <<-PWSH
    echo "Running as '$(whoami)'..."
    New-Item -ItemType Directory -Path '#{config[:root_path]}/modules' -Force -ErrorAction SilentlyContinue
    Set-Location -Path "#{config[:root_path]}"
    # Send the pwsh here string to the file kitchen_cmd.ps1
    @'
    try {
        if (@('Bypass', 'Unrestricted') -notcontains (Get-ExecutionPolicy)) {
            Set-ExecutionPolicy Unrestricted -Force -Scope Process
        }
    }
    catch {
        $_ | Out-String | Write-Warning
    }
    #{Util.outdent!(use_local_powershell_modules(code))}
    '@ | Set-Content -Path kitchen_cmd.ps1 -Encoding utf8 -Force -ErrorAction 'Stop'
    # create the modules folder, making sure it's done as current user (not root)
    #
    # Invoke the created kitchen_cmd.ps1 file using pwsh
    #{shell_cmd} ./kitchen_cmd.ps1
  PWSH
  wrap_shell_code(Util.outdent!(my_command))
end

#register_psrepository_scriptblockArray<String>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the string command to set a PS Repository for each PSRepo configured.

Returns:

  • (Array<String>)

    array of suite files



403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'lib/kitchen/verifier/pester.rb', line 403

def register_psrepository_scriptblock
  return if config[:register_repository].nil?

  info("Registering a new PowerShellGet Repository")
  config_list("register_repository", config[:register_repository]).map do |psrepo|
    repo_name = module_name!("register_repository", psrepo)
    # Using Set-PSRepo from ../../*/*/*/PesterUtil.psm1
    debug("Command to set PSRepo #{repo_name}.")
    <<-PS1
      Write-Host 'Registering psrepo #{repo_name}...'
      ${#{repo_name}} = #{ps_hash(psrepo)}
      Set-PSRepo -Repository ${#{repo_name}}
    PS1
  end
end

#remote_basename(path) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the final segment of a path that lives on the SUT.

File.basename applies the workstation's separator rules, so a Windows remote path such as 'C:\results\out.xml' comes back unchanged when kitchen runs on macOS or Linux -- the usual case for a Windows SUT. Split on either separator instead.

Parameters:

  • path (String)

    a path as it exists on the instance

Returns:

  • (String)

    the last path segment



945
946
947
# File 'lib/kitchen/verifier/pester.rb', line 945

def remote_basename(path)
  path.to_s.split(%r{[\\/]}).last.to_s
end

#resolve_downloads_paths!nil

Resolves the remote Downloads path from the verifier root path, unless they're absolute path (starts with / or C:) This updates the config, nothing (nil) is returned.

Returns:

  • (nil)

    updates config downloads



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/kitchen/verifier/pester.rb', line 219

def resolve_downloads_paths!
  info("Resolving Downloads path from config.")
  config[:downloads] = config[:downloads]
    .map do |source, destination|
      source = source.to_s
      if destination.nil?
        raise UserError, "The verifier's 'downloads' entry for '#{source}' has no local " \
                         "destination. Every entry needs one, for example " \
                         "'#{source}: ./testresults/'."
      end

      destination = destination.to_s.gsub("%{instance_name}", instance.name)
      info("  resolving remote source's absolute path.")
      unless source.match?(%r{^/|^[a-zA-Z]:[\\/]}) # is Absolute?
        info("  '#{source}' is a relative path, resolving to: #{File.join(config[:root_path], source)}")
        source = File.join(config[:root_path], source.to_s).to_s
      end

      if destination.match?(%r{[\\/]$}) # is Folder (ends with / or \)
        # Append to the separator the user already supplied. File.join
        # would add a second one, of whichever flavour the workstation
        # happens to use.
        destination = "#{destination}#{remote_basename(source)}"
      end
      info("  Destination: #{destination}")
      if !File.directory?(File.dirname(destination))
        FileUtils.mkdir_p(File.dirname(destination))
      else
        info("  Directory #{File.dirname(destination)} seems to exist.")
      end

      [ source, destination ]
    end
    .to_h # Hash#map yields pairs; keep :downloads the hash it started as
  nil # make sure we do not return anything
end

#restart_winrm_serviceString?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the command that schedules and runs a WinRM restart.

The restart is driven through a scheduled task so that it survives the WinRM session being torn down by the restart itself.

Returns:

  • (String, nil)

    a shell command string, or nil on a non-Windows instance



628
629
630
631
632
633
634
635
636
637
638
639
# File 'lib/kitchen/verifier/pester.rb', line 628

def restart_winrm_service
  return unless windows_os?

  cmd = "schtasks /Create /TN restart_winrm /TR " \
        '"powershell -Command Restart-Service winrm" ' \
        "/SC ONCE /ST 00:00 "
  wrap_shell_code(Util.outdent!(<<-CMD
    #{cmd}
    schtasks /RUN /TN restart_winrm
  CMD
                               ))
end

#run_commandString

Generates a command string which will invoke the main verifier command on the prepared instance. If no work is required, then nil will be returned.

Returns:

  • (String)

    a command string



210
211
212
# File 'lib/kitchen/verifier/pester.rb', line 210

def run_command
  really_wrap_shell_code(invoke_pester_scriptblock)
end

#sandbox_module_pathString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the path of the modules subfolder in the sandbox, where PS modules and folders will be copied to.

Returns:

  • (String)

    absolute path to the sandbox's modules folder



781
782
783
# File 'lib/kitchen/verifier/pester.rb', line 781

def sandbox_module_path
  File.join(sandbox_path, "modules")
end

#script_rootstring

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the current file's parent folder's full path.

Returns:

  • (string)


678
679
680
# File 'lib/kitchen/verifier/pester.rb', line 678

def script_root
  @script_root ||= File.dirname(__FILE__)
end

#shell_cmdString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the shell binary used to run the generated script.

An explicit shell config wins, which allows pwsh-preview or a full path to a specific binary. Otherwise Windows uses powershell and every other platform uses pwsh. sudo is honoured everywhere except the Windows branch, where it is meaningless.

Returns:

  • (String)

    the shell command, prefixed with sudo when configured



496
497
498
499
500
501
502
503
504
# File 'lib/kitchen/verifier/pester.rb', line 496

def shell_cmd
  if !config[:shell].nil?
    config[:sudo] ? "sudo #{config[:shell]}" : "#{config[:shell]}"
  elsif windows_os?
    "powershell"
  else
    config[:sudo] ? "sudo pwsh" : "pwsh"
  end
end

#suite_test_folderArray<String>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns an Array of test suite filenames for the related suite currently residing on the local workstation. Any special provisioner-specific directories (such as a Chef roles/ directory) are excluded.

Returns:

  • (Array<String>)

    array of suite files



670
671
672
# File 'lib/kitchen/verifier/pester.rb', line 670

def suite_test_folder
  @suite_test_folder ||= File.join(test_folder, config[:suite_name])
end

#support_psmodule_folderstring

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the absolute path of the Supporting PS module to be copied to the SUT via the Sandbox.

Returns:

  • (string)


687
688
689
# File 'lib/kitchen/verifier/pester.rb', line 687

def support_psmodule_folder
  @support_psmodule_folder ||= Pathname.new(File.join(script_root, "../../support/modules/PesterUtil")).cleanpath
end

#test_folderString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the folder containing the test suites, falling back to test_base_path when test_folder is not set.

Returns:

  • (String)

    path to the folder holding the suites



865
866
867
# File 'lib/kitchen/verifier/pester.rb', line 865

def test_folder
  config[:test_folder].nil? ? config[:test_base_path] : absolute_test_folder
end

#use_local_powershell_modules(script) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Prefixes a script with the preamble that makes the sandbox's modules folder importable.

Parameters:

  • script (String)

    the PowerShell to run after the preamble

Returns:

  • (String)

    the script with the PSModulePath preamble prepended



575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
# File 'lib/kitchen/verifier/pester.rb', line 575

def use_local_powershell_modules(script)
  <<-PS1
    Write-Host -Object ("{0} - PowerShell {1}" -f $PSVersionTable.OS,$PSVersionTable.PSVersion)
    $global:ProgressPreference = 'SilentlyContinue'
    $PSModPathToPrepend = Join-Path "#{config[:root_path]}" -ChildPath 'modules'
    Write-Verbose "Adding '$PSModPathToPrepend' to `$Env:PSModulePath."
    if (!$isLinux -and -not (Test-Path -Path $PSModPathToPrepend)) {
      # if you create this folder now in Linux, it may run as root (via sudo).
      $null = New-Item -Path $PSModPathToPrepend -Force -ItemType Directory
    }

    if ($Env:PSModulePath.Split([io.path]::PathSeparator) -notcontains $PSModPathToPrepend) {
      $env:PSModulePath   = @($PSModPathToPrepend, $env:PSModulePath) -Join [io.path]::PathSeparator
    }

    #{script}
  PS1
end