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



866
867
868
869
870
# File 'lib/kitchen/verifier/pester.rb', line 866

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
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



256
257
258
259
260
261
262
263
# File 'lib/kitchen/verifier/pester.rb', line 256

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

#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



837
838
839
840
841
842
843
844
845
846
847
848
849
850
# File 'lib/kitchen/verifier/pester.rb', line 837

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



639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
# File 'lib/kitchen/verifier/pester.rb', line 639

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



367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/kitchen/verifier/pester.rb', line 367

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...")
  Array(bootstrap[:modules]).map do |powershell_module|
    if powershell_module.is_a? Hash
      <<-PS1
        ${#{powershell_module[:Name]}} = #{ps_hash(powershell_module)}

        Install-ModuleFromNuget -Module ${#{powershell_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



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

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



596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
# File 'lib/kitchen/verifier/pester.rb', line 596

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.



439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/kitchen/verifier/pester.rb', line 439

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

  Array(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 = powershell_module[:Name].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



416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/kitchen/verifier/pester.rb', line 416

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



274
275
276
277
278
279
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
# File 'lib/kitchen/verifier/pester.rb', line 274

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



803
804
805
806
807
# File 'lib/kitchen/verifier/pester.rb', line 803

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

#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



892
893
894
# File 'lib/kitchen/verifier/pester.rb', line 892

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.



783
784
785
786
787
788
789
790
791
792
793
# File 'lib/kitchen/verifier/pester.rb', line 783

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[: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.



698
699
700
701
702
703
704
705
706
707
# File 'lib/kitchen/verifier/pester.rb', line 698

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.



813
814
815
816
817
# File 'lib/kitchen/verifier/pester.rb', line 813

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.



824
825
826
827
828
# File 'lib/kitchen/verifier/pester.rb', line 824

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



746
747
748
749
750
751
752
# File 'lib/kitchen/verifier/pester.rb', line 746

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



719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
# File 'lib/kitchen/verifier/pester.rb', line 719

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



764
765
766
# File 'lib/kitchen/verifier/pester.rb', line 764

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



541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/kitchen/verifier/pester.rb', line 541

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



475
476
477
# File 'lib/kitchen/verifier/pester.rb', line 475

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



507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# File 'lib/kitchen/verifier/pester.rb', line 507

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



396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/kitchen/verifier/pester.rb', line 396

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

  info("Registering a new PowerShellGet Repository")
  Array(config[:register_repository]).map do |psrepo|
    # Using Set-PSRepo from ../../*/*/*/PesterUtil.psm1
    debug("Command to set PSRepo #{psrepo[:Name]}.")
    <<-PS1
      Write-Host 'Registering psrepo #{psrepo[:Name]}...'
      ${#{psrepo[:Name]}} = #{ps_hash(psrepo)}
      Set-PSRepo -Repository ${#{psrepo[: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



882
883
884
# File 'lib/kitchen/verifier/pester.rb', line 882

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
# 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
      destination = destination.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



620
621
622
623
624
625
626
627
628
629
630
631
# File 'lib/kitchen/verifier/pester.rb', line 620

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



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

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)


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

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



488
489
490
491
492
493
494
495
496
# File 'lib/kitchen/verifier/pester.rb', line 488

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



662
663
664
# File 'lib/kitchen/verifier/pester.rb', line 662

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)


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

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



857
858
859
# File 'lib/kitchen/verifier/pester.rb', line 857

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



567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
# File 'lib/kitchen/verifier/pester.rb', line 567

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