Class: Kitchen::Verifier::Pester

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

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


59
60
61
# File 'lib/kitchen/verifier/pester.rb', line 59

def initialize(config = {})
  init_config(config)
end

Instance Method Details

#absolute_test_folderObject

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 relative folders containing the test suites, use default i not set.


566
567
568
569
570
# File 'lib/kitchen/verifier/pester.rb', line 566

def absolute_test_folder
  path = (Pathname.new config[:test_folder]).realpath
  integration_path = File.join(path, "integration")
  Dir.exist?(integration_path) ? integration_path : path
end

#call(state) ⇒ Object


168
169
170
171
172
# File 'lib/kitchen/verifier/pester.rb', line 168

def call(state)
  super
ensure
  download_test_files(state) unless config[:download].nil?
end

#copy_if_src_exists(src_to_validate, destination) ⇒ Object

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.

Copies a folder recursively preserving its layers, mostly used to copy to the sandbox.


539
540
541
542
543
544
545
546
547
548
549
550
551
552
# File 'lib/kitchen/verifier/pester.rb', line 539

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

  debug("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_sandboxObject

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

81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/kitchen/verifier/pester.rb', line 81

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) ⇒ Object


391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/kitchen/verifier/pester.rb', line 391

def download_test_files(state)
  return if config[:downloads].nil?

  info("Downloading test result files from #{instance.to_str}")
  instance.transport.connection(state) do |conn|
    config[:downloads].to_h.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_nugetapiObject


206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/kitchen/verifier/pester.rb', line 206

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


437
438
439
440
# File 'lib/kitchen/verifier/pester.rb', line 437

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


141
142
143
# File 'lib/kitchen/verifier/pester.rb', line 141

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


102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/kitchen/verifier/pester.rb', line 102

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_scriptObject


362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/kitchen/verifier/pester.rb', line 362

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.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 sepcified in install_modules config.

Returns:

  • (Array<String>)

    array of PS commands.


277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/kitchen/verifier/pester.rb', line 277

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


254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/kitchen/verifier/pester.rb', line 254

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

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

returns an array of string Creates a flat list of files contained in a folder. This is useful when trying to debug what has been copied to the sandbox.

Returns:

  • (Array<String>)

    array of files in a folder


514
515
516
517
518
# File 'lib/kitchen/verifier/pester.rb', line 514

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) ⇒ Object

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 string of space of the specified depth. This is used to pad messages or when building PS hashtables.


576
577
578
# File 'lib/kitchen/verifier/pester.rb', line 576

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


151
152
153
154
# File 'lib/kitchen/verifier/pester.rb', line 151

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

#prepare_copy_foldersObject

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.

copy files into the 'modules' folder of the sandbox, so that copied folders can be discovered with the updated $Env:PSModulePath.


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

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_helpersObject

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.

Copies all common testing helper files into the suites directory in the sandbox.


446
447
448
449
450
451
452
453
454
455
# File 'lib/kitchen/verifier/pester.rb', line 446

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_testsObject

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.

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


523
524
525
526
527
# File 'lib/kitchen/verifier/pester.rb', line 523

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_psmodulesObject


529
530
531
532
533
# File 'lib/kitchen/verifier/pester.rb', line 529

def prepare_supporting_psmodules
  debug("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_hash(obj, depth = 0) ⇒ Object

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 PowerShell hashtable from a ruby map. The only types supported for now are hash, array, string and Boolean.


461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
# File 'lib/kitchen/verifier/pester.rb', line 461

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.
    "'" + obj.to_s + "'"
  end
end

#really_wrap_posix_shell_code(code) ⇒ Object

Writing the command to a ps1 file, adding the pwsh shebang invoke the file


311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/kitchen/verifier/pester.rb', line 311

def really_wrap_posix_shell_code(code)
  if config[:sudo]
    pwsh_cmd = "sudo pwsh"
  else
    pwsh_cmd = "pwsh"
  end

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

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

#really_wrap_shell_code(code) ⇒ Object


301
302
303
# File 'lib/kitchen/verifier/pester.rb', line 301

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) ⇒ Object


305
306
307
# File 'lib/kitchen/verifier/pester.rb', line 305

def really_wrap_windows_shell_code(code)
  wrap_shell_code(Util.outdent!(use_local_powershell_modules(code)))
end

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


234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/kitchen/verifier/pester.rb', line 234

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

#restart_winrm_serviceObject


378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/kitchen/verifier/pester.rb', line 378

def restart_winrm_service
  return unless verifier.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


161
162
163
# File 'lib/kitchen/verifier/pester.rb', line 161

def run_command
  really_wrap_shell_code(run_command_script)
end

#run_command_scriptObject

private


187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/kitchen/verifier/pester.rb', line 187

def run_command_script
  <<-PS1
    Import-Module -Name Pester -Force -ErrorAction Stop

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

    $options = New-PesterOption -TestSuiteName "Pester - #{instance.to_str}"

    $result = Invoke-Pester -Script $TestPath -OutputFile $OutputFilePath -OutputFormat NUnitXml -PesterOption $options -PassThru
    $result | Export-CliXml -Path (Join-Path -Path $TestPath -ChildPath 'result.xml')

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

    exit $LASTEXITCODE
  PS1
end

#sandbox_module_pathObject

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.


487
488
489
# File 'lib/kitchen/verifier/pester.rb', line 487

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)

419
420
421
# File 'lib/kitchen/verifier/pester.rb', line 419

def script_root
  @script_root ||= File.dirname(__FILE__)
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


411
412
413
# File 'lib/kitchen/verifier/pester.rb', line 411

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)

428
429
430
# File 'lib/kitchen/verifier/pester.rb', line 428

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

#test_folderObject

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 folders containing the test suites, use default if not set.


558
559
560
# File 'lib/kitchen/verifier/pester.rb', line 558

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

#use_local_powershell_modules(script) ⇒ Object


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 335

def use_local_powershell_modules(script)
  <<-PS1
    try {
      if (!$IsLinux -and !$IsMacOs) {
        Set-ExecutionPolicy Unrestricted -force
      }
    }
    catch {
        $_ | Out-String | Write-Warning
    }

    $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 un Linux, it will 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